UI toolkit
Reference for dd.ui: create, signal, computed, effect, bindParts, token, pressGuard, and the components registry.
DOM and reactive helpers. Destructure what you use at the top of widget.js:
const { create, signal, computed, effect } = dd.ui;Two design constraints run through everything here, both deliberate:
- Bindings are declared visibly at the
create()call site: a prop or child that is a function or Signal is reactive, everything else is applied once. - Updates are synchronous. There is no scheduler and no batching, so nothing hides execution order.
dd.ui.create()
Builds a DOM element.
create(tag: string, props?: Props, ...children: Child[]): HTMLElement | SVGElementProps are optional: when the second argument is a Node, string, number, array, function, or Signal, it is treated as the first child.
| Prop | Behavior |
|---|---|
class | sets className (write class, not className) |
style | object: merged into el.style (camelCase keys); string: the style attribute |
on* (onClick, onPointerDown, ...) | addEventListener with the lowercased remainder; never reactive |
ref: (el) => void | called after props and children are applied, for elements you keep a handle to |
key contains - (data-*, aria-*, stroke-width), or the element is SVG | attribute: true becomes "", false and nullish remove it |
| anything else | element property if one exists (hidden, disabled, value), else attribute |
value is a function or Signal (except on* and ref) | live binding, re-applied whenever the signals it reads change |
Children: strings and numbers become text nodes, arrays are flattened, null,
undefined and booleans are skipped (so cond && node works), and a function
or Signal child binds one text node (String(value), nullish renders as
""). Reactive children are text-only by contract; for reactive structure,
write an explicit effect(() => container.replaceChildren(...)). SVG tags
(svg, path, circle, rect, ...) are created in the SVG namespace
automatically.
Signals
signal<T>(initial: T): Signal<T> // { value, peek() }
computed<T>(fn: () => T): Signal<T>
effect(fn: () => void): () => voidsignal(initial) returns an object with a value you read and write, plus
peek() to read without subscribing the current effect. computed(fn)
derives eagerly from the signals fn reads. effect(fn) runs now and again
whenever any signal it read changes; it returns a dispose function, usually
ignored.
Semantics, all deliberate:
- Synchronous, no batching. A write re-runs subscribers before it returns; an effect reading N signals may run up to N times for a multi-signal update.
- Writes are
Object.isguarded. Assigning an equal value propagates nothing, so acomputedthat recomputes to the same result stops the chain. - A cascade of more than 1000 effect runs from one write throws (and logs). That is the circular-write guard.
- No disposal lifecycle. Widgets die with their iframe, so effects simply
live as long as the document. The one backstop: bindings created by
create()self-dispose when their element has left the DOM.
The one rule when mixing patterns: per subtree, pick build-once with
bindings, or wholesale re-render with plain create(). Never put reactive
props inside rows that a render function throws away; the auto-dispose is a
backstop, not a design.
dd.ui.bindParts()
Applies live props to markup that already exists: the companion to create()
for a subtree whose shape is fixed but whose parts react. Stateful inline SVG
is the usual case.
bindParts(root: Element, bindings: Record<string, Props>): voidAuthor the art in index.html, mark the changing nodes data-bind="name",
then pass the same props create() takes. Reactivity, the
attribute-vs-property rules, on* listeners, ref, and element-lifetime
auto-dispose all behave identically.
bindParts(batteryEl, {
level: {
width: () => (pct.value / 100 * 11.6).toFixed(1),
opacity: () => (charging.value ? "0" : "1"),
},
bolt: { opacity: () => (charging.value ? "1" : "0") },
});root itself is eligible if it carries a data-bind. A name matching several
nodes binds all of them; a name matching none logs a warning (typo insurance).
Text is just a prop: { textContent: () => label.value }.
dd.ui.token()
token(name: string, fallback?: string): stringReads a design token (a --dd-* custom property) as a string, for JS that
cannot use CSS variables directly: canvas fills, inline-SVG strokes, computed
layout. It reads the current value off the document root, so calling it again
after a dd.theme.onChange picks up the new theme. fallback is returned
when the token is unset. Unlike dd.theme.tokens(), this sees
manifest font pins, so canvas text should
resolve faces through it.
function readColors() {
ctx.strokeStyle = dd.ui.token("--dd-chart-1", "#888");
}
dd.theme.onChange(() => { readColors(); redraw(); });dd.ui.pressGuard()
pressGuard(el: Element, ms?: number): booleanThe double-activation guard for button-ish elements: returns false while
el is still disabled from a previous press, otherwise disables it now and
re-enables it after ms (default 500). Works on dd-* component hosts too;
they reflect disabled onto their inner button. Use it at the top of a click
handler:
btn.addEventListener("click", () => {
if (!pressGuard(btn)) return;
// ...
});dd.ui.components
The dd-* custom element constructors, for subclassing: DdElement,
DdPanel, DdText, DdIcon, DdButton, DdIconButton, DdChip,
DdMeter, DdAppTile, DdVolumePill. See
the component reference for the
override pattern.