When we close a User-triggered modal, we need to set the buttonās aria-expanded attribute.
Timed modals donāt have a buttonElement. We cannot set aria-expanded inside TimedModal. This means everything regarding buttonElement should be moved to UserTriggeredModal.
We used modal.close to close the modal. We did this three times with event listeners inside BaseModal.
When we use modal.close to close the modal in User-triggered modals, we need to set the buttonās aria-expanded attribute back to false.
Iāll bring you through a process to discover the solution I recommend.
Step 1: Creating event listeners in each derivative modal
When we close User-triggered modals, we need to run an extra buttonElement.setAttribute line. But we donāt have to do this when we close Timed modals. This means thereās a difference in requirements when closing modals.
The easiest way to resolve this difference is to create event listeners inside derivative modals.
function BaseModal (settings) {
// ...
// Remove these
closeButtonElement.addEventListener('click', _ => {
modal.close()
})
overlayElement.addEventListener('click', event => {
if (!event.target.closest('.modal')) {
modal.close()
}
})
document.addEventListener('keydown', event => {
if (modal.isOpen && event.key === 'Escape') {
modal.close()
}
})
}
We copy-paste the event listeners into each derivative modal:
function UserTriggeredModal (settings) {
// ...
// Paste code here
}
function TimedModal (settings) {
// ...
// Paste code here
}
For these event listeners to work, we need to declare variables like closeButtonElement and modalOverlay on each derivative modal.
Creating a method with the same name in the derivative class
Call the parentās method
Add any code you wish to add
This process is easy if you used a Class syntax.
// Example for Class syntax
class BaseModal {
// ...
close () { /* ...*/ }
}
class UserTriggeredModal extends BaseModal {
close () {
super.close()
// Add code here
}
}
The process is tougher with Factory Functions. We need to create an instance of BaseModal first. Weāll name this base instead of modal since we want modal to be the eventual object.
function UserTriggeredModal (settings) {
const base = BaseModal(settings)
}
Then, we need to extend base with another object. Normally we can do this with Object.assign.
function UserTriggeredModal (settings) {
const base = BaseModal(settings)
const modal = Object.assign({}, base, {/* ... */})
}
But we cannot use Object.assign here because we used getter functions. Object.assign doesnāt copy getter functions. It copies the results from a getter function.
We need to use a custom function called mix to merge base with a new object.
Hereās the simple version of mix. Thereās also a robust version if you wish to use it. I explained how mix works in this article.
function mix (...sources) {
const result = {}
for (const source of sources) {
const props = Object.keys(source)
for (const prop of props) {
const descriptor = Object.getOwnPropertyDescriptor(source, prop)
Object.defineProperty(result, prop, descriptor)
}
}
return result
}
We can use mix like Object.assign. The difference is: we donāt need to pass an empty object as the first argument. mix creates this empty object automatically.
function UserTriggeredModal (settings) {
const base = BaseModal()
const modal = mix(base, {/* ... */})
}
We can now overwrite the close method by providing a method with the same name.
function UserTriggeredModal (settings) {
const base = BaseModal()
const modal = mix(base, {
close () {
// Overwrites the close method
}
})
}
Then we call the parentās close method (which is base.close) and add any code we wish. In this case, weāll add the buttonElement.setAttribute line.
function UserTriggeredModal (settings) {
const { modalElement, buttonElement } = settings
const base = BaseModal()
const modal = mix(base, {
close () {
base.close()
buttonElement.setAttribute('aria-expanded', false)
buttonElement.focus()
}
})
}
We can now use modal.close in the three event listeners.
Since we overwrote close in UserTriggredModal, it makes sense to overwrite open too.
function UserTriggeredModal (settings) {
// Declare variables
const modal = mix(base, {
open () {
base.open()
buttonElement.setAttribute('aria-expanded', true)
},
// Other methods
})
buttonElement.addEventListener('click', _ => {
modal.open()
})
// Other event listeners
}
Step 3: Adding the event listeners inside Modal
Weāve reduced the amount of code by overwriting the close method in UserTriggeredModal. But thereās still a lot of duplication between UserTriggeredModal and TimedModal.
We can reduce code duplication by deferring the adding of common event listeners after the derivative modals are created. The easiest way is to add event listeners inside Modal instead of derivative modals.
To do this, we need to return the modal in each derivative modal.
function UserTriggeredModal (settings) {
// ...
return modal
}
function TimedModal (settings) {
// ...
return modal
}
We then add event listeners in Modal instead of the derivative modals. You can remove these three event listeners from UserTriggeredModal and TimedModal at this point.
Building other components can be frustrating when you have a Timed Modal on screen. This is because the Timed Modal pops up when you donāt want it to.
An easy way to āfixā this is to create the Timed Modal when a button is clicked.
This is just a neat trick to use for development š.
Bonus 2
Try extending this Modal to include a Scroll-based modal and an Exit Intent modal. These shouldnāt be too hard for you at this point. (You might have to google around to figure out how to build an Exit Intent Modal though).