Skip to content

Handlers System

A Handler is a specialized controller class that bridges the gap between the framework core and individual component instances. Handlers are responsible for:

  1. Lifecycle Management - Initializing and destroying component instances across page transitions
  2. Smart Loading - Determining when to load components (instantly vs. lazy via Boostify)
  3. Instance Tracking - Registering instances with the Manager for centralized memory management
  4. Event Coordination - Listening to framework events (SWUP transitions, content changes)

Think of Handlers as orchestrators: they don’t implement component logic themselves, but rather manage when, where, and how components are created and destroyed.


In projects with SWUP page transitions, components need to be:

  • Re-initialized on every page navigation (new DOM elements)
  • Destroyed properly before page transitions (prevent memory leaks)
  • Loaded efficiently (only when needed, not all upfront)

Handlers solve these problems by providing a consistent pattern that:

✅ Automatically handles SWUP lifecycle
✅ Integrates with Boostify for performance
✅ Centralizes instance management via Manager
✅ Reduces boilerplate code
✅ Ensures memory cleanup


All handlers follow this structure:

Handler (extends CoreHandler)
CoreHandler (provides base functionality)
Main.js (initializes handlers)
Manager (stores instances)

CoreHandler (src/scripts/handler/CoreHandler.js) provides:

  • Instance creation and destruction logic
  • Boostify integration for lazy loading
  • Library loading on-demand
  • Manager integration
  • Debug mode support

All custom handlers extend CoreHandler to inherit this functionality.


Every new handler should start from the template located at:

src/scripts/handler/_handlerFolder/Handler.js

Template Structure:

import CoreHandler from "../CoreHandler";
/**
* TEMPLATE HANDLER
*
* Substitute all elements starting by _ for your desired element and add concrete
* instructions for it
*/
class Handler extends CoreHandler {
constructor(payload) {
super(payload);
this.init();
this.events();
// Configuration for the component instances
this.config = {};
// Or as a callback: this.config = (element) => ({...})
}
get updateTheDOM() {
return {
_libraryElements: document.querySelectorAll(`.js--library`),
};
}
init() {
super.init();
super.getLibraryName("_Library");
}
events() {
super.events();
// When content is replaced (new page loaded)
this.emitter.on("MitterContentReplaced", async () => {
this.DOM = this.updateTheDOM; // Re-query elements
this.Manager.instances._Library = [];
super.assignInstances({
elementGroups: [
{
elements: this.DOM._libraryElements,
config: {
...this.config,
// Additional instance-specific config
},
boostify: { distance: 30 }, // Optional: scroll distance
},
],
});
});
// Before content is replaced (cleanup phase)
this.emitter.on("MitterWillReplaceContent", () => {
if (this.DOM._libraryElements.length) {
super.destroyInstances({ libraryName: '_Library' });
}
});
}
}
export default Handler;

Let’s create a real example - a SliderHandler:

import CoreHandler from "../CoreHandler";
/**
* SliderHandler
*
* Manages the lifecycle of Slider component instances across page transitions.
* Handles initialization, destruction, and lazy loading via Boostify.
*/
class SliderHandler extends CoreHandler {
constructor(payload) {
super(payload);
this.init();
this.events();
// Default configuration for all slider instances
this.config = {
autoplay: true,
speed: 400,
nav: true,
};
}
get updateTheDOM() {
return {
sliderElements: document.querySelectorAll('.js--slider'),
};
}
init() {
super.init();
super.getLibraryName("Slider"); // Must match Manager allocation
}
events() {
super.events();
this.emitter.on("MitterContentReplaced", async () => {
this.DOM = this.updateTheDOM;
this.Manager.instances.Slider = [];
super.assignInstances({
elementGroups: [
{
elements: this.DOM.sliderElements,
config: this.config,
boostify: { distance: 50 }, // Load 50px before viewport
},
],
});
});
this.emitter.on("MitterWillReplaceContent", () => {
if (this.DOM.sliderElements.length) {
super.destroyInstances({ libraryName: 'Slider' });
}
});
}
}
export default SliderHandler;

Add your handler to Main.js:

import SliderHandler from "@scripts/handler/slider/Handler";
class Main extends Core {
async init() {
super.init();
// Initialize the handler
new SliderHandler({
...this.handler,
name: "SliderHandler"
});
// ... other handlers
}
}

Step 4: Allocate Instance Pool in Project.js

Section titled “Step 4: Allocate Instance Pool in Project.js”

Pre-allocate the instance array in Project.js:

this.Manager.allocateInstances([
"Slider", // Must match getLibraryName() in handler
"Modal",
// ... other instances
]);

Add the component library to src/scripts/preload/extraAssets.js:

export const getModules = () => {
return [
{
name: "Slider",
domElement: document.querySelectorAll(".js--slider"),
resource: async () => {
const { default: Slider } = await import("@scripts/handler/slider/Slider");
return Slider;
},
},
// ... other modules
];
};

this.Manager is a centralized registry system passed from Project.jsMain.jsHandler. It provides:

this.Manager = {
instances: {}, // Storage for component instances
libraries: {}, // Cached library references
// ... methods
}

Key Methods:

MethodPurpose
allocateInstances([names])Pre-create empty instance arrays
addInstance(name, instance)Add an instance to the registry
cleanInstances(name)Empty an instance array
addLibrary({ name, lib })Cache a library for reuse
getLibrary(name)Retrieve a cached library

Initialization Flow:

// 1. Created in Project.js
class Project {
constructor() {
this.Manager = new Manager({ terraDebug: this.terraDebug });
}
}
// 2. Passed to Main.js
new Main({ Manager: this.Manager, ... });
// 3. Passed to Handlers
class Main extends Core {
constructor(payload) {
this.handler = {
Manager: this.Manager,
emitter: this.emitter,
// ...
};
}
init() {
new SliderHandler({ ...this.handler, name: "SliderHandler" });
}
}
// 4. Available in CoreHandler and all Handlers
class CoreHandler {
constructor(payload) {
this.Manager = payload.Manager; // Now accessible!
}
}

Why Pass Manager Down?

  • Single Source of Truth - One Manager instance across entire app
  • Shared State - All handlers access same instance registry
  • Memory Management - Centralized cleanup prevents leaks
  • Debugging - Inspect Manager.instances in console with ?debug

Purpose: Pre-create empty arrays for component instances before any handlers run.

src/scripts/Project.js
class Project {
constructor() {
this.Manager = new Manager({ terraDebug: this.terraDebug });
// Allocate instance pools
this.Manager.allocateInstances([
"Slider",
"Modal",
"Collapsify",
"RevealIt",
"RevealStack",
"AnchorTo",
// ... all component types
]);
}
}

What This Does:

Manager.instances = {
Slider: [],
Modal: [],
Collapsify: [],
// ... etc
}

Step 2: Define Libraries in extraAssets.js

Section titled “Step 2: Define Libraries in extraAssets.js”

Purpose: Specify how to load each library/component on-demand.

src/scripts/preload/extraAssets.js
export const getModules = () => {
return [
{
name: "Slider", // Must match allocateInstances name
domElement: document.querySelectorAll(".js--slider"),
resource: async () => {
// Dynamic import - code splitting!
const { default: Slider } = await import("@scripts/handler/slider/Slider");
return Slider;
},
},
{
name: "Modal",
domElement: document.querySelectorAll(".js--modal-btn"),
resource: async () => {
const { default: Modal } = await import("@terrahq/modal");
return Modal;
},
},
// ... more modules
];
};

Categories in extraAssets.js:

  • getTerraInternal() - Core libraries (GSAP, Boostify, ScrollTrigger)
  • getAnimations() - Hero and reveal animations
  • getModules() - Component classes
  • getDeferred() - Low-priority components
  • getThirdParty() - External scripts

When a handler needs a library, CoreHandler.assignInstances() automatically:

  1. Checks Manager cache - Is library already loaded?
  2. Loads if needed - Dynamically imports from extraAssets.js
  3. Caches in Manager - Stores for future use
  4. Creates instances - Instantiates components
// Inside CoreHandler.assignInstances()
if (!this.library) {
const asset = await loadLibrary({ libraryName: this.libraryName });
const library = await asset.resource();
this.Manager.addLibrary({ name: this.libraryName, lib: library });
this.library = this.Manager.getLibrary(this.libraryName);
}

Use when all instances share the same config:

class SliderHandler extends CoreHandler {
constructor(payload) {
super(payload);
this.config = {
autoplay: true,
speed: 400,
nav: true,
};
}
events() {
super.assignInstances({
elementGroups: [{
elements: this.DOM.sliderElements,
config: this.config, // Same for all
}],
});
}
}

Use when config varies per element (data attributes, etc.):

class SliderHandler extends CoreHandler {
constructor(payload) {
super(payload);
// Config as a callback function
this.config = (element) => ({
autoplay: element.dataset.autoplay === "true",
speed: parseInt(element.dataset.speed) || 400,
nav: element.dataset.nav !== "false",
});
}
// CoreHandler will call this.config(element) for each element
}

Combine static defaults with per-instance overrides:

this.config = {
autoplay: true,
speed: 400,
};
super.assignInstances({
elementGroups: [{
elements: this.DOM.sliderElements,
config: {
...this.config,
nav: true, // Additional config
},
}],
});

Boostify is a performance library that loads components on-demand based on scroll position, reducing initial page load.

CoreHandler.assignInstances() automatically determines:

  • In viewport? → Load instantly
  • Below fold? → Load when user scrolls near it (via Boostify)
// Inside CoreHandler
const inViewport = this.Manager.libraries.isElementInViewport({
el: element,
debug: this.terraDebug,
});
if (inViewport) {
this.createInstance({ element, config }); // Instant
} else {
this.boostify.scroll({
distance: 30, // Load 30px before entering viewport
callback: async () => {
// Load library if needed
// Create instance
}
});
}
super.assignInstances({
elementGroups: [{
elements: this.DOM.sliderElements,
config: this.config,
boostify: { distance: 100 }, // Load 100px early
}],
});

Best Practices:

  • Heavy components (sliders, videos): distance: 50-100
  • Light components (accordions): distance: 20-30
  • Critical components (above fold): Will load instantly anyway

import CoreHandler from "../CoreHandler";
class ModalHandler extends CoreHandler {
constructor(payload) {
super(payload);
this.init();
this.events();
this.config = {
closeOnBackdropClick: true,
showCloseButton: true,
animationDuration: 300,
};
}
get updateTheDOM() {
return {
modalTriggers: document.querySelectorAll('.js--modal-btn'),
};
}
init() {
super.init();
super.getLibraryName("Modal");
}
events() {
super.events();
this.emitter.on("MitterContentReplaced", async () => {
this.DOM = this.updateTheDOM;
this.Manager.instances.Modal = [];
super.assignInstances({
elementGroups: [{
elements: this.DOM.modalTriggers,
config: this.config,
}],
});
});
this.emitter.on("MitterWillReplaceContent", () => {
if (this.DOM.modalTriggers.length) {
super.destroyInstances({ libraryName: 'Modal' });
}
});
}
}
export default ModalHandler;

Some handlers manage multiple component types:

class AlgoliaHandler extends CoreHandler {
constructor(payload) {
super(payload);
this.init();
this.events();
this.configSearch = { /* search config */ };
this.configFilters = { /* filter config */ };
}
get updateTheDOM() {
return {
searchElements: document.querySelectorAll('.js--algolia-search'),
filterElements: document.querySelectorAll('.js--algolia-filter'),
};
}
init() {
super.init();
super.getLibraryName("Algolia");
}
events() {
super.events();
this.emitter.on("MitterContentReplaced", async () => {
this.DOM = this.updateTheDOM;
this.Manager.instances.Algolia = [];
super.assignInstances({
elementGroups: [
{
elements: this.DOM.searchElements,
config: this.configSearch,
},
{
elements: this.DOM.filterElements,
config: this.configFilters,
boostify: { distance: 0 }, // Load on scroll
},
],
});
});
this.emitter.on("MitterWillReplaceContent", () => {
if (this.DOM.searchElements.length || this.DOM.filterElements.length) {
super.destroyInstances({ libraryName: 'Algolia' });
}
});
}
}
class CountdownHandler extends CoreHandler {
constructor(payload) {
super(payload);
this.init();
this.events();
// Config as callback to read data attributes
this.config = (element) => ({
targetDate: element.dataset.targetDate,
format: element.dataset.format || 'default',
onComplete: element.dataset.onComplete || null,
});
}
// ... rest of handler
}

  1. Always extend CoreHandler - Don’t reinvent the wheel

    class MyHandler extends CoreHandler { }
  2. Use getLibraryName() - Must match Manager allocation

    super.getLibraryName("Slider"); // Same as allocateInstances
  3. Reset instance array - Before assignInstances

    this.Manager.instances.Slider = [];
  4. Destroy on willReplaceContent - Prevent memory leaks

    this.emitter.on("MitterWillReplaceContent", () => {
    super.destroyInstances({ libraryName: 'Slider' });
    });
  5. Use updateTheDOM getter - Re-query elements on each page

    get updateTheDOM() {
    return {
    elements: document.querySelectorAll('.js--component'),
    };
    }
  6. Check element existence - Before destroying

    if (this.DOM.sliderElements.length) {
    super.destroyInstances({ libraryName: 'Slider' });
    }
  7. Provide meaningful handler names - For debugging

    new SliderHandler({ ...this.handler, name: "SliderHandler" });
  1. Don’t create instances manually - Use assignInstances
  2. Don’t forget to listen to both events
  3. Don’t hardcode DOM references - Use updateTheDOM
  4. Don’t skip Manager allocation - Must be in Project.js
  5. Don’t modify CoreHandler - Extend it instead

Add ?debug to the URL:

https://netwrix.com/page?debug

With debug enabled, you’ll see:

✅ library Slider was in manager
🚧 loading library Slider
⏰ Boostify - loading library Modal
❌ Destroying Slider SliderHandler

In browser console:

// View all instances
Manager.instances
// Check specific component
Manager.instances.Slider // Array of slider instances
// View cached libraries
Manager.libraries

Problem: Instances not destroyed on page change

// Solution: Check MitterWillReplaceContent listener
this.emitter.on("MitterWillReplaceContent", () => {
if (this.DOM.elements.length) {
super.destroyInstances({ libraryName: 'Component' });
}
});

Problem: Library not loading

// Solution: Ensure it's in extraAssets.js
export const getModules = () => [{
name: "MyComponent", // Must match getLibraryName
domElement: document.querySelectorAll(".js--my-component"),
resource: async () => { /* import */ }
}];

Problem: Duplicate instances

// Solution: Reset array before assignInstances
this.Manager.instances.Component = [];

Handlers are the glue between the framework core and individual components. They provide:

  • Lifecycle automation across SWUP transitions
  • Performance optimization via Boostify lazy loading
  • Memory management through Manager integration
  • Consistent patterns that reduce boilerplate

By following the Handler pattern, you ensure that components are:

  • ✅ Properly initialized on every page
  • ✅ Efficiently loaded (only when needed)
  • ✅ Completely destroyed (no memory leaks)
  • ✅ Centrally managed (easy debugging)