Handlers System
What is a Handler?
Section titled “What is a Handler?”A Handler is a specialized controller class that bridges the gap between the framework core and individual component instances. Handlers are responsible for:
- Lifecycle Management - Initializing and destroying component instances across page transitions
- Smart Loading - Determining when to load components (instantly vs. lazy via Boostify)
- Instance Tracking - Registering instances with the Manager for centralized memory management
- 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.
Why Use Handlers?
Section titled “Why Use Handlers?”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
Handler Architecture
Section titled “Handler Architecture”All handlers follow this structure:
Handler (extends CoreHandler) ↓CoreHandler (provides base functionality) ↓Main.js (initializes handlers) ↓Manager (stores instances)CoreHandler Base Class
Section titled “CoreHandler Base Class”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.
Creating a Handler
Section titled “Creating a Handler”Step 1: Use the Template
Section titled “Step 1: Use the Template”Every new handler should start from the template located at:
src/scripts/handler/_handlerFolder/Handler.jsTemplate 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;Step 2: Customize for Your Component
Section titled “Step 2: Customize for Your Component”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;Step 3: Register in Main.js
Section titled “Step 3: Register in Main.js”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]);Step 5: Define Library in extraAssets.js
Section titled “Step 5: Define Library in extraAssets.js”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 ];};Understanding this.Manager
Section titled “Understanding this.Manager”What is this.Manager?
Section titled “What is this.Manager?”this.Manager is a centralized registry system passed from Project.js → Main.js → Handler. It provides:
this.Manager = { instances: {}, // Storage for component instances libraries: {}, // Cached library references // ... methods}Key Methods:
| Method | Purpose |
|---|---|
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 |
Where Does Manager Come From?
Section titled “Where Does Manager Come From?”Initialization Flow:
// 1. Created in Project.jsclass Project { constructor() { this.Manager = new Manager({ terraDebug: this.terraDebug }); }}
// 2. Passed to Main.jsnew Main({ Manager: this.Manager, ... });
// 3. Passed to Handlersclass 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 Handlersclass 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.instancesin console with?debug
Instance Dependencies and Lifecycle
Section titled “Instance Dependencies and Lifecycle”Step 1: Allocate Instances in Project.js
Section titled “Step 1: Allocate Instances in Project.js”Purpose: Pre-create empty arrays for component instances before any handlers run.
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.
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 animationsgetModules()- Component classesgetDeferred()- Low-priority componentsgetThirdParty()- External scripts
Step 3: Handler Loads Library On-Demand
Section titled “Step 3: Handler Loads Library On-Demand”When a handler needs a library, CoreHandler.assignInstances() automatically:
- Checks Manager cache - Is library already loaded?
- Loads if needed - Dynamically imports from
extraAssets.js - Caches in Manager - Stores for future use
- 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);}Configuration Patterns
Section titled “Configuration Patterns”Static Configuration
Section titled “Static Configuration”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 }], }); }}Dynamic Configuration (Callback)
Section titled “Dynamic Configuration (Callback)”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}Merging Configurations
Section titled “Merging Configurations”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 Integration
Section titled “Boostify Integration”What is Boostify?
Section titled “What is Boostify?”Boostify is a performance library that loads components on-demand based on scroll position, reducing initial page load.
How Handlers Use Boostify
Section titled “How Handlers Use Boostify”CoreHandler.assignInstances() automatically determines:
- In viewport? → Load instantly
- Below fold? → Load when user scrolls near it (via Boostify)
// Inside CoreHandlerconst 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 } });}Customizing Boostify Distance
Section titled “Customizing Boostify Distance”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
Real-World Examples
Section titled “Real-World Examples”Example 1: Modal Handler
Section titled “Example 1: Modal Handler”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;Example 2: Multiple Element Groups
Section titled “Example 2: Multiple Element Groups”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' }); } }); }}Example 3: Data-Attribute Configuration
Section titled “Example 3: Data-Attribute Configuration”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}Best Practices
Section titled “Best Practices”✅ Do’s
Section titled “✅ Do’s”-
Always extend CoreHandler - Don’t reinvent the wheel
class MyHandler extends CoreHandler { } -
Use getLibraryName() - Must match Manager allocation
super.getLibraryName("Slider"); // Same as allocateInstances -
Reset instance array - Before assignInstances
this.Manager.instances.Slider = []; -
Destroy on willReplaceContent - Prevent memory leaks
this.emitter.on("MitterWillReplaceContent", () => {super.destroyInstances({ libraryName: 'Slider' });}); -
Use updateTheDOM getter - Re-query elements on each page
get updateTheDOM() {return {elements: document.querySelectorAll('.js--component'),};} -
Check element existence - Before destroying
if (this.DOM.sliderElements.length) {super.destroyInstances({ libraryName: 'Slider' });} -
Provide meaningful handler names - For debugging
new SliderHandler({ ...this.handler, name: "SliderHandler" });
❌ Don’ts
Section titled “❌ Don’ts”- Don’t create instances manually - Use assignInstances
- Don’t forget to listen to both events
- Don’t hardcode DOM references - Use updateTheDOM
- Don’t skip Manager allocation - Must be in Project.js
- Don’t modify CoreHandler - Extend it instead
Debugging Handlers
Section titled “Debugging Handlers”Enable Debug Mode
Section titled “Enable Debug Mode”Add ?debug to the URL:
https://netwrix.com/page?debugConsole Logs
Section titled “Console Logs”With debug enabled, you’ll see:
✅ library Slider was in manager🚧 loading library Slider⏰ Boostify - loading library Modal❌ Destroying Slider SliderHandlerInspect Manager
Section titled “Inspect Manager”In browser console:
// View all instancesManager.instances
// Check specific componentManager.instances.Slider // Array of slider instances
// View cached librariesManager.librariesCommon Issues
Section titled “Common Issues”Problem: Instances not destroyed on page change
// Solution: Check MitterWillReplaceContent listenerthis.emitter.on("MitterWillReplaceContent", () => { if (this.DOM.elements.length) { super.destroyInstances({ libraryName: 'Component' }); }});Problem: Library not loading
// Solution: Ensure it's in extraAssets.jsexport const getModules = () => [{ name: "MyComponent", // Must match getLibraryName domElement: document.querySelectorAll(".js--my-component"), resource: async () => { /* import */ }}];Problem: Duplicate instances
// Solution: Reset array before assignInstancesthis.Manager.instances.Component = [];Summary
Section titled “Summary”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)
Related Topics
Section titled “Related Topics”- 🎯 Classes Structure - Component implementation patterns
- 🎬 Animations - GSAP integration in handlers
- 📦 Libraries - Available components and tools
- 📚 Framework Overview - Overall architecture context