Js Classes
JavaScript classes provide a structured way to create reusable components with shared behavior and state. They act as blueprints for creating objects that encapsulate both data (properties) and functionality (methods).
At Terra, classes are the foundation of our component architecture. We use them to build interactive UI components that can be initialized, managed, and destroyed cleanly across page transitions. Every class follows a consistent structure that ensures predictability, maintainability, and proper memory management.
Classes are also the backbone of our framework’s functionality.
Key Terra class principles:
- Consistent structure - All classes follow the same constructor → init → events pattern
- DOM organization - DOM elements are stored in a dedicated
this.DOMobject - Object parameters - All classes accept constructor’s parameters as a
payloadobject for flexibility - Proper cleanup - Every class includes a
destroy()method to prevent memory leaks - Clear documentation - All classes are documented with usage examples
This guide explains Terra’s class conventions and best practices. We’ll use a regular “library” style class as examples for all explanations. The other pattern we follow is the handler, you can find more about that here
Constructor
Section titled “Constructor”The constructor is where you receive parameters and set up the initial state of your class.
Pattern
Section titled “Pattern”All of our constructors will follow a common pattern:
- If extending another class, we usually send the whole payload into the other class
class Handler extends CoreHandler { constructor(payload) { super(payload); ... } ...}- We create the functions and variables we’re going to use in the constructor, if they’re available at instantiation. If not, there’s no need, but you can add them to have the full blueprint in the constructor.
class Header { constructor(payload) { this.DOM = { nav: payload.nav, dropdowns: [...payload.dropdowns], burger: payload.burger, overlay: payload.overlay, }; this.Manager = payload.Manager;
this.burger = null; this.dropdownItems = [];
this.gsap = this.Manager.getLibrary("GSAP").gsap; this.canPlay = true;
... }}- We (almost) always execute the class’
initandeventsmethod from our constructor, so the chain of operations starts when instantiating the class.
class Header { constructor(payload) { ...
this.init(); this.events(); }}DOM Element Structure
Section titled “DOM Element Structure”For classes that interact with the DOM, always include a this.DOM = {} object to store DOM elements.
The primary DOM element must be stored in this.DOM.element for consistency across all Terra classes. This element should use the js-- prefix to indicate it’s targeted by JavaScript.
class InfiniteMarquee{ constructor(payload){ var { element, speed, controlsOnHover, reversed, Manager } = payload;
this.DOM = { element }; ... } ...}
// Usage in a handler this.config = ({element}) => ({ Manager: this.Manager, element:element, speed: element.getAttribute("data-speed") || 1, controlsOnHover:element.getAttribute("data-controls-on-hover") || "false", reversed: element.getAttribute("data-reversed") || "false", })And the element would look like this:
<ul class="c--marquee-a js--marquee"> {Array.from({ length: 15 }).map((_, index) => ( <div class="c--marquee-a__item">{index + 1}</div> ))}</ul>Other example with direct instantiation:
class Header { constructor(payload) { this.DOM = { nav: payload.nav, dropdowns: [...payload.dropdowns], burger: payload.burger, overlay: payload.overlay, };
} ...}
// Usagenew Header({ nav: document.querySelector(".js--nav"), dropdowns: document.querySelectorAll(".js--dropdown"), burger: document.querySelector(".js--burger"), overlay: document.querySelector(".js--overlay"), Manager: this.Manager,});In any case, the main element of the DOM must be the element on which the class is being applied and must be only one element, not an array of them. If we have an array, we’ll loop through it and create a new instance for each one of them (which is what happens when we use our handlers).
Init & Events Methods
Section titled “Init & Events Methods”The init() and events() methods organize your class functionality into two clear categories:
init()- Initialization logic, setting initial states, animations on load, configuring sliders, etc.events()- EventListener setup for user interactions (clicks, scrolls, hovers, etc.)
Both methods are required.
class Header { constructor(payload) { this.DOM = { nav: payload.nav, dropdowns: [...payload.dropdowns], burger: payload.burger, overlay: payload.overlay, }; ...
this.init(); this.events(); }
/** * Initializes header and all child components. */ init() { if (this.DOM.burger && this.DOM.nav) { ... }
if (this.DOM.dropdowns.length) { this.DOM.dropdowns.forEach((dropdownEl) => { this.dropdownItems.push( new Dropdown({ dropdownEl: dropdownEl, Manager: this.Manager, getCanPlay: () => this.canPlay, setCanPlay: (val) => (this.canPlay = val), checkIfAnythingIsOpen: this.checkIfAnythingIsOpen, updateOverlay: this.updateOverlay, }) ); }); } }
/** * Sets up event listeners for resize and click outside detection. */ events() { window.addEventListener("resize", debounce(this.closeOnResize, 10)); document.addEventListener("click", this.handleClickOutside); }}
export default Header;When to Omit Methods
Section titled “When to Omit Methods”Even if we don’t see an initial use for both our methods, we always include both of them in our classes for consistency. Do not omit any of the two methods when building new classes.
Spreading Logic Across Methods
Section titled “Spreading Logic Across Methods”When logic starts to get a bit too much or will be reused across more than one event, use helper functions to store that logic and call it when needed. If a helper method is only going to be used inside the class, you can make it private adding a # to the beginning of its name.
If it’s possible that you’ll need to call that method from outside this class, leave it as public, as in the following example.
import {horizontalLoop} from '@andresclua/infinite-marquee-gsap';import { u_stringToBoolean } from '@andresclua/jsutil';
class InfiniteMarquee{ constructor(payload){ var { element, speed, controlsOnHover, reversed, Manager } = payload;
this.DOM = { element }; this.gsap = Manager.getLibrary("GSAP").gsap;
this.speed = speed; this.controlsOnHover = u_stringToBoolean(controlsOnHover);
// Define reversed attribute and initial direction this.reversed = u_stringToBoolean(reversed); this.initialDirection = this.reversed ? -1 : 1;
this.paused = false; this.init(); this.events(); }
init(){ ... }
events(){ if (this.controlsOnHover && this.DOM.element?.parentElement){ this.mouseEnterHandler = () => this.pause(); this.mouseLeaveHandler = () => this.play();
const parent = this.DOM.element; parent.addEventListener("mouseenter", this.mouseEnterHandler); parent.addEventListener("mouseleave", this.mouseLeaveHandler); } }
pause(){ this.paused = true; this.gsap.to(this.loop, { timeScale: 0, overwrite: true }); }
play(){ if (this.paused) { // Always go back to the initial direction this.gsap.to(this.loop, { timeScale: this.initialDirection, overwrite: true }); this.paused = false; } }
destroy(){ ... } }
export default InfiniteMarquee;Destroy Method
Section titled “Destroy Method”Since we use SWUP for page transitions, it’s essential to include a destroy() method that removes event listeners and clears references. This prevents memory leaks when components are removed during transitions.
What to clean up in destroy():
- Remove all event listeners
- Clear handler references
- Clear DOM references
- Kill animations/timelines
- Clear any intervals or timeouts
class InfiniteMarquee{ ... destroy(){ // Remove event listeners if (this.controlsOnHover && this.DOM.element?.parentElement) { const parent = this.DOM.element; parent.removeEventListener("mouseenter", this.mouseEnterHandler); parent.removeEventListener("mouseleave", this.mouseLeaveHandler);
// Clear handler references this.mouseEnterHandler = null; this.mouseLeaveHandler = null; }
// Kill loop if (this.loop) { this.loop.kill(); this.loop = null; }
// Clear all properties this.gsap = null; this.speed = null; this.controlsOnHover = null; this.reversed = null; this.initialDirection = null; this.paused = null; this.DOM = null; }}Why this matters:
With SWUP page transitions, content is replaced dynamically. Without proper cleanup:
- Event listeners remain attached to removed elements
- Memory usage increases over time
- Multiple handlers can fire for the same action
- Performance degrades
Documentation Requirement
Section titled “Documentation Requirement”All classes must include clear documentation. Even if the class seems self-explanatory, documentation ensures that developers of all skill levels can understand and use the code.
Documentation should include:
- Purpose of the class
- Parameters with types and descriptions
- Usage example
/** * InfiniteMarquee - A GSAP-powered infinite horizontal marquee component * * Creates smooth, continuous scrolling animations for a list of elements. * Supports hover controls, variable speed, and bi-directional scrolling. * * @class InfiniteMarquee * * @param {Object} payload - Configuration object * @param {HTMLElement} payload.element - The container element whose children will be animated in the marquee * @param {number} [payload.speed=1] - The speed of the marquee animation. Higher values = faster scrolling * @param {string|boolean} [payload.controlsOnHover="false"] - When "true", pauses the marquee on mouse enter and resumes on mouse leave * @param {string|boolean} [payload.reversed="false"] - When "true", the marquee scrolls in the opposite direction * @param {Object} payload.Manager - The Manager instance that provides access to GSAP library via Manager.getLibrary("GSAP") * * @example * new InfiniteMarquee({ * element: document.querySelector('.js--Marquee'), * speed: 2, * reversed: true, * controlsOnHover: true * }); */
class InfiniteMarquee { ...}Real-World Example
Section titled “Real-World Example”Here’s the full InfiniteMarquee class we’ve been dissecting:
import { horizontalLoop } from "@andresclua/infinite-marquee-gsap";import { u_stringToBoolean } from "@andresclua/jsutil";
/** * InfiniteMarquee - A GSAP-powered infinite horizontal marquee component * * Creates smooth, continuous scrolling animations for a list of elements. * Supports hover controls, variable speed, and bi-directional scrolling. * * @class InfiniteMarquee * * @param {Object} payload - Configuration object * @param {HTMLElement} payload.element - The container element whose children will be animated in the marquee * @param {number} [payload.speed=1] - The speed of the marquee animation. Higher values = faster scrolling * @param {string|boolean} [payload.controlsOnHover="false"] - When "true", pauses the marquee on mouse enter and resumes on mouse leave * @param {string|boolean} [payload.reversed="false"] - When "true", the marquee scrolls in the opposite direction * @param {Object} payload.Manager - The Manager instance that provides access to GSAP library via Manager.getLibrary("GSAP") * * @example * new InfiniteMarquee({ * element: document.querySelector('.js--Marquee'), * speed: 2, * reversed: true, * controlsOnHover: true * }); */
class InfiniteMarquee { constructor(payload) { var { element, speed, controlsOnHover, reversed, Manager } = payload;
this.DOM = { element }; this.gsap = Manager.getLibrary("GSAP").gsap;
this.speed = speed; this.controlsOnHover = u_stringToBoolean(controlsOnHover);
// Define reversed attribute and initial direction this.reversed = u_stringToBoolean(reversed); this.initialDirection = this.reversed ? -1 : 1;
this.paused = false; this.init(); this.events(); }
init() { this.loop = horizontalLoop(this.DOM.element.children, { paused: false, repeat: -1, reversed: this.reversed, speed: this.speed, });
this.gsap.set(this.loop, { timeScale: this.initialDirection }); }
events() { if (this.controlsOnHover && this.DOM.element?.parentElement) { this.mouseEnterHandler = () => this.pause(); this.mouseLeaveHandler = () => this.play();
const parent = this.DOM.element; parent.addEventListener("mouseenter", this.mouseEnterHandler); parent.addEventListener("mouseleave", this.mouseLeaveHandler); } }
pause() { this.paused = true; this.gsap.to(this.loop, { timeScale: 0, overwrite: true }); }
play() { if (this.paused) { // Always go back to the initial direction this.gsap.to(this.loop, { timeScale: this.initialDirection, overwrite: true }); this.paused = false; } }
destroy() { // Remove event listeners if (this.controlsOnHover && this.DOM.element?.parentElement) { const parent = this.DOM.element; parent.removeEventListener("mouseenter", this.mouseEnterHandler); parent.removeEventListener("mouseleave", this.mouseLeaveHandler);
// Clear handler references this.mouseEnterHandler = null; this.mouseLeaveHandler = null; }
// Kill loop if (this.loop) { this.loop.kill(); this.loop = null; }
// Clear all properties this.gsap = null; this.speed = null; this.controlsOnHover = null; this.reversed = null; this.initialDirection = null; this.paused = null; this.DOM = null; }}
export default InfiniteMarquee;This example demonstrates:
- ✅ Complete documentation with parameters and usage
- ✅ Proper
this.DOMstructure - ✅ Clear
init()andevents()separation - ✅ Conditional event listener setup
- ✅ Additional methods for specific functionality
- ✅ Thorough
destroy()method - ✅ Default export for easy importing
Summary
Section titled “Summary”Terra’s class structure ensures:
- Consistency across all projects
- Maintainability through clear organization
- Memory safety with proper cleanup
- Scalability with multiple instances
- Documentation for all team members
Knowledge Check
Test your understanding of this section
Loading questions...