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.
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.
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:
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.
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:
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.
Related
- The lifecycle — how
boot*/init*methods are collected and run. - Sub-components &
HasX— the trait family for nested components. - The Globals registry — name, moniker, and the export-alias rule.