JavaScript Basics
At Terra, JavaScript is organized around handlers that manage component lifecycle, async imports for performance, and SWUP for smooth page transitions. This section covers the essentials you need to build interactive features.
The Handler System
Section titled “The Handler System”Handlers are controller classes that bridge the framework and component instances. They manage when and how components are initialized and destroyed across page transitions.
Why Handlers?
Section titled “Why Handlers?”In projects with SWUP page transitions, components need to:
- ✅ Re-initialize on every page navigation (new DOM elements)
- ✅ Destroy properly before transitions (prevent memory leaks)
- ✅ Load efficiently (only when needed, not all upfront)
Handlers solve these problems automatically.
SWUP Integration
Section titled “SWUP Integration”SWUP is a page transition library that enables smooth navigation without full page reloads. Handlers listen to events hooked up to SWUP’s lifecycle to know when to create and destroy components.
Key Events
Section titled “Key Events”// After new page content is loadedthis.emitter.on("MitterContentReplaced", async () => { // Initialize components for new page});
// Before transitioning to new pagethis.emitter.on("MitterWillReplaceContent", () => { // Destroy existing components});Flow:
- User clicks a link
- SWUP fetches new page content
MitterWillReplaceContentfires → Destroy old components- Content is replaced
MitterContentReplacedfires → Initialize new components
Creating a Handler
Section titled “Creating a Handler”Let’s create a handler for a simple accordion component using Collapsify.
01: Create the Handler
Section titled “01: Create the Handler”Use our handler template at src/js/handler/_handlerFolder/Handler.js, copy and paste it in the corresponding folder for your new library.
File: src/js/handler/collapsify/Handler.js
Understand how to build a handler and what everything does in detail here.
import CoreHandler from "../CoreHandler";import { updateScrollTriggers } from "@js/utilities/updateScrollTriggers";/** * Collapsify Handler */
class Handler extends CoreHandler { constructor(payload) { super(payload);
// Shared callbacks for all configurations this.callbacks = { onComplete: () => { // Update scroll triggers after collapsify is initialized updateScrollTriggers({ Manager: this.Manager }); }, onSlideEnd: (isOpen, contentID) => { // Update scroll triggers after collapsify slide ends updateScrollTriggers({ Manager: this.Manager }); }, };
// Generic configuration this.configSimple = ({ element }) => ({ element, ...this.callbacks, });
// Accordion version this.configAccordion = ({ element }) => ({ element, closeOthers: true, nameSpace: "accordion", ...this.callbacks, });
// Tabs version with dropdown this.configTabs = ({ element }) => ({ element, isTab: true, nameSpace: "tab", dropdownElement: element.querySelector(".c--tabs-a__hd__wrapper__item"), ...this.callbacks, });
this.init(); this.events(); }
get updateTheDOM() { return { collapsifyElement: document.querySelectorAll(`.js--collapsify`), collapsifyAccordion: document.querySelectorAll(`.js--collapsify-accordion`), collapsifyTab: document.querySelectorAll(`.js--collapsify-tab`), }; }
init() { super.getLibraryName("Collapsify"); }
events() { this.emitter.on("Collapsify:load", async () => { await super.assignInstances({ elementGroups: [ { elements: this.DOM.collapsifyElement, config: this.configSimple, boostify: { distance: 30 }, }, { elements: this.DOM.collapsifyAccordion, config: this.configAccordion, boostify: { distance: 30 }, }, { elements: this.DOM.collapsifyTab, config: this.configTabs, boostify: { distance: 30 }, }, ], forceLoad: true, }); });
this.emitter.on("MitterContentReplaced", async () => { this.DOM = this.updateTheDOM; await super.assignInstances({ elementGroups: [ { elements: this.DOM.collapsifyElement, config: this.configSimple, boostify: { distance: 30 }, }, { elements: this.DOM.collapsifyAccordion, config: this.configAccordion,
boostify: { distance: 30 }, }, { elements: this.DOM.collapsifyTab, config: this.configTabs, boostify: { distance: 30 }, }, ], }); });
this.emitter.on("MitterWillReplaceContent", () => { if (this.DOM.collapsifyElement.length) { super.destroyInstances(); } }); }}
export default Handler;Key parts:
updateTheDOM- Returns fresh DOM queriesinit()- Sets library nameevents()- Listens for SWUP transitionsMitterContentReplaced- Creates instances for new pageMitterWillReplaceContent- Destroys instances before transition
02: Register in Main.js
Section titled “02: Register in Main.js”File: src/scripts/Main.js
import CollapsifyHandler from "@scripts/handler/collapsify/Handler";
class Main extends Core { async init() { super.init();
// Initialize the handler new CollapsifyHandler({ ...this.handler, name: "CollapsifyHandler", });
// ... other handlers }}Learn more about the Main file here
03: Define Library Import
Section titled “03: Define Library Import”File: src/js/resources.js
export const getModules = () => { return [ { name: "Collapsify", resource: async () => { const { default: Collapsify } = await import("@terrahq/collapsify"); return Collapsify; }, options: { modifyHeight: true, }, }, // ... other modules ];};Understand how our resources file works here
Benefits of async imports:
- ✅ Smaller initial bundle
- ✅ Faster page load
- ✅ Only loads when needed
How async imports work:
// Traditional import - loads immediatelyimport Collapsify from "@terrahq/collapsify";
// Async import - loads when neededconst { default: Collapsify } = await import("@terrahq/collapsify");Performance impact:
Before async imports:
bundle.js: 500KB (includes all libraries) → Slow initial loadAfter async imports:
bundle.js: 100KB (core only)collapsify.js: 50KB (loads when needed)fadeIn.js: 30KB (loads when needed)→ Fast initial load + on-demand loadingCustom Component Class with GSAP
Section titled “Custom Component Class with GSAP”Let’s create a custom animation class using GSAP (GreenSock Animation Platform).
What is GSAP?
Section titled “What is GSAP?”GSAP is a powerful JavaScript animation library we use for smooth, performant animations. It provides fine-tuned control over timing, easing, and sequencing.
Why we use GSAP:
- High performance
- Precise control
- Timeline management
- Works everywhere (including mobile)
Creating a Fade-In Animation Component
Section titled “Creating a Fade-In Animation Component”File: src/scripts/handler/fadeIn/FadeIn.js
import gsap from "gsap";
/** * FadeIn Class * * Creates a smooth fade-in animation on scroll using GSAP. * Elements start invisible and fade in when they enter the viewport. * * Parameters: * - element (DOM Element): The element to animate * - duration (number): Animation duration in seconds (default: 0.8) * - delay (number): Delay before animation starts (default: 0) * - yOffset (number): Vertical offset for slide effect (default: 30) * * Example Usage: * ```javascript * new FadeIn({ * element: document.querySelector('.js--fade-in'), * duration: 1, * delay: 0.2, * yOffset: 40 * }); * ``` */
class FadeIn { constructor({ element, duration = 0.8, delay = 0, yOffset = 30 }) { this.DOM = { element: element, }; this.duration = duration; this.delay = delay; this.yOffset = yOffset;
this.init(); this.events(); }
init() { // Set initial state (invisible, offset down) gsap.set(this.DOM.element, { opacity: 0, y: this.yOffset, });
// Create the animation timeline (paused) this.timeline = gsap.timeline({ paused: true }); this.timeline.to(this.DOM.element, { opacity: 1, y: 0, duration: this.duration, delay: this.delay, ease: "power2.out", }); }
events() { // Create Intersection Observer to trigger on scroll this.observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { this.play(); this.observer.unobserve(entry.target); // Animate once } }); }, { threshold: 0.2 } // Trigger when 20% visible );
this.observer.observe(this.DOM.element); }
play() { if (this.timeline) { this.timeline.play(); } }
destroy() { // Disconnect observer if (this.observer) { this.observer.disconnect(); this.observer = null; }
// Kill GSAP timeline if (this.timeline) { this.timeline.kill(); this.timeline = null; }
// Clear references this.DOM = null; this.duration = null; this.delay = null; this.yOffset = null; }}
export default FadeIn;This class demonstrates:
- ✅ Proper
this.DOMstructure - ✅ GSAP timeline creation
- ✅ Intersection Observer for scroll triggering
- ✅ Complete
destroy()method - ✅ Full documentation
Creating the Handler for FadeIn
Section titled “Creating the Handler for FadeIn”File: src/scripts/handler/fadeIn/Handler.js
import CoreHandler from "../CoreHandler";
class FadeInHandler extends CoreHandler { constructor(payload) { super(payload); this.init(); this.events();
// Dynamic config based on data attributes this.config = (element) => ({ duration: parseFloat(element.dataset.duration) || 0.8, delay: parseFloat(element.dataset.delay) || 0, yOffset: parseInt(element.dataset.yOffset) || 30, }); }
get updateTheDOM() { return { fadeInElements: document.querySelectorAll(".js--fade-in"), }; }
init() { super.init(); super.getLibraryName("FadeIn"); }
events() { super.events();
this.emitter.on("MitterContentReplaced", async () => { this.DOM = this.updateTheDOM; this.Manager.instances.FadeIn = [];
super.assignInstances({ elementGroups: [ { elements: this.DOM.fadeInElements, config: this.config, // Callback for per-element config boostify: { distance: 50 }, }, ], }); });
this.emitter.on("MitterWillReplaceContent", () => { if (this.DOM.fadeInElements.length) { super.destroyInstances({ libraryName: "FadeIn" }); } }); }}
export default FadeInHandler;Using the Component in HTML
Section titled “Using the Component in HTML”<!-- Simple fade-in with defaults --><div class="c--banner-a js--fade-in"> <h2 class="c--banner-a__title">This fades in on scroll</h2></div>
<!-- Custom animation settings via data attributes --><div class="c--banner-a js--fade-in" data-duration="1.2" data-delay="0.3" data-y-offset="50"> <h2 class="c--banner-a__title">Custom fade-in animation</h2></div>Common GSAP Patterns
Section titled “Common GSAP Patterns”Simple Fade Animation
Section titled “Simple Fade Animation”gsap.to(element, { opacity: 1, duration: 0.6, ease: "power2.out",});Stagger Animation (Multiple Elements)
Section titled “Stagger Animation (Multiple Elements)”gsap.to(".js--card", { opacity: 1, y: 0, stagger: 0.1, // 0.1s delay between each duration: 0.6,});Timeline (Sequence)
Section titled “Timeline (Sequence)”const tl = gsap.timeline();tl.to(".js--title", { opacity: 1, duration: 0.6 }) .to(".js--subtitle", { opacity: 1, duration: 0.6 }, "-=0.3") // Overlap .to(".js--button", { opacity: 1, y: 0, duration: 0.6 });ScrollTrigger Integration
Section titled “ScrollTrigger Integration”import { ScrollTrigger } from "gsap/ScrollTrigger";gsap.registerPlugin(ScrollTrigger);
gsap.to(".js--element", { opacity: 1, scrollTrigger: { trigger: ".js--element", start: "top 80%", // When top of element hits 80% of viewport end: "bottom 20%", scrub: true, // Animate with scroll },});Best Practices
Section titled “Best Practices”✅ Do’s
Section titled “✅ Do’s”Always extend CoreHandler:
class MyHandler extends CoreHandler {}Destroy instances on page transition:
this.emitter.on("MitterWillReplaceContent", () => { if (this.DOM.collapsifyElement.length) { super.destroyInstances(); }});Include complete destroy() methods:
destroy() { if (this.timeline) { this.timeline.kill(); this.timeline = null; } this.DOM = null;}Use data attributes for configuration:
this.config = (element) => ({ speed: parseInt(element.dataset.speed) || 400,});❌ Don’ts
Section titled “❌ Don’ts”Don’t create instances manually:
// ❌ Wrongnew Slider({ element });
// ✅ Correct - let handler manage itsuper.assignInstances({ elementGroups: [...] });Don’t forget both SWUP events:
// ❌ Missing - will cause memory leaksthis.emitter.on("MitterContentReplaced", async () => {});
// ✅ Complete - both events handledthis.emitter.on("MitterContentReplaced", async () => {});this.emitter.on("MitterWillReplaceContent", () => {});Don’t skip destroy() in classes:
// ❌ Wrong - will leak memoryclass MyClass { constructor({ element }) {} // Missing destroy()}
// ✅ Correct - cleanup includedclass MyClass { constructor({ element }) {} destroy() { this.DOM = null; }}Summary
Section titled “Summary”JavaScript at Terra is organized around:
- Handlers - Manage component lifecycle across SWUP transitions
- Async Imports - Optimize performance with code splitting
- GSAP - Create smooth, performant animations
- SWUP Events - Know when to initialize and destroy components
By following these patterns, your components will:
- ✅ Load efficiently
- ✅ Work across page transitions
- ✅ Prevent memory leaks
- ✅ Provide smooth animations
Deep dive
Section titled “Deep dive”This was only a summary and quick reference guide on how we work. Understand our framework and how we work with JavaScript in our JS section
Knowledge Check
Test your understanding of this section
Loading questions...