Theme Framework How the LeaseLeads theme frontend works

Sub-components & HasX

How a host discovers, loads, and drives the behavioural components inside it.

On this page 5

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 own booted event, 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.

Important

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:

js
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 imageComponents runs a single querySelectorAll over 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.
  • bootImageComponents is a boot* method, so the lifecycle collects and runs it automatically. It only queues — it converts a descriptor onto the host's componentProcessor if a matching element is present.
  • The processor is shared. Every HasX on the host queues onto the same componentProcessor the host provides, so all the queued components drain together in one afterBoot pass. 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:

js
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:

js
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:

Warning

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:

js
const Final = new Traits(Intro, [
  AutoInitializes,
  HasImage,
  HasButtons,
  HasSplideGallery,
])