Appearance
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
- Core System
- Component Loading
- Component Lifecycle
- Core Instances
- Event System
- Page Transitions
- Best Practices
Architecture Overview
The JavaScript architecture follows these principles:
- Automatic Component Loading - Components are discovered via
data-componentattributes - Lifecycle Management - Components have consistent lifecycle methods
- Instance Management - All component instances are tracked and accessible
- Event-Driven - Components communicate through events
- 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:
- Configure GSAP
- Set defaults (instances Map, Tailwind theme)
- Create core instances
- Create app-level instances
- Create page instances
- 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:
- Scans for all
[data-component]elements - Extracts the component path (e.g.,
blocks/accordion) - Dynamically imports the JavaScript file
- 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.jsDynamic 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 withdata-componentattribute
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
- Link Click - User clicks an internal link
- Content Fetch - Swup fetches new page content
- Page Leave - Current page's
leave()methods are called - Content Replace - New content replaces old content
- 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 animationAccessing 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()
}
})
}Related Documentation
- Component System - Component structure and organization
- Common Patterns - JavaScript patterns and examples
- Component Guidelines - Component JavaScript standards
Next Steps:
- Review Component System to understand component structure
- Check existing components for real-world examples
- See Common Patterns for JavaScript examples