Js Classes
At Terra, we follow a consistent structure across all our projects when building classes.
Constructor Section
Section titled “Constructor Section”For classes interacting 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. It’s mandatory to use the js--ClassName
prefix in the class name of any element being targeted by JavaScript.
This ensures that elements with JavaScript functionality can be easily identified in the HTML.
Only use querySelector or an ID for the primary element—never querySelectorAll—to ensure a single element is selected for interaction.
Example
Section titled “Example”MyClass({ element: document.querySelector('.js--MyClass-item'), maxTime: 30,});
constructor({ element, maxTime }) { this.DOM = { element: element, // Primary DOM element with js-- prefix }; this.maxTime = maxTime; // Store maxTime parameter}
As you can see, you can pass only querySelector or ID, but never querySelectorAll for primary, element, the idea is to respect that only 1 element will interact with it.
Init & Events Methods
Section titled “Init & Events Methods”The init and events methods are both optional yet essential by default. This means that all classes should include them unless explicitly unnecessary. After defining this.DOM or any additional variables, these methods help organize functionality.
-
this.init()
is responsible for initializing the class, containing logic for setting initial states or preparing the class for further operations. Examples include animations on load, transitions, initializing sliders, tabs, etc. -
this.events()
manages everything related to EventListeners, such as mouse movements, scroll, clicks, etc.
Both init and events are sibling methods inside the constructor. If a class has no events (e.g., no click or scroll behavior), the this.events() method should be omitted.
this.init();this.events();
Sometimes, this.init() is not needed if the class only fires events like scroll or clicks, in which case only this.events() would be used.
init() { console.log('ExpandendButton initialized with maxTime:', this.max);}
events() { this.DOM.element.addEventListener('click', () => { this.handleClick(); });}
// Example method for handling click eventshandleClick() { console.log('Button clicked!');}
As you can see, not all logic needs to be wrapped strictly within init and events. You can spread your logic across multiple functions and methods, allowing for better modularity and code organization.
This approach keeps the init
and events
methods clean, while more complex logic can be divided across various methods, improving readability and maintainability.
Destroy Event Listeners
Section titled “Destroy Event Listeners”Since 2023, as we started incorporating page transitions with Swup.js into our websites, it became important to manage event listeners properly to prevent memory leaks. Swup.js operates by replacing content during transitions, so any lingering event listeners from previously rendered components can cause issues and waste memory.
To handle this, it’s essential to include a destroy method that removes these event listeners before a component is removed or replaced. This ensures that we avoid memory leaks and that our code remains clean and efficient.
Below is an example of how to implement a destroy method in the ExpandendButton class to manage event listeners:
class MyClass { constructor({ element, maxTime }) { this.DOM = { element: element, // Storing the DOM element }; this.max = maxTime; // Storing the maxTime parameter this.init(); // Initializing the class this.events(); // Setting up event listeners }
// The init method is for initialization logic init() { console.log('ExpandendButton initialized with maxTime:', this.max); }
// The events method is for setting up event listeners only events() { this.DOM.element.addEventListener('click', (event) => this.handleClick(event)); }
// Example method for handling click events handleClick(event) { console.log('Button clicked!'); }
// Public destroy method to remove event listeners and clean up destroy() { // Remove the event listener this.DOM.element.removeEventListener('click', (event) => this.handleClick(event)); this.DOM = null; // Clearing DOM references }}
Documentation Requirement
Section titled “Documentation Requirement”All classes must include clear and detailed documentation. Even if the class seems self-explanatory, documentation ensures that other developers—regardless of their skill level—can understand and use the code efficiently.
The documentation should cover the purpose of the class, explain its methods, and describe how the class is used within the project.
Here’s an example with the appropriate documentation for MyClass:
/**** * MyClass Class * * This class is used to create an expandable button that responds to click events. * It handles event listeners and provides methods to initialize and destroy event listeners. * * Parameters: * - element (DOM Element): The button element that will have the click event listener attached. * - maxTime (number): The maximum time or upper limit value that the class will manage. * * Example Usage: * ```javascript * new MyClass({ * element: document.querySelector('.js--TriggerButton'), * maxTime: 20 * }); * ``` ****/
class MyClass { constructor({ element, maxTime }) { this.DOM = { element: element, // Storing the DOM element }; this.max = maxTime; // Storing the maxTime parameter this.init(); // Initialize class setup this.events(); // Set up event listeners }
/** * The init method is used for class initialization logic. * Here, you can set up any necessary variables or perform actions that need to run when the class is instantiated. */ init() { console.log('MyClass initialized with maxTime:', this.max); }
/** * The events method adds event listeners to the DOM element. * In this case, it attaches a click event to the specified element. */ events() { this.DOM.element.addEventListener('click', this.handleClick.bind(this)); }
/** * The handleClick method defines what happens when the button is clicked. * This method is triggered by the click event. * @param {Event} event - The click event object. */ handleClick(event) { console.log('Button clicked!'); }
/** * The destroy method removes event listeners and cleans up when the component is no longer needed. * This prevents memory leaks by detaching event listeners when the class is destroyed or the page transitions. */ destroy() { this.DOM.element.removeEventListener('click', this.handleClick.bind(this)); console.log('MyClass destroyed, event listeners removed.'); }}
export default MyClass;
Handling Multiple Instances
Section titled “Handling Multiple Instances”What if you need to instantiate the class for multiple elements on the page, such as having multiple “fire” elements? In this case, you should use querySelectorAll to target all instances of the desired element, then iterate over them to create a new class instance for each.
Here’s how you can correctly implement this:
const myItems = document.querySelectorAll('.js--MyClass-item');myItems.forEach((element) => { new MyClass({ element: element, // Use each individual element maxTime: 30, // Pass any additional parameters });});
While querySelectorAll should not be used for a single element, it is appropriate when handling multiple elements. Each element in the NodeList is passed to the class constructor individually, ensuring that every instance of the element gets its own instance of the class.
This approach maintains the scalability of the class and ensures that each element operates independently with its own JavaScript logic.
Real Example
Section titled “Real Example”This real example is taken from an inifinte marquee.
import { horizontalLoop } from '@andresclua/infinite-marquee-gsap';import gsap from 'gsap';import { u_stringToBoolean } from '@andresclua/jsutil';
/**** * InfiniteMarquee Class * * This class creates an infinite marquee that loops its child elements horizontally. * It supports controlling the marquee speed, direction, and pausing on hover. * * Parameters: * - element (DOM Element): The DOM element that contains the marquee items. * - speed (number): The speed of the marquee animation (default is 1). * - reversed (boolean): Whether the marquee should move in reverse (default is false). * - controlsOnHover (boolean): Whether the marquee pauses on hover (default is 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, }; var 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(); }
events() { /** * pause marquee on hover */ 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); } }
init() { this.loop = horizontalLoop(this.DOM.element.children, { paused: false, repeat: -1, reversed: this.reversed, speed: this.speed, }); }
destroy() { // Remove the event listeners if (this.controlsOnHover) { this.DOM.element.removeEventListener("mouseenter", this.mouseEnterHandler); this.DOM.element.removeEventListener("mouseleave", this.mouseLeaveHandler); }
// Clear loop and reset variables this.speed = null; this.loop.clear(); }
pause() { gsap.to(this.loop, { timeScale: 0, overwrite: true }); }
play() { gsap.to(this.loop, { timeScale: 1, overwrite: true }); }}
export default InfiniteMarquee;