🛠️ Modal: Exposing properties and methods

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Modal: Exposing properties and methods

Sometimes we have to expose properties and methods to our users. When we expose these properties and methods, we want to be careful about what we expose.

For example, imagine you have a clock. As a user, you don’t care about what goes inside the clock. You only care about two things:

  1. You need to be able to read the time
  2. You need to be able to set the time

If we’re the creators of this clock, we only need to expose the clock’s face (for reading time) and a dial (for setting time). We don’t need to expose the internals and gears and allow users to mess with them.

If we expose internals, users will be able to reach in and use things we don’t want them to use. When this happens, we say there’s tight coupling.

Coupling is simply a term that’s loosely tied to the amount of inter-connectivity between components. There’s no absolute measure for coupling, so everything is relative.

Tight coupling here means people can reach in and use the internals. Loose coupling means people can only use the APIs that are exposed.

Exposing the Modal’s properties

Our modals contains the following properties and methods:

  1. isOpen
  2. siblingElements
  3. showSiblingElements
  4. hideSiblingElements
  5. open
  6. close

Which methods should we expose?

If we want to expose everything, we can simply return the modal object from Modal. Users will get access to all 6 properties.

// modal.js
export default function Modal () {
  return modal
}
// main.js
const timedModal = Modal({
  type: 'timed',
  delayBeforeOpening: 1000,
  modalElement: document.querySelector('#timed-modal')
})

console.log(timedModal)
Expose all methods.

But does the user need all 6 properties / methods?

Most likely no. The user doesn’t need to know isOpen or getSiblingElements because those functions are used internally within the Modal component.

The user, however, may need to know the open and close method. So we can choose to expose only these two methods by returning an object.

export default function Modal () {
  return {
    open: modal.open,
    close: modal.close
  }
}
// main.js
console.log(timedModal)
Exposed only open and close methods

Why expose close

It’s not important to expose open because each Modal has a unique way of opening. This unique way of opening is built into each Modal type already.

For example, Timed modals opens automatically after a delay.

However, it is important to expose close because users may need this close method.

For example, the timed modal contains an Ok button. If you click the Ok button, you’ll expect the modal to close.

Timed modal contains ok button

When we expose close, users can add an event listener to the Ok button and use the close method to close the modal.

const timedModal = Modal({
  type: 'timed',
  // Other properties
})

// Get the Ok button
const timedModalElement = document.querySelector('#timed-modal')
const timedModalOkButton = timedModalElement.querySelector('.modal__content').querySelector('button')

// Add event listener to Ok button
timedModalOkButton.addEventListener('click', _ => {
  timedModal.close()
})

Exposing HTML Elements

I find it helpful to expose HTML Elements that are declared (or defined) inside my components.

For example, if we expose the modal’s contentElement, users won’t have to search for the content element (again) before they find the Ok button. They can use the element we expose.

Here’s how we can expose contentElement (along with other elements).

function BaseModal () {
  // Declare Variables

  const modal = {
    // Exposes some HTML elements
    modalElement,
    contentElement,
    overlayElement,

    // Other methods
  }

  // Return modal
}
export default function Modal () {
  // ...
  return {
    open: modal.open,
    close: modal.close,
    modalElement: modal.modalElement,
    contentElement: modal.contentElement,
    overlayElement: modal.overlayElement
  }
}

When we expose contentElement, users don’t have to search for the content element again. They can use our contentElement to find the Ok button.

const timedModal = Modal({
  type: 'timed',
  // Other properties
})

// Finds the OK button
const timedModalOkButton = timedModal.contentElement.querySelector('button')

// Allows OK button to close modal
timedModalOkButton.addEventListener('click', _ => {
  timedModal.close()
})

Easier way to expose elements

You can see we exposed HTML Elements twice: Once in BaseModal and once in Modal

function BaseModal () {
  // Declare Variables

  const modal = {
    // Exposes some HTML elements
    modalElement,
    contentElement,
    overlayElement,

    // Other methods
  }

  // Return modal
}
export default function Modal () {
  // ...
  return {
    open: modal.open,
    close: modal.close,
    modalElement: modal.modalElement,
    contentElement: modal.contentElement,
    overlayElement: modal.overlayElement
  }
}

When we code, we don’t want to repeat these declarations because it’s a lot of work! The simple way is to expose everything inside Modal.

export default function Modal () {
  // ...
  return modal
}

But this begets the original question: Are we exposing too many things?

Are we exposing too many things?

In an ideal scenario, we don’t want to expose ANY unnecessary properties or methods. We can achieve this. It just takes a bit more effort (like what we showed above).

There are three other ways you can do this:

  1. Private by convention
  2. True private members with closures
  3. Creating an internals object

Private by convention

We can prepend a property or method with _ to signal to developers that this property or method is intended to be private. Unfortunately, this doesn’t stop users from using these “private” members.

True private members with closures

We can create true private members by using closures. One example here is trapFocus. We can create every method like trapFocus, but this creates a lot of declarations, which can make the component hard to read.

Compare these two and you’ll see:

// Version 1: With closures
function Component () {
  function one () {
    // ...
  }

  function two () {
    // ...
  }

  const component = {
    // ...
  }
}
// Version 2: Current way with methods
function Component () {
  const component = {
    one () {
      // ...
    },

    two () {
      // ...
    }
  }
}

See this? The indentation makes the code easier to parse.

Since the indentation makes code easy to parse, I thought of third method.

Creating an internal object

I need to declare this upfront. I don’t see anyone doing this at all. But I want to share this as a possible way of structuring code.

Here, we create an internal object that stores all properties and methods for private variables.

function BaseComponent () {
  const internal = {
    one () {},
    two () {}
  }

  const component = {
    someMethod () {
      internal.one()
    }
  }
}

We can use internal variables directly like this:

function BaseComponent () {
  // ...
  const component = {
    someMethod () {
      internal.one()
    }
  }
}

We can expose internals to subclasses like this:

function BaseComponent () {
  // ...
  return { internal, component }
}

function SubComponent () {
  const { internal, component } = BaseComponent()

  // Use internal methods
  internal.one()
}

When we expose the component, we omit internal so users don’t see them at all.

export default function Component () {
  // ...
  return component
}

Which way should you use?

All three methods are valid. Ultimately it boils down to personal and team preferences.

You can even expose the entire modal object without filtering properties too. I tend to do this for components that don’t affect many people. (If there’s a small impact, it doesn’t require as much detailed thought).

It’s all a factor of efficiency, necessity, and results. You want to create a balance for yourself. You don’t need to force everything to be “perfect” or “correct”.

That’s it!