When you add a new behavioural Component, no host can
use it until a matching HasX trait exists. The trait is what lets a host find the
Component in its subtree and lazy-load its asset. This page walks the whole edit, based on
the real HasImage / HasButtons traits in
utilities/Traits/AutoInitializableComponent.js.
Say you've built a Tabs Component (selector .tabs, class registered as Tabs). Here's
how you make it discoverable.
1. Write the trait
Every HasX is a class built on the shared AutoLoadsComponent base and stamped through
Traits. It contributes exactly two members: a getter and a boot* method.
const HasTabs = new Traits(class {
get tabsComponents() {
const cls = Globals.get('Tabs')
if ( ! cls ) {
return []
}
const els = Array.from(this.element.querySelectorAll('.tabs'))
if ( this.element.matches('.tabs') ) {
els.unshift(this.element)
}
return els.map(el => cls.instance(el))
}
async bootTabsComponents() {
if ( ! this.element.querySelector('.tabs') && ! this.element.matches('.tabs') ) {
return
}
return this.componentProcessor.convert({
selector: '.tabs',
name: 'Tabs',
loader: () => import('../../components/Tabs/Tabs.js'),
})
}
}, [AutoLoadsComponent])
Both halves follow a rigid convention — copy an existing trait and rename.
The getter: get xComponents()
querySelectorAllover the whole subtree, plus athis.element.matchescheck for the host itself. Discovery is deliberately flat, not recursive — the host reaches every matching element beneath it in one query, regardless of nesting.Globals.get('Tabs')first, bail to[]if absent. The class may not be loaded yet at the moment a getter is read. Returning[]keeps callers safe until the asset resolves. See The Globals registry.cls.instance(el)returns the one instance per element fromInstantiable's registry — never a fresh construction.
The getter's name must end in Components — tabsComponents, not tabs. That
suffix is exactly what autoInitializeComponents()
scans the prototype for. Name it anything else and the Component loads but never inits.
The boot method: bootXComponents()
- Presence-gated. It returns early unless a
.tabsis actually present, so nothing is queued for a host that doesn't contain the Component. this.componentProcessor.convert(...)queues the asset on the host's sharedProcessor— the oneAutoLoadsComponentlazily creates and everyHasXon the host reuses.selector,name, andloadermirror a global loader entry.- Named
boot*, so the lifecycle collects it into the host's boot phase automatically. All the host'sboot*calls queue their assets, thenafterBootLoadComponents()drains the processor once — which is why boot order between traits never matters.
2. Export it
Add the trait to the export block at the bottom of
utilities/Traits/AutoInitializableComponent.js:
export {
HasBackground,
HasImage,
HasButtons,
// …
HasTabs,
}
3. Mix it into the host
In the host Module (or Layout, or Template), add the trait to its Traits list:
import { HasTabs } from '../../utilities/Traits/AutoInitializableComponent.js'
const Final = new Traits(Intro, [
AutoInitializes,
HasBackground,
HasTabs, // ← now Intro can discover .tabs in its subtree
])
export { Final as Intro }
That single line is the whole contract. new Traits stamps tabsComponents and
bootTabsComponents onto the host as own-properties, so the lifecycle collector finds
them on the flattened prototype.
4. Confirm the host reaches init
A behavioural host already calls autoInitializeComponents() in its init():
async init() {
// …hand-ordered work…
await this.autoInitializeComponents()
await super.init()
}
autoInitializeComponents() walks every get *Components() getter on the prototype chain
and inits each discovered instance, skipping any already initialized. Because your getter
is named tabsComponents, it's swept up here for free — there's nothing to register. If
the host has no init() of its own, it inherits one that still runs the sweep.
Why the split into two conventions. The boot* method queues the download; the
*Components getter feeds the init sweep. Separating "load the asset" from "init the
instance" is what lets a host lazy-load a Component during boot and then order its init
precisely during init — the load is presence-gated and parallel, the init is controlled
and idempotent. See Sub-components & HasX.
The failure mode
Forget step 3 and the Component still renders — its PHP view is unaffected — but its
asset never loads and it never inits. It's dead markup. When a behavioural Component
appears on the page but does nothing, the first thing to check is whether its host mixes
the matching HasX.
Related
- Sub-components &
HasX— the discovery-and-load contract in full. - Components — building the Component the trait loads.
- Traits — how
new Traits(...)stamps a trait onto a host. - The lifecycle — how
boot*and the init sweep are collected and run. - The Globals registry — the
name/monikerlookup the getter depends on.