Guide
Every Ultima component extends, directly or indirectly, ComponentBase.
Components that need declarative data binding extend ReactiveComponent,
which in turn extends ComponentBase.
ComponentBase
Handles loading the external HTML template, optional isolation via Shadow DOM, and resolving paths relative to the component's own module (not to the page that uses it).
constructor(properties, subclassUrl)
properties.templateUrl— path to the template's HTML file, relative to the.jsmodule.properties.shadowDom—trueto isolate in Shadow DOM (mode:'open');falseto render directly as children of the element itself.subclassUrl— always the child class'simport.meta.url.
Reacting to loading: onLoad()
Override onLoad() to run initialization once the component — and all
of its descendants that are also ComponentBase — finish mounting.
It's called exactly once per instance, directly at the point the framework determines loading is
complete, so there's no event to subscribe to and no filtering to do yourself:
onLoad() {
super.onLoad();
// the component itself finished loading — safe to read this.rootNode, set initial state, etc.
}
ReactiveComponent itself overrides onLoad()
to trigger the first render() — subclasses that override it too must
call super.onLoad(), the same rule as any other overridden hook.
The loaded event (for code outside the component)
ComponentBase.LOADED_EVENT ("component-loaded")
is the event backing onLoad(). It bubbles
(bubbles:true, composed:true) and crosses Shadow DOM boundaries, which
is what lets a parent component know when a nested child is done. Code that isn't the
component's own class (e.g. the page that uses it) can't override onLoad(),
so use element.whenLoaded() instead — it returns a Promise that
resolves once, whether the element is already loaded or not:
brTest.whenLoaded().then(() => {
// the brTest element itself finished loading
});
addEventListener(ComponentBase.LOADED_EVENT, ...)?
The event bubbles from any descendant, not just the element the listener is on, and when
a descendant has its own Shadow Root the browser retargets it —
event.target shows up as the listening element itself even though the
event came from inside the descendant. whenLoaded() already filters
that out internally via event.composedPath()[0] and removes its
listener after resolving once — this has already caused real re-render loops in this project
when done by hand, which is why it's now built in rather than a pattern to repeat per component.
ReactiveComponent
Adds declarative reactivity via data-* attributes read from the
template — the full list is in the DSL reference. Here, the
concepts behind it.
State
get state() // returns the current state
set state(newState) // writes and re-renders, if anything changed
The data-state HTML attribute is also observed — changing it from
the outside (setAttribute) has the same effect as using the
state setter. Writes that don't change the content (compared by
value, not by reference) are ignored — they don't trigger a re-render or events.
Computed properties and watchers
computed() // -> Object, derived from this.state, merged into every binding
watchers() // -> { "path": (newValue, oldValue) => {...} }
Override computed() to declare values derived from the raw state —
the return value is available to any binding (data-bind,
data-if, data-class,
data-style, data-for) as if it were a
regular piece of state. Override watchers() to react whenever a
specific path's value changes between one render and the next.
Scope
Every nesting level — the component's root, a resolved conditional branch, a list item — has
its own data scope. Inside a list item, the loop variable's name is available as if it were a
regular field, and whatever comes from the parent scope stays accessible (scope chains by
prototype). Two nested reactive components don't share scope: a child receives data explicitly
via data-bind='{"data-state":"path"}'.
List reconciliation
data-for can reconcile by key — when the item has a stable identity
field (declared, or inferred from uuid/id),
existing DOM nodes are reused and moved instead of being destroyed and recreated on every
render, preserving focus, scroll position and nested components' internal state. Full details
and syntax in the Reference.