Skip to content

Classes Structure

Classes in the NETWRIX project follow Terra’s strict structural conventions, ensuring consistency, maintainability, and compatibility with the SWUP transition system. Every class adheres to specific patterns for DOM management, initialization, event handling, and cleanup.

Understanding these conventions is crucial for creating components that integrate seamlessly with the framework’s Handler system and lifecycle management.


Every Terra class follows this foundation:

class ComponentName {
constructor({ element, param1, param2 }) {
this.DOM = {
element: element,
};
this.param1 = param1;
this.param2 = param2;
this.init();
this.events();
}
init() {
// Initialization logic
}
events() {
// Event listener setup
}
destroy() {
// Cleanup logic
}
}
  1. Constructor - Parameter handling and DOM storage
  2. init() - Initialization and setup logic
  3. events() - Event listener registration
  4. destroy() - Cleanup and memory management

All classes must include a this.DOM = {} object to store DOM element references.

Rules:

  • The primary element must be stored in this.DOM.element
  • Use the js-- prefix for all JavaScript-targeted elements
  • Only use querySelector or ID for the primary element
  • Never use querySelectorAll for the primary element
constructor({ element, config }) {
this.DOM = {
element: element, // Primary element (required)
buttons: element.querySelectorAll('.js--button'), // Child elements
container: element.querySelector('.js--container'),
};
}

The js-- prefix distinguishes elements used by JavaScript from those used only for styling:

<!-- ✅ JavaScript target -->
<div class="js--slider c--slider-a">
<button class="js--slider-next c--slider-a__next"></button>
</div>
<!-- ❌ Confusing - which is for JS? -->
<div class="slider">
<button class="slider-next"></button>
</div>

Benefits:

  • Clear separation of concerns (JS vs CSS)
  • Prevents accidental class removal during styling
  • Easy to identify interactive elements in HTML

All class parameters should be passed as a single object:

// ✅ Correct - Object parameters
new Slider({
element: document.querySelector('.js--slider'),
autoplay: true,
speed: 400,
});
// ❌ Wrong - Positional parameters
new Slider(element, true, 400);

Store Parameters as Instance Properties:

constructor({ element, autoplay, speed }) {
this.DOM = {
element: element,
};
// Store for use in other methods
this.autoplay = autoplay;
this.speed = speed;
this.init();
this.events();
}

Purpose: Handles initialization logic, setting initial states, and preparing the class for interaction.

Use Cases:

  • Setting up initial animations
  • Creating internal state
  • Initializing third-party libraries (sliders, carousels)
  • Setting default values
  • Creating timelines

Do NOT include event listeners in init() - that’s what events() is for.

init() {
// Import and initialize tiny-slider
this.slider = tns({
container: this.DOM.element,
items: 1,
slideBy: 'page',
autoplay: this.autoplay,
speed: this.speed,
nav: this.nav,
});
console.log('Slider initialized with speed:', this.speed);
}
init() {
// Set initial state for animation
gsap.set(this.DOM.items, {
opacity: 0,
y: 20,
});
// Create animation timeline
this.timeline = gsap.timeline({ paused: true });
this.timeline.to(this.DOM.items, {
opacity: 1,
y: 0,
stagger: 0.1,
duration: 0.6,
});
}

If a class only handles events with no initialization needed:

class ClickCounter {
constructor({ element }) {
this.DOM = { element };
this.count = 0;
// No init() needed - only tracking clicks
this.events();
}
events() {
this.clickHandler = () => this.handleClick();
this.DOM.element.addEventListener('click', this.clickHandler);
}
handleClick() {
this.count++;
}
}

Purpose: Exclusively manages event listener setup. Keep all addEventListener calls here.

Rules:

  • Add event listeners only (no initialization logic)
  • Store handler references for cleanup
  • Use arrow functions or bind handlers to preserve this

Critical for proper cleanup in destroy():

events() {
// ✅ Store handler reference
this.clickHandler = (event) => this.handleClick(event);
this.scrollHandler = () => this.handleScroll();
this.DOM.element.addEventListener('click', this.clickHandler);
window.addEventListener('scroll', this.scrollHandler);
}

Keep handlers in separate methods for clarity:

events() {
this.clickHandler = (event) => this.handleClick(event);
this.mouseEnterHandler = () => this.pause();
this.mouseLeaveHandler = () => this.play();
this.DOM.element.addEventListener('click', this.clickHandler);
this.DOM.element.addEventListener('mouseenter', this.mouseEnterHandler);
this.DOM.element.addEventListener('mouseleave', this.mouseLeaveHandler);
}
handleClick(event) {
// Click handling logic
console.log('Button clicked!');
}
pause() {
// Pause logic
}
play() {
// Play logic
}

Based on configuration:

events() {
// Always add this listener
this.clickHandler = () => this.handleClick();
this.DOM.element.addEventListener('click', this.clickHandler);
// Only add if hover controls are enabled
if (this.controlsOnHover) {
this.mouseEnterHandler = () => this.pause();
this.mouseLeaveHandler = () => this.play();
this.DOM.element.addEventListener('mouseenter', this.mouseEnterHandler);
this.DOM.element.addEventListener('mouseleave', this.mouseLeaveHandler);
}
}

Critical for SWUP: Every class must include a destroy() method to prevent memory leaks during page transitions.

  1. Remove Event Listeners - Prevent memory leaks
  2. Destroy Third-Party Instances - Slider libraries, etc.
  3. Clear Timelines - GSAP animations
  4. Null References - Handler functions and DOM elements
destroy() {
// 1. Remove event listeners
this.DOM.element.removeEventListener('click', this.clickHandler);
if (this.controlsOnHover) {
this.DOM.element.removeEventListener('mouseenter', this.mouseEnterHandler);
this.DOM.element.removeEventListener('mouseleave', this.mouseLeaveHandler);
}
// 2. Clear handler references
this.clickHandler = null;
this.mouseEnterHandler = null;
this.mouseLeaveHandler = null;
// 3. Destroy third-party instances
if (this.slider && this.slider.destroy) {
this.slider.destroy();
this.slider = null;
}
// 4. Clear GSAP timelines
if (this.timeline) {
this.timeline.kill();
this.timeline = null;
}
// 5. Clear DOM references
this.DOM = null;
// 6. Clear configuration
this.autoplay = null;
this.speed = null;
}

Without proper cleanup:

// Page 1: User visits, slider initializes
new Slider({ element: document.querySelector('.js--slider') });
// User clicks link, SWUP transitions to Page 2
// ❌ Without destroy(): Event listeners still active on removed DOM
// ❌ Memory leak: Old slider instance remains in memory
// ❌ Performance: Multiple instances accumulate
// With destroy() called by Handler:
// ✅ Event listeners removed
// ✅ Memory freed
// ✅ Clean slate for new page

Every class must include detailed documentation explaining:

  1. Purpose - What the class does
  2. Parameters - What arguments it accepts
  3. Example Usage - How to instantiate it
/****
* ClassName
*
* [Brief description of what this class does and its purpose]
*
* Parameters:
* - element (DOM Element): [Description of the primary element]
* - param1 (type): [Description and default value if any]
* - param2 (type): [Description and default value if any]
*
* Example Usage:
* ```javascript
* new ClassName({
* element: document.querySelector('.js--component'),
* param1: value1,
* param2: value2,
* });
* ```
****/
class ClassName {
// Class implementation
}
/****
* Slider Class
*
* Creates an interactive content slider with navigation controls,
* autoplay functionality, and touch support. Built on top of tiny-slider.
*
* Parameters:
* - element (DOM Element): The container element for the slider
* - autoplay (boolean): Whether to automatically advance slides (default: false)
* - speed (number): Transition speed in milliseconds (default: 400)
* - nav (boolean): Show navigation dots (default: true)
* - controls (boolean): Show prev/next arrows (default: true)
*
* Example Usage:
* ```javascript
* new Slider({
* element: document.querySelector('.js--slider'),
* autoplay: true,
* speed: 600,
* nav: true,
* controls: true,
* });
* ```
****/
class Slider {
constructor({ element, autoplay = false, speed = 400, nav = true, controls = true }) {
this.DOM = {
element: element,
};
this.autoplay = autoplay;
this.speed = speed;
this.nav = nav;
this.controls = controls;
this.init();
this.events();
}
// ... rest of implementation
}

When you need multiple instances of a class on a page:

<div class="js--slider" data-autoplay="true" data-speed="600">
<!-- Slider content -->
</div>
<div class="js--slider" data-autoplay="false" data-speed="400">
<!-- Different slider content -->
</div>

Use querySelectorAll and forEach:

const sliderElements = document.querySelectorAll('.js--slider');
sliderElements.forEach((element) => {
new Slider({
element: element,
autoplay: element.dataset.autoplay === 'true',
speed: parseInt(element.dataset.speed) || 400,
});
});

❌ Don’t use querySelectorAll on the primary element parameter:

// ❌ Wrong - creates one instance with ALL sliders
new Slider({
element: document.querySelectorAll('.js--slider'), // Don't do this
});

In NETWRIX, Handlers manage multiple instances automatically:

class SliderHandler extends CoreHandler {
events() {
this.emitter.on("MitterContentReplaced", async () => {
this.DOM = this.updateTheDOM;
// Handler creates one instance per element
super.assignInstances({
elementGroups: [{
elements: this.DOM.sliderElements, // NodeList
config: this.config,
}],
});
});
}
}

import { horizontalLoop } from '@andresclua/infinite-marquee-gsap';
import gsap from 'gsap';
import { u_stringToBoolean } from '@andresclua/jsutil';
/****
* InfiniteMarquee Class
*
* Creates an infinite horizontal marquee that loops its child elements.
* Supports speed control, direction reversal, and pause on hover.
*
* Parameters:
* - element (DOM Element): The DOM element containing marquee items
* - speed (number): Animation speed multiplier (default: 1)
* - reversed (boolean): Whether to move in reverse (default: false)
* - controlsOnHover (boolean): Pause on hover (default: false)
*
* Example Usage:
* ```javascript
* new InfiniteMarquee({
* element: document.querySelector('.js--marquee'),
* speed: 2,
* reversed: true,
* controlsOnHover: true
* });
* ```
****/
class InfiniteMarquee {
constructor(payload) {
this.DOM = {
element: payload.element,
};
// Parse boolean from string if needed
const reversed = u_stringToBoolean(payload.reversed);
this.reversed = payload.reversed === undefined || payload.reversed === null
? false
: reversed;
this.speed = payload.speed === undefined ? 1 : payload.speed;
this.controlsOnHover = payload.controlsOnHover === undefined
? false
: payload.controlsOnHover;
this.init();
this.events();
}
init() {
// Initialize the horizontal loop animation
this.loop = horizontalLoop(this.DOM.element.children, {
paused: false,
repeat: -1,
reversed: this.reversed,
speed: this.speed,
});
}
events() {
// Only add hover controls if enabled
if (this.controlsOnHover) {
this.mouseEnterHandler = () => this.pause();
this.mouseLeaveHandler = () => this.play();
this.DOM.element.addEventListener("mouseenter", this.mouseEnterHandler);
this.DOM.element.addEventListener("mouseleave", this.mouseLeaveHandler);
}
}
pause() {
gsap.to(this.loop, { timeScale: 0, overwrite: true });
}
play() {
gsap.to(this.loop, { timeScale: 1, overwrite: true });
}
destroy() {
// Remove event listeners
if (this.controlsOnHover) {
this.DOM.element.removeEventListener("mouseenter", this.mouseEnterHandler);
this.DOM.element.removeEventListener("mouseleave", this.mouseLeaveHandler);
this.mouseEnterHandler = null;
this.mouseLeaveHandler = null;
}
// Clear GSAP loop
if (this.loop) {
this.loop.kill();
this.loop = null;
}
// Clear all properties
this.speed = null;
this.reversed = null;
this.controlsOnHover = null;
this.DOM = null;
}
}
export default InfiniteMarquee;

  1. Always include this.DOM - Even for simple classes

    this.DOM = { element: element };
  2. Use js— prefix - For all JavaScript-targeted elements

    <div class="js--slider c--slider-a"></div>
  3. Store handler references - Essential for cleanup

    this.clickHandler = (e) => this.handleClick(e);
  4. Include destroy() - Required for SWUP compatibility

    destroy() {
    this.DOM.element.removeEventListener('click', this.clickHandler);
    this.clickHandler = null;
    this.DOM = null;
    }
  5. Document thoroughly - Help future developers

    /****
    * ComponentName
    * [Full documentation with examples]
    ****/
  6. Use default parameters - Make classes flexible

    constructor({ element, speed = 400, autoplay = false }) { }
  7. Separate concerns - init() for setup, events() for listeners

  8. Export as default - Consistent import pattern

    export default ClassName;
  1. Don’t skip documentation
  2. Don’t use querySelectorAll for primary element
  3. Don’t forget to null references in destroy()
  4. Don’t add listeners directly in constructor
  5. Don’t use anonymous functions in addEventListener
  6. Don’t mix initialization and event logic

class FadeInAnimation {
constructor({ element, duration = 0.6, delay = 0 }) {
this.DOM = { element };
this.duration = duration;
this.delay = delay;
this.init();
}
init() {
this.timeline = gsap.timeline({ paused: true });
this.timeline.from(this.DOM.element, {
opacity: 0,
y: 20,
duration: this.duration,
delay: this.delay,
});
}
play() {
this.timeline.play();
}
destroy() {
if (this.timeline) {
this.timeline.kill();
this.timeline = null;
}
this.DOM = null;
}
}
class FormValidator {
constructor({ element, onSuccess, onError }) {
this.DOM = {
element: element,
inputs: element.querySelectorAll('input, textarea'),
submit: element.querySelector('button[type="submit"]'),
};
this.onSuccess = onSuccess;
this.onError = onError;
this.events();
}
events() {
this.submitHandler = (e) => this.handleSubmit(e);
this.DOM.element.addEventListener('submit', this.submitHandler);
}
handleSubmit(event) {
event.preventDefault();
const isValid = this.validate();
if (isValid) {
this.onSuccess && this.onSuccess();
} else {
this.onError && this.onError();
}
}
validate() {
// Validation logic
return true;
}
destroy() {
this.DOM.element.removeEventListener('submit', this.submitHandler);
this.submitHandler = null;
this.DOM = null;
this.onSuccess = null;
this.onError = null;
}
}
class LazyVideo {
constructor({ element }) {
this.DOM = { element };
// Read configuration from data attributes
this.videoSrc = element.dataset.videoSrc;
this.autoplay = element.dataset.autoplay === 'true';
this.loop = element.dataset.loop === 'true';
this.init();
this.events();
}
init() {
// Create video element
this.video = document.createElement('video');
this.video.src = this.videoSrc;
this.video.autoplay = this.autoplay;
this.video.loop = this.loop;
this.DOM.element.appendChild(this.video);
}
events() {
this.playHandler = () => this.play();
this.DOM.element.addEventListener('click', this.playHandler);
}
play() {
this.video.play();
}
destroy() {
this.DOM.element.removeEventListener('click', this.playHandler);
this.playHandler = null;
if (this.video) {
this.video.pause();
this.video.remove();
this.video = null;
}
this.DOM = null;
}
}

Terra’s class structure ensures:

  • Consistency - All classes follow the same pattern
  • Maintainability - Clear separation between setup, events, and cleanup
  • SWUP Compatibility - destroy() prevents memory leaks
  • Readability - Well-documented, self-explanatory code
  • Team Alignment - Everyone writes classes the same way

The Four Essential Elements:

  1. this.DOM - Store all element references
  2. init() - Setup and initialization
  3. events() - Event listener registration
  4. destroy() - Complete cleanup

By adhering to these conventions, your classes will integrate seamlessly with NETWRIX’s Handler system and work reliably across page transitions.