Skip to content

JavaScript Architecture

LamaPress uses a modern, component-based JavaScript architecture that automatically loads and manages components. This guide explains how the JavaScript system works, from initialization to component lifecycle.

Table of Contents

Architecture Overview

The JavaScript architecture follows these principles:

  1. Automatic Component Loading - Components are discovered via data-component attributes
  2. Lifecycle Management - Components have consistent lifecycle methods
  3. Instance Management - All component instances are tracked and accessible
  4. Event-Driven - Components communicate through events
  5. Page-Aware - Components are scoped to pages for proper cleanup

Entry Point

The application starts in main.js:

javascript
import "./src/scss/styles.scss"
import './src/js/scripts.js'

Which loads src/js/scripts.js:

javascript
import { App } from './core/app'

document.addEventListener('DOMContentLoaded', () => new App())

Core System

App Class

The App class (src/js/core/app.js) is the central orchestrator:

javascript
export class App {
  constructor() {
    app = this
    this.init()
  }

  init = () => {
    configGsap()
    this.setDefaults()
    this.createApp()
  }

  createApp = async () => {
    await this.createCoreInstances()
    await this.createAppInstances()
    await this.createPageInstances()
    app.instances.get('loader')?.loadApp()
  }
}

Initialization Order:

  1. Configure GSAP
  2. Set defaults (instances Map, Tailwind theme)
  3. Create core instances
  4. Create app-level instances
  5. Create page instances
  6. Load the app

Instance Management

All component instances are stored in a Map:

javascript
this.instances = new Map()

Instance Types:

  • Core instances - System-level instances (router, scroller, etc.)
  • App instances - Components outside the page container
  • Page instances - Components within the page container

Component Loading

Automatic Discovery

Components are automatically discovered via the data-component attribute:

html
<div data-component="blocks/accordion">
  <!-- Component content -->
</div>

The system:

  1. Scans for all [data-component] elements
  2. Extracts the component path (e.g., blocks/accordion)
  3. Dynamically imports the JavaScript file
  4. Instantiates the component class

Component Path Resolution

The data-component value maps to a file path:

data-component="blocks/accordion"
→ components/blocks/accordion/index.js

data-component="sections/hero_basic"
→ components/sections/hero_basic/index.js

Dynamic Import

Components are loaded using Vite's dynamic import:

javascript
const importFunction = getModule(element.dataset.component, 'component')
const importedClass = await importFunction()
const instance = new importedClass.default(element)

Benefits:

  • Code splitting
  • Lazy loading
  • Smaller initial bundle

Component Lifecycle

Every component follows a consistent lifecycle:

1. Constructor

javascript
export default class MyComponent {
  constructor(element) {
    this.element = element
    this.init()
  }
}

Receives:

  • element - The DOM element with data-component attribute

Should:

  • Store the element reference
  • Call init() method

2. init()

javascript
init = () => {
  // Initialization logic
  this.bindEvents()
}

Purpose:

  • Set up component state
  • Initialize variables
  • Call bindEvents()

Called: Automatically after constructor

3. bindEvents()

javascript
bindEvents = () => {
  // Add event listeners
  this.element.addEventListener('click', this.handleClick)
}

Purpose:

  • Set up all event listeners
  • Bind methods to component instance

Best Practice: Call from init() method

4. enter()

javascript
enter = () => {
  // Reveal animation
  gsap.from(this.element, {
    opacity: 0,
    y: 20,
    duration: 0.6
  })
}

Purpose:

  • Reveal animations when component enters viewport
  • Scroll-triggered animations

Called:

  • When component enters viewport (via ScrollTrigger)
  • When page transitions in (via router)

5. destroy()

javascript
destroy = () => {
  // Cleanup
  this.timeline?.kill()
  this.element.removeEventListener('click', this.handleClick)
}

Purpose:

  • Clean up timelines
  • Remove event listeners
  • Free up resources

Called: When component is destroyed (page transition, etc.)

Complete Example

javascript
import { gsap } from 'gsap/all'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { killTimeline } from '@src/js/core/utils/helpers'

export default class Accordion {
  constructor(element) {
    this.element = element
    this.timeline = null
    this.init()
  }

  init = () => {
    this.bindEvents()
    this.createRevealAnimation()
  }

  bindEvents = () => {
    // Event listeners
  }

  createRevealAnimation = () => {
    this.timeline = gsap.timeline({
      scrollTrigger: {
        trigger: this.element,
        start: 'top 80%',
      }
    })
    
    this.timeline.from(this.element, {
      opacity: 0,
      y: 20,
      duration: 0.6
    })
  }

  enter = () => {
    // Additional enter logic if needed
  }

  destroy = () => {
    killTimeline(this.timeline)
    ScrollTrigger.getAll().forEach(trigger => {
      if (trigger.vars.trigger === this.element) {
        trigger.kill()
      }
    })
  }
}

Core Instances

The app creates several core instances that provide system-level functionality:

EventManager

Manages custom events and event scoping.

javascript
app.instances.get('eventManager')

Loader

Handles page loading and initialization.

javascript
app.instances.get('loader')

Router

Manages page transitions using Swup.

javascript
app.instances.get('router')

Scroller

Manages smooth scrolling with Lenis.

javascript
app.instances.get('scroller')

Device

Provides device detection utilities.

javascript
app.instances.get('device')

Resizer

Handles window resize events.

javascript
app.instances.get('resizer')

Ticker

GSAP ticker for animations.

javascript
app.instances.get('ticker')

DataLayer

Google Tag Manager data layer management.

javascript
app.instances.get('datalayer')

Event System

Custom Events

Components can trigger and listen to custom events:

javascript
// Trigger event
app.instances.get('eventManager').trigger('customEvent', {
  data: 'value'
})

// Listen to event
app.instances.get('eventManager').on('customEvent', (data) => {
  console.log(data)
})

Event Scoping

Events can be scoped to specific components or pages:

javascript
// Page-scoped event
app.instances.get('page').instances.get('eventManager').trigger('pageEvent')

Page Transitions

LamaPress uses Swup for smooth page transitions:

How It Works

  1. Link Click - User clicks an internal link
  2. Content Fetch - Swup fetches new page content
  3. Page Leave - Current page's leave() methods are called
  4. Content Replace - New content replaces old content
  5. Page Enter - New page's enter() methods are called

Component Lifecycle During Transitions

javascript
// Current page
component.destroy() // Cleanup
component.leave()   // Exit animation

// New page
new Component(element) // Constructor
component.init()       // Initialization
component.enter()      // Enter animation

Accessing Page Instances

javascript
// Get current page
const page = app.instances.get('page')

// Get component instance on current page
const accordion = page.instances.get(accordionElement)

Best Practices

1. Use Arrow Functions

Always use arrow functions for component methods:

javascript
// ✅ Good
init = () => {
  // ...
}

// ❌ Bad
init() {
  // ...
}

Why: Arrow functions preserve this context

2. Bind Timelines to Instance

Store GSAP timelines on the component instance:

javascript
this.timeline = gsap.timeline()

Why: Allows proper cleanup in destroy()

3. Always Implement destroy()

Every component should clean up:

javascript
destroy = () => {
  killTimeline(this.timeline)
  // Remove event listeners
  // Kill ScrollTriggers
}

Why: Prevents memory leaks and conflicts during page transitions

4. Use Helper Functions

Import and use helper functions:

javascript
import { killTimeline } from '@src/js/core/utils/helpers'

Why: Consistent cleanup patterns

5. Access Other Components Safely

Always check if instances exist:

javascript
const loader = app.instances.get('loader')
if (loader) {
  loader.promises.push(promise)
}

6. Use data-component Attribute

Always include data-component in your HTML:

html
<div data-component="blocks/accordion">

Why: Enables automatic component loading

7. Import Paths

Use path aliases for imports:

javascript
import { app } from '@src/js/core/app'
import { killTimeline } from '@src/js/core/utils/helpers'

Why: Cleaner imports, easier refactoring

Common Patterns

Pattern 1: Hover Animations

javascript
bindEvents = () => {
  this.element.addEventListener('mouseenter', this.handleMouseEnter)
  this.element.addEventListener('mouseleave', this.handleMouseLeave)
}

handleMouseEnter = () => {
  killTimeline(this.hoverTimeline)
  this.hoverTimeline = gsap.timeline()
  this.hoverTimeline.to(this.element, {
    scale: 1.05,
    duration: 0.3
  })
}

handleMouseLeave = () => {
  killTimeline(this.hoverTimeline)
  this.hoverTimeline = gsap.timeline()
  this.hoverTimeline.to(this.element, {
    scale: 1,
    duration: 0.3
  })
}

Pattern 2: Scroll-Triggered Animations

javascript
createRevealAnimation = () => {
  this.timeline = gsap.timeline({
    scrollTrigger: {
      trigger: this.element,
      start: 'top 80%',
    }
  })
  
  this.timeline.from(this.element, {
    opacity: 0,
    y: 20,
    duration: 0.6
  })
}

Pattern 3: Accessing Child Components

javascript
init = () => {
  this.items = [...this.element.querySelectorAll('[data-component="blocks/accordion_item"]')]
}

closeItems = () => {
  this.items.forEach(item => {
    const instance = app.instances.get('page').instances.get(item)
    if (instance && instance.close) {
      instance.close()
    }
  })
}

Next Steps:

Released under the MIT License.