Reusable component primitive on top of
uiSession. Encapsulates template + state + lifecycle + automatic cleanup.
Module component | Source packages/front/fw/src/dom/rendering/component.js | Deps none | Worker-safe no
Resolve
const component = runtime.resolve('component');
// → { define }
API
| Method | Signature | Returns |
|---|---|---|
define |
(spec) => (opts: { ui, parent?, slot?, id?, props }) => Instance |
Component factory |
instance.update |
(newProps) => void |
Re-validates, re-applies propsFn, triggers update |
instance.destroy |
() => void |
Explicitly unmounts (idempotent) |
instance.get / .text / .attr / .on |
proxies | See self |
instance.props |
getter | Current transformed props |
instance.blockId |
getter | Internal BlockId |
instance.state |
getter | Free state object populated by hooks |
instance.scopeClass |
getter | Scope CSS class or null |
component.define(spec) → factory
Defines a component blueprint. Returns a function create({ui, parent?, slot?, id?, props}) → instance.
spec field |
Type | Description |
|---|---|---|
template |
ParseResult |
Required. HTML template parsed by parser.fromHTML or ui.parse. |
name |
string |
Optional. Human name used in error messages (component[name]: …). |
props |
Object<string, PropSpec> |
Optional. Props validation schema (see below). |
css |
string |
Optional. Scoped CSS — & is rewritten to .fw-comp-N. Injected once per blueprint into <head>. |
propsFn(props) |
Function |
Optional. Transforms validated props into DOM bindings before render. Re-evaluated on every update(). |
mount(self) |
Function |
Optional. Called once after DOM mount. Ideal for self.on(...). |
update(self, prevProps) |
Function |
Optional. Called on instance.update(newProps), after re-applying bindings. |
unmount(self) |
Function |
Optional. Called before the DOM disappears. App cleanup (fetches, observers, intervals). |
PropSpec
| Field | Type | Description |
|---|---|---|
type |
'string'|'number'|'boolean'|'function'|'object'|'array' |
Type check; 'array' uses Array.isArray. |
required |
boolean |
Throws if the prop is absent at creation or update. |
default |
any | () => any |
Default value when the prop is undefined. Function form = re-evaluated per instance (useful for []/{}). |
validator |
(value) => boolean |
Throws if returns false. |
The self passed to hooks exposes:
| Member | Description |
|---|---|
self.props |
Transformed props (after optional propsFn) — the exact shape bound to the DOM. |
self.rawProps |
Validated props before propsFn — raw user input after defaults are applied. |
self.prevProps |
Previous transformed props (populated during update). |
self.ui |
The parent UISession. |
self.blockId |
Internal component BlockId. |
self.scopeClass |
Scope CSS class ('fw-comp-N') or null if no css. |
self.state |
Free object persisted between hooks (timers, refs, abort controllers). |
self.bind |
Lazy reactiveBind controller (see Reactive bindings). undefined after unmount. |
self.get(lid?) |
ui.get(blockId, lid); without argument → block root. |
self.text(lid?, value) |
Proxy of ui.text(blockId, ...). |
self.attr(lid?, name, value) |
Proxy of ui.attr(blockId, ...). |
self.on(lid?, type, fn, …) |
Proxy of ui.on(blockId, ...). Listener auto-removed on destroy. |
self.query(lid, sel) / self.queryAll(lid, sel) |
CSS selector within a subtree. |
self.off(name) |
Removes a managed listener. |
Instance
const instance = ComponentFactory({ ui, parent, slot, id, props });
| Method / Property | Description |
|---|---|
instance.update(newProps) |
Re-validates, re-applies propsFn, triggers the update hook. |
instance.destroy() |
Explicitly unmounts (DOM, listeners, hooks). Idempotent. |
instance.get(lid?) / .text/.attr/.on |
Same proxies as on self. |
instance.props |
Getter — current transformed props. |
instance.blockId |
Internal BlockId. |
instance.state |
Free state object populated by hooks. |
instance.scopeClass |
Scope CSS class or null. |
Lifecycle
create(opts)
├─ ui.add(...) ← DOM mounted
├─ ui.onUnmount(blockId, …) ← hook attached
└─ spec.mount(self) ← called once
instance.update(newProps)
└─ spec.update(self, prev)
instance.destroy()
└─ ui.clear(blockId)
└─ ui.onUnmount fire ← once only
└─ spec.unmount(self)
// Indirect unmount (parent cleared externally):
ui.clear(parentBlockId)
└─ cascade → ui.clear(childBlockId)
└─ ui.onUnmount fire → spec.unmount(self)
The component survives external clears: if the parent block is ui.clear-ed, the cascade descends via _children tracking and fires the component's unmount hook.
Examples
Card with fetch and cleanup
const cardTpl = ui.parse(`
<article id="root" class="card">
<h3 id="title">#{title}</h3>
<p id="body">#{body}</p>
<button id="closeBtn">×</button>
</article>
`);
const Card = component.define({
template: cardTpl,
mount(self) {
self.on('closeBtn', 'click', () => self.props.onClose?.());
// Resource lifecycle : abort controller in self.state
self.state.controller = new AbortController();
fetch(self.props.url, { signal: self.state.controller.signal })
.then(r => r.text())
.then(text => self.text('body', text));
},
update(self, prev) {
// Re-fetch only if the URL changed
if (self.props.url !== prev.url) {
self.state.controller.abort();
self.state.controller = new AbortController();
fetch(self.props.url, { signal: self.state.controller.signal })
.then(r => r.text())
.then(text => self.text('body', text));
}
},
unmount(self) {
self.state.controller.abort(); // cancel in-flight fetch
},
});
// Instantiate
const c = Card({
ui,
parent: 'main',
slot: 'content',
id: 'card-42',
props: { title: 'Hi', body: '…', url: '/data/42', onClose: () => c.destroy() },
});
// Update props later
c.update({ title: 'Hi v2', body: '…', url: '/data/42', onClose: …});
// Or destroy explicitly
c.destroy();
Scoped CSS
spec.css supports three authoring patterns beyond the basic & { } root selector:
Nesting (& chains)
Nested & chains are flattened to plain CSS selectors at injection time — no
browser nesting support is required:
/* authored */
& { padding: 1rem; }
& .title { font-weight: bold; }
& .title .sub { color: gray; }
/* injected (all & resolved to .fw-comp-N) */
.fw-comp-7 { padding: 1rem; }
.fw-comp-7 .title { font-weight: bold; }
.fw-comp-7 .title .sub { color: gray; }
:global(sel) — escape hatch
Wrap a selector in :global(...) to emit it without the scope prefix:
/* authored */
:global(body) { margin: 0; box-sizing: border-box; }
& { color: var(--fw-color-text); }
/* injected */
body { margin: 0; box-sizing: border-box; }
.fw-comp-7 { color: var(--fw-color-text); }
@keyframes — unique per-blueprint names
Keyframe identifiers are renamed to <scopeClass>-<name> so two components
that both define @keyframes spin never collide. Every animation /
animation-name reference in the same blueprint is rewritten to the new name
automatically:
/* authored */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
& .icon { animation: spin 1s linear infinite; }
/* injected (blueprint gets scope class fw-comp-7) */
@keyframes fw-comp-7-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.fw-comp-7 .icon { animation: fw-comp-7-spin 1s linear infinite; }
Injection remains once per blueprint — multiple instances of the same
component share the single <style data-fw-component="fw-comp-N"> tag in
<head>.
Reactive bindings (self.bind)
self.bind is a reactiveBind controller scoped to the
component's uiSession. It is created lazily on first access and
disposed automatically when the component unmounts — no manual cleanup
needed:
const Ticker = component.define({
template: ui.parse('<div id="root"><span id="count">0</span></div>'),
mount(self) {
const count = sig.create(0);
// Bind the signal to the <span id="count"> node.
self.bind.text(self.blockId, 'count', count);
// Increment every second (stored in self.state for cleanup reference).
self.state.timer = setInterval(() => count.set(count.peek() + 1), 1000);
},
unmount(self) {
clearInterval(self.state.timer);
// self.bind.dispose() is called automatically — no explicit call needed.
},
});
Auto-disposal contract:
self.bindisundefinedafter the component unmounts (whether destroyed explicitly viac.destroy()or removed via a parentui.clear()).- Accessing
self.bindafter unmount returnsundefinedand does not create a new controller — a disposed component stays disposed. - If
self.bindwas never accessed, no controller is created and unmount is a no-op fromreactiveBind's perspective.
See reactiveBind for the full controller API
(text, attr, class, style, show, model, list, dispose).
Notes
self.propsvsself.rawProps:self.propsholds the shape afterpropsFn— the exact data bound to the DOM.self.rawPropsholds validated props before transformation. Both are updated symmetrically on everyupdate().- No automatic reactivity:
update(newProps)must be called explicitly. For fine-grained DOM patching without a full re-render, useself.bind(signals) orself.attr/self.textdirectly in theupdatehook. - Scoped CSS:
spec.cssis injected once per blueprint (define()) — no duplication even on hot-reload. The.fw-comp-Nclass is added automatically to every instance's root element. - Props validation: throws on missing required prop or wrong type, at construction and on every
update(). The function form ofdefaultis re-evaluated per instance at creation. - State isolation: each instance has its own
self.state. No implicit sharing between components. - Hook errors:
unmountis internally try-caught (never blocking).mount/updatepropagate errors to the caller — intentional for easier debugging.
See also
- uiSession — underlying session layer.
- reactiveBind — signal-to-DOM binding controller used by
self.bind. - parser — produces
ParseResulttemplates. - Guide rendering-pipeline.