Theme Framework How the LeaseLeads theme frontend works

Traits

How a component composes many capabilities at once — the flattening mixin factory.

On this page 4

A Module is rarely just one thing. Intro has a background, images, buttons, a carousel, scroll animation, and a fade-in — all at once. JavaScript's single inheritance can't express "is all of these," so the theme composes capabilities with a flattening mixin factory called Traits.

How you use it

Every component file ends the same way: define the class, then stamp its capabilities onto it and export the result under the component's moniker.

js
class Intro extends Instantiable {
  static selector = '.intro'

  async init() {
    await this.carousel.init()
    await this.autoInitializeComponents()
    await super.init()
  }
}

const Final = new Traits(Intro, [
  AutoInitializes,
  FadesIn,
  HasBackground,
  HasImage,
  HasButtons,
  HasSplideGallery,
  AnimatesOnScroll,
])

export { Final as Intro }

You read a component's capabilities as a flat list. Adding one is a one-line edit to the array — never a new subclass. HasImage gives the host its imageComponents getter and an bootImageComponents(); FadesIn gives it initWithFade(); AutoInitializes makes it kick off its own init(). The class body stays focused on what's unique to this component.

Important

The export alias must match the component's registered name: export { Final as Intro }. The loader resolves the class by that name (exports['Intro']), so a mismatch throws at load. See The Globals registry for the name/moniker rules.

What new Traits(...) actually does

It copies each base's prototype and static members onto the derived class as own-properties, then returns the derived class:

js
new Traits(Intro, [FadesIn, HasImage])
// → Intro, now owning initWithFade, imageComponents, bootImageComponents, …

The bases never enter the prototype chain — their members are stamped on. That's what lets the lifecycle collector find every trait's boot* and init* methods on the one flattened prototype, so a trait can contribute lifecycle work just by naming a method correctly.

Note

Why it works this way. A Module needs many orthogonal capabilities at once, and single inheritance can only give it one parent. A mixin factory makes capabilities composable and the list declarative. The trade-off is that this is not real inheritance: super doesn't reach trait methods, name collisions resolve last-write-wins in list order, and instanceof against a trait won't work. Capabilities are added by editing the list, never by subclassing.

Writing a trait

A trait is just a class whose members you want stamped onto a host. The HasX family (see Sub-components & HasX) are traits built on a shared base so a host can discover and load nested components; simpler traits like FadesIn are a plain class with one method:

js
export class FadesIn {
  initWithFade() {
    // runs during the host's init phase, because it is named init*
  }
}

Name a method boot* or init* and it joins the host's lifecycle automatically. That is the whole contract.