Theme Framework How the LeaseLeads theme frontend works

The lifecycle

boot → afterBoot → init → destroy, and what belongs in each phase.

On this page 6

Every Instantiable runs the same four ordered phases. Understanding them is mostly understanding one rule:

boot declares what you need and defers; init wires behaviour once what you declared has loaded.

Everything else — parallelism, events, idempotency — exists to make that rule safe regardless of what order components come up in.

The four phases

Phase When What belongs here
boot* On construction Read the element, queue sub-component assets (bootXComponents), register deferred listeners. Assume nothing else is loaded.
afterBoot* Once every boot* has resolved Drain the now-complete asset queue (afterBootLoadComponents).
init* Once loaded — via the booted event or a controller Wire real interactions, call autoInitializeComponents(). Idempotent.
destroy* Reserved Collected symmetrically; no live teardown use yet.

Methods are collected by prefix

You don't call these phases by hand, and you don't register methods into them. The framework walks the entire flattened prototype — every method a class owns plus everything Traits stamped onto it — and runs every method whose name starts with the phase prefix. The contract is entirely in the name:

js
class Intro extends Instantiable {
  bootImageComponents() { /* queues assets — runs in the boot phase */ }
  initWithFade()        { /* wires behaviour — runs in the init phase */ }
}

Name a method bootImageComponents or initWithFade and it joins the host's lifecycle automatically — even from a trait file the host class never mentions. afterBoot*, init*, and destroy* are collected the same way, each phase draining before the next begins.

Important

Same-phase methods run in parallel, not in sequence — the .sort() fixes collection order, not execution order. Never make one init* method depend on another init* having finished. If you need ordering, do it explicitly inside a single method (that is what a controller's init() is for — see Sub-components & HasX).

Why boot and afterBoot are separate

boot* methods only queue work — each bootXComponents adds an asset to a shared processor but doesn't load it. afterBoot* drains that queue. Splitting them means a boot* method never has to care whether the method that drains the queue ran before or after it: the queue is guaranteed complete before anything reads it. You get this split for free — the HasX traits contribute the boot*/afterBoot* pair; your class just names its nested components.

The lifecycle events

Each phase ends by firing a pair of CustomEvents on the element — a generic module.* and a moniker-scoped variant (e.g. intro.*) — each carrying the instance in event.detail.instance. You listen for them; the framework fires them:

  • created — fired by make, the instant the instance exists.
  • booted — fired after the boot phase resolves.
  • initialized — fired after the init phase resolves.

These are the theme's public seam. External scripts listen for intro.initialized (or the generic module.initialized) to get a fully-wired instance — see The Globals registry.

init is idempotent

init() guards itself. It can be triggered by the host's own booted event (via the AutoInitializes trait) and called again by a controller ordering its children — the second call is a no-op. That guarantee is what lets autoInitializeComponents() safely re-init a subtree after a controller has already hand-initialized part of it. In practice this frees you from tracking who initialized what: call init() whenever you need an instance ready, and trust that the real wiring runs exactly once.

Note

Why it works this way. Nested components can be reached by two paths — the automatic bootedinit chain, and a parent controller that wants to order its children explicitly. Without the guard, the two paths would double-wire. With it, either path (or both) leaves the instance initialized exactly once.