Modal opens a dialog over the page and locks the scroll behind it. The dialog, ModalA, is rendered once by the Layout outside #swup, so it survives page transitions; every page only needs buttons. The opening, closing, the Escape key and the scroll lock come from the @terrahq/modal package. The class in src/js/handler/modal creates one instance per click and opens it with the button as trigger.
The content is where the handler does its work. Before the dialog opens, beforeOpen reads data-modal-variation from the button that was clicked and prepares the content for it: the default paragraph, a video, or a slider. On close everything injected is removed, so the next button finds the dialog clean. Each variation is a small function in variations/index.js, which keeps Handler.js a switch.
The markup
A button with the js--modal-button hook and data-modal-open set to the id of the dialog. The hook is for the handler; the attribute is what the package reads.
<!-- The trigger: the hook for the handler, and the id of the dialog to open for the package -->
<button class="c--btn-a js--modal-button" data-modal-open="my-modal">Open modal</button>
The dialog is already on every page. Its id is what the buttons point to; c--dialog-a__content is where the variations inject their content.
<!-- ModalA: rendered once by the Layout, outside #swup. Anything with data-modal-close closes it -->
<div id="my-modal" class="c--modal-a js--modal" aria-hidden="true">
<div class="c--modal-a__overlay" data-modal-close></div>
<div class="c--dialog-a">
<h2 class="c--dialog-a__title">Modal Title</h2>
<button class="c--dialog-a__btn" data-modal-close>×</button>
<div class="c--dialog-a__content">
<p>…</p>
</div>
</div>
</div>
The handler
Handler.js gives the package the dialog element and the open class, and hooks the two callbacks. beforeOpen receives the dialog and the trigger, which is how it knows which button asked and which variation to build:
this.config = ({element}) => ({
selector: document.querySelector(".c--modal-a"),
openClass: "c--modal-a--is-active",
beforeOpen: (modalEl, trigger) => {
const dialogContent = modalEl.querySelector(".c--dialog-a__content");
const variation = trigger?.element?.getAttribute("data-modal-variation") || "default";
const paragraph = dialogContent.querySelector("p");
resetDialogContent({ dialogContent, paragraph });
switch (variation) {
case "video":
videoVariation({ boostify: this.boostify, dialogContent, paragraph, trigger });
break;
case "slider":
sliderVariation({ eventSystem: this.eventSystem, dialogContent, paragraph });
break;
case "hubspot":
hubspotVariation({ boostify: this.boostify, dialogContent, paragraph, trigger });
break;
default:
defaultVariation({paragraph})
}
},
onClose: (modalEl, trigger) => {
handleOnClose({
modalEl,
trigger,
eventSystem: this.eventSystem
});
},
});
What the framework does for you
- Lazy loading on click. Boostify uses the
click method here: nothing is imported on scroll. The first click on a button loads Modal.js and the package, builds the instance and opens the dialog in one go. - Reopening. On close the instance is destroyed through the
Modal:destroy event, but the click triggers stay alive, so the next click builds a fresh instance. No stale state between openings. - Page transitions. Before swup replaces the content the instances and the click triggers of the old page are destroyed; the new page registers its own buttons. The dialog itself is never replaced.
- Cross-handler events. A variation can ask another handler for help through the event system: the slider variation fires
Slider:load on open and Slider:destroy on close.