A Module rarely acts alone. Intro contains images, buttons, a background, a gallery —
each a real behavioural Component with its own lifecycle. Something has to find those,
load their chunks, and initialize them in the right order. That something is the
host, and the capability it uses is the HasX trait family.
Host vs sub-component
Two roles, one clean split:
- A host (a Module, Layout, Template, or Global) mixes
AutoInitializes, so it self-inits off its ownbootedevent, and it is the sole controller of every behavioural Component in its subtree. - A sub-component is a behavioural Component discovered inside a host. It is
passive: it never mixes
AutoInitializes, so it never self-inits.
That asymmetry is deliberate. Because sub-components don't init themselves, the host
keeps total control over when and in what order they come up. Mixing
AutoInitializes is all it takes to make something a host: the trait listens for the
host's own booted event and kicks off its init() — you never wire that listener
yourself, you just add the trait to the list.
A Component never mixes AutoInitializes. If it did, it would race its host and
the host would lose ordering control. Self-init is a host-only privilege.
What a HasX trait adds
Each HasX gives its host two methods for one component type — a getter that
discovers instances, and a boot method that queues the chunk:
const HasImage = new Traits(class {
get imageComponents() {
const cls = Globals.get('Image')
if ( ! cls ) return []
const els = Array.from(this.element.querySelectorAll('div.image'))
if ( this.element.matches('div.image') ) els.unshift(this.element)
return els.map(el => cls.instance(el))
}
async bootImageComponents() {
if ( ! this.element.querySelector('div.image') && ! this.element.matches('div.image') ) {
return
}
return this.componentProcessor.convert({
selector: 'div.image',
name: 'Image',
loader: () => import('../../components/Image/Image.js'),
})
}
}, [AutoLoadsComponent])
Three things to notice:
- Discovery is flat, not recursive.
get imageComponentsruns a singlequerySelectorAllover the host's entire subtree and returns every match at any depth — it does not walk a component tree. One host sees all its descendants of that type in one flat list. bootImageComponentsis aboot*method, so the lifecycle collects and runs it automatically. It only queues — it converts a descriptor onto the host'scomponentProcessorif a matching element is present.- The processor is shared. Every
HasXon the host queues onto the samecomponentProcessorthe host provides, so all the queued components drain together in oneafterBootpass. You get that processor and its drain for free — the trait base supplies both.
Driving them: autoInitializeComponents()
Once the chunks have loaded, the host inits its sub-components by calling
autoInitializeComponents(). It walks the host's own get *Components() getters and
calls init() on each discovered instance, skipping any already initialized — you call
the one method and the whole subtree comes up:
await this.autoInitializeComponents() // init every discovered sub-component, once each
The isInitialized guard is what makes this composable. A host can hand-initialize
the children whose order matters, then call autoInitializeComponents() to sweep up the
rest — the ones it already did are skipped:
async init() {
await this.carousel.init() // ordering that matters, done explicitly
await this.autoInitializeComponents() // everything else, idempotently
await super.init()
}
The contract
This is the one rule that catches people:
A Module must mix the matching HasX for every behavioural Component it can
contain — or that component never boots and stays dead. There is no global scanner
that finds loose components; discovery is entirely host-driven. If Intro can hold
an Image but doesn't mix HasImage, the image renders its markup and never wires
its behaviour.
You add a capability the same way you add any trait — one
line in the host's new Traits(...) list:
const Final = new Traits(Intro, [
AutoInitializes,
HasImage,
HasButtons,
HasSplideGallery,
])
Related
- Traits — how
HasXgets stamped onto a host. - The lifecycle — how
bootXComponentsandinit*are collected and run. - The AssetLoader pipeline — the Processor
bootXComponentsqueues onto.