@llui/dom
Runtime for the LLui web framework -- The Elm Architecture with compile-time bitmask optimization.
No virtual DOM. view() runs once at mount, building real DOM nodes with reactive bindings that update surgically when state changes.
Install
pnpm add @llui/dom
Quick Start
import { component, mountApp, div, button } from '@llui/dom'
type State = { count: number }
type Msg = { type: 'inc' } | { type: 'dec' }
const Counter = component<State, Msg, never>({
name: 'Counter',
init: () => [{ count: 0 }, []],
update: (state, msg) => {
switch (msg.type) {
case 'inc':
return [{ ...state, count: state.count + 1 }, []]
case 'dec':
return [{ ...state, count: state.count - 1 }, []]
}
},
view: ({ send, text }) => [
button({ onClick: () => send({ type: 'dec' }) }, [text('-')]),
text((s) => String(s.count)),
button({ onClick: () => send({ type: 'inc' }) }, [text('+')]),
],
})
mountApp(document.getElementById('app')!, Counter)
View<S, M> -- the helper bundle
view receives a single View<S, M> bag. Destructure what you need -- send plus any state-bound helpers. TypeScript infers S from the component definition, so no per-call generics:
view: ({ send, text, show, each, branch, memo }) => [
text(s => s.label), // s is State -- inferred
...show({ when: s => s.visible, render: () => [...] }),
...each({ items: s => s.items, key: i => i.id, render: ({ item }) => [...] }),
]
Element helpers (div, button, span, etc.) stay as imports -- they're stateless and don't need the S binding.
API
Core
| Export | Purpose |
|---|---|
component(def) |
Create a component definition |
mountApp(el, def) |
Mount a component to a DOM element |
hydrateApp(el, def) |
Hydrate server-rendered HTML |
mountAtAnchor(anchor, def) |
Mount a component relative to a comment anchor |
hydrateAtAnchor(anchor, def) |
Hydrate server-rendered HTML relative to a comment anchor |
flush() |
Synchronously flush all pending updates |
createView(send) |
Create a full View bundle (for tests/dynamic use) |
View Primitives
| Primitive | Purpose |
|---|---|
text(accessor) |
Reactive text node |
show({ when, render }) |
Conditional rendering |
branch({ on, cases, default? }) |
Multi-case switching with optional default |
scope({ on, render }) |
Keyed subtree rebuild on key change |
each({ items, key, render }) |
Keyed list rendering |
portal({ target, render }) |
Render into a different DOM location |
memo(accessor) |
Memoized derived value |
sample(selector) |
One-shot imperative state read (no binding) |
selector(field) |
O(1) one-of-N selection binding |
onMount(callback) |
Lifecycle hook (runs once after mount) |
errorBoundary(opts) |
Catch render errors |
foreign({ create, update }) |
Integrate non-LLui libraries |
slice(h, selector) |
View over a sub-slice of state |
Composition
| Export | Purpose |
|---|---|
combine({ slice: reducer, ... }, top?) |
Route messages of shape ${slice}/${action} to slice reducers |
mergeHandlers(...handlers) |
Combine multiple update handlers |
sliceHandler({ get, set, narrow, sub }) |
Route messages to a state slice |
subApp({ reason, def, data?, onHandle? }) |
Embed an isolated TEA loop (escape hatch — requires non-empty reason string) |
Context
| Export | Purpose |
|---|---|
createContext(defaultValue) |
Create a context |
provide(ctx, accessor, children) |
Provide value to subtree |
useContext(ctx) |
Read context value |
Element Helpers
50+ typed element constructors: div, span, button, input, a, h1-h6, table, tr, td, ul, li, img, form, label, select, textarea, canvas, video, nav, header, footer, section, article, p, pre, code, and more.
SSR
| Export | Purpose |
|---|---|
renderToString(def) |
Render component to HTML string |
initSsrDom() |
Initialize jsdom for SSR (from @llui/dom/ssr) |
Sub-path Exports
import { installDevTools } from '@llui/dom/devtools' // dev-only, tree-shaken
import { initSsrDom } from '@llui/dom/ssr' // server-only
import { replaceComponent } from '@llui/dom/hmr' // HMR support
Performance
Competitive with Solid and Svelte on js-framework-benchmark. 5.8 KB gzipped.
Functions
component()
function component<S, M, E = never, D = void>(
def: ComponentDef<S, M, E, D>,
): ComponentDef<S, M, E, D>
createView()
Create a View<S, M> bundle for a component's view callback.
Delegates straight to the underlying primitives — zero per-call overhead.
function createView<S, M>(send: Send<M>): View<S, M>
mountApp()
function mountApp<S, M, E>(
container: HTMLElement,
def: ComponentDef<S, M, E>,
data?: unknown,
_options?: MountOptions,
): AppHandle
hydrateApp()
function hydrateApp<S, M, E>(
container: HTMLElement,
def: ComponentDef<S, M, E>,
serverState: S,
): AppHandle
flush()
function flush(): void
renderToString()
Render a component to an HTML string for SSR. Evaluates view() against the initial state (or provided data), serializes the DOM to HTML, and adds data-llui-hydrate markers on nodes with reactive bindings. Call initSsrDom() once before using this on the server.
function renderToString<S, M, E>(def: ComponentDef<S, M, E>, initialState?: S): string
mergeHandlers()
Compose multiple update handlers into one. Each handler returns [newState, effects] if it handled the message, or null to pass through. The first handler that returns non-null wins.
function mergeHandlers<S, M, E>(
...handlers: Array<(state: S, msg: M) => [S, E[]] | null>
): (state: S, msg: M) => [S, E[]]
createContext()
Create a typed context key. Pass a default value to make consumers without a provider resolve to it; omit to make unprovided consumption throw.
const ThemeContext = createContext<'light' | 'dark'>('light')
function createContext<T>(defaultValue?: T, name?: string): Context<T>
provide()
Provide a reactive value for ctx to every descendant rendered inside children.
The accessor (s: S) => T is evaluated lazily at binding read time, so providers
can thread state slices down without prop drilling.
view: ({ send }) => [
provide(
ThemeContext,
(s: State) => s.theme,
() => [header(send), main(send)],
),
]
Nested providers shadow outer ones within their subtree. The outer value is
restored after children() returns, so sibling subtrees aren't affected.
function provide<S, T>(ctx: Context<T>, accessor: (s: S) => T, children: () => Node[]): Node[]
useContext()
Read a context accessor within a view or view-function. Walks the scope chain
from the current render point to find the nearest provider. Returns an
(s: S) => T accessor that can be passed to bindings (text/class/etc.).
export function themedCard(): Node[] {
const theme = useContext(ThemeContext)
return div({ class: (s) => `card theme-${theme(s)}` }, [...])
}
function useContext<S, T>(ctx: Context<T>): (s: S) => T
sliceHandler()
Lens-style adapter that lifts a sub-component's update into a handler that
operates on a parent's full state and message type. Pairs with
mergeHandlers to compose sub-components into a parent component's reducer.
Full form — explicit lens for custom state paths or message shapes:
sliceHandler({
get: (s) => s.confirm,
set: (s, v) => ({ ...s, confirm: v }),
narrow: (m) => (m.type === 'confirm' ? m.msg : null),
sub: dialog.update,
})
Shorthand — when the state key matches the message's type field and
the parent message wraps the child message in a msg property:
// Equivalent to the full form above.
// Derives get/set/narrow from the key string.
sliceHandler('confirm', dialog.update)
The shorthand assumes the convention:
state[key]holds the sub-state- Messages matching this slice have shape
{ type: key; msg: SubMsg }
function sliceHandler<S, M, E, SubS, SubM>(
keyOrOpts:
| string
| {
get: (state: S) => SubS
set: (state: S, slice: SubS) => S
narrow: (msg: M) => SubM | null
sub: (slice: SubS, msg: SubM) => [SubS, E[]]
},
sub?: (slice: SubS, msg: SubM) => [SubS, E[]],
): (state: S, msg: M) => [S, E[]] | null
composeModules()
Create a merged handler from a map of component modules. Each module's
update is wired via sliceHandler(key, module.update) convention:
state[key] holds the sub-state, messages match { type: key; msg: SubMsg }.
Returns a handler compatible with mergeHandlers.
function composeModules<S, M, E>(
modules: Record<string, { update: (state: never, msg: never) => [unknown, unknown[]] }>,
): (state: S, msg: M) => [S, E[]] | null
text()
function text<S>(accessor: ((s: S) => string) | (() => string) | string, mask?: number): Text
branch()
function branch<S, M = unknown>(opts: BranchOptions<S, M>): Node[]
scope()
Rebuild a subtree when a derived key changes. Sugar over branch({ on, cases: {}, default: render }).
function scope<S, M = unknown>(opts: ScopeOptions<S, M>): Node[]
interface ScopeOptions<S, M> {
on: (s: S) => string
render: (h: View<S, M>) => Node[]
enter?: (nodes: Node[]) => void | Promise<void>
leave?: (nodes: Node[]) => void | Promise<void>
}
Each time on(state) returns a new value (compared with Object.is), the current arm's Lifetime is disposed and render(h) runs against a fresh one. Combine with sample() for a whole-state snapshot read at rebuild time — see the cookbook recipe "Rebuild a subtree when a derived value changes".
sample()
One-shot imperative read of current state inside a render context. No binding is created.
function sample<S, R>(selector: (s: S) => R): R
sample() is a construction-time primitive. It throws if called outside a render context, and it also throws if called from inside an accessor — each.key, each.items, branch.on, show.when, scope.on, child.props, foreign.props, or a binding accessor like text(s => …). Accessors run during the update phase (no render context) and must be pure functions of their parameter; sample() reads outside the parameter, which the compiler's mask analysis can't see. The lint rule llui/no-sample-in-accessor catches the same antipattern at edit time. To depend on outer state inside an accessor, lift the dep into the parameter (e.g. for each.key, bake it into the items map: items: (s) => s.rows.map(it => ({ it, rev: s.rev }))). Also available as h.sample(...) on the View bag.
each()
function each<S, T, M = unknown>(opts: EachOptions<S, T, M>): Node[]
virtualEach()
Virtualized list — renders only the rows visible in the scroll viewport.
Use for lists with 1k+ items where a regular each() would be too slow.
Current limitations:
- Fixed row height (
itemHeight) — dynamic heights not supported - No transitions / animations
- No cross-container reuse (items outside view are fully disposed)
view: ({ text }) => [
...virtualEach({
items: (s) => s.rows,
key: (r) => r.id,
itemHeight: 40,
containerHeight: 600,
render: ({ item }) => [div({ class: 'row' }, [text(item.label)])],
}),
]
function virtualEach<S, T, M = unknown>(opts: VirtualEachOptions<S, T, M>): Node[]
show()
function show<S, M = unknown>(opts: ShowOptions<S, M>): Node[]
slice()
Build a View<Sub, M> that composes a selector into every state-bound
accessor. Used to write view-functions over a sub-slice of parent state:
import { slice } from '@llui/dom'
view: (h) => {
const formView = slice(h, (s) => s.form)
return [...formView.show({ when: f => f.valid, render: (h) => [...] })]
}
Kept as a standalone function rather than a method on the View bundle so apps that don't use it don't pay for its bundle cost — tree-shaken when unused.
function slice<Root, Sub, M>(
h: View<Root, M> | { send: Send<M> },
lift: (r: Root) => Sub,
): View<Sub, M>
portal()
function portal(opts: PortalOptions): Node[]
foreign()
function foreign<S, M, T extends Record<string, unknown>, Instance>(
opts: ForeignOptions<S, M, T, Instance>,
): Node[]
combine()
Compose slice reducers by routing messages of shape ${slice}/${action} to the matching slice. The slice reducer receives the message with msg.type rewritten to the un-prefixed form. Preserves top-level state reference equality when slices return unchanged, which keeps the path-keyed dirty walker precise.
function combine<S, M extends { type: string }, E>(
slices: { [SliceKey: string]: (state: any, msg: any) => [any, E[]] },
top?: (state: S, msg: M) => [S, E[]],
): (state: S, msg: M) => [S, E[]]
subApp()
Embed an independent TEA loop with its own state lifetime. The framework gives you the embedded app's AppHandle through onHandle for imperative send / getState. The reason field is required and non-empty; it surfaces in the rendered DOM as data-llui-sub-app-reason and is enforced at lint time by llui/subapp-requires-reason. Reserved for cases where the embedded code's state lifetime must be distinct from the host's — third-party bundled apps, demo embeds, separately-versioned libraries. Not a tool for decomposing complex views; use view functions for that.
function subApp<S, M, E, D = void>(opts: {
reason: string
def: ComponentDef<S, M, E, D>
data?: D
onHandle?: (handle: AppHandle) => void
}): Node[]
lazy()
Load a component asynchronously. Renders fallback immediately, then swaps
in the loaded component when the loader's Promise resolves. If the loader
rejects, renders error (or nothing if no error handler is provided).
view: ({ text }) => [
...lazy({
loader: () => import('./Chart').then((m) => m.default),
fallback: ({ text }) => [div([text('Loading chart...')])],
error: (err, { text }) => [div([text(`Failed: ${err.message}`)])],
}),
]
The loaded component's S, M, E types are internal — lazy() only needs
the D (init data) type to match. LazyDef<D> is a type-erased shape
that any ComponentDef<S, M, E, D> satisfies structurally, avoiding the
View<S, M> invariance trap that would otherwise require user-side casts.
If the parent scope is disposed before the loader resolves, the load is
cancelled — the loaded component is never mounted.
function lazy<S, M, D = undefined>(opts: LazyOptions<S, M, D>): Node[]
memo()
function memo<S, T>(accessor: (s: S) => T, mask?: number): (s: S) => T
selector()
Optimized "one-of-N" reactive binding — O(1) updates instead of O(n). Uses a generation counter instead of per-row disposers:
- bind() tags entries with the current generation
- Individual row removal bumps the generation for that entry (set gen = -1)
- Bulk clear bumps the generation and clears the registry via registerOnClear
- updateSelector() skips stale entries (gen !== current) and compacts lazily Zero per-row closure allocations. Zero Set.delete calls on disposal.
function selector<S, V>(field: (s: S) => V): SelectorInstance<V>
onMount()
function onMount(callback: (el: Element) => (() => void) | void): void
errorBoundary()
function errorBoundary(opts: {
render: () => Node[]
fallback: (error: Error) => Node[]
onError?: (error: Error) => void
}): Node[]
applyField()
Apply a field update to state immutably. Returns a new state object with the specified field updated. Usage in update(): case 'setField': return [applyField(state, msg.field, msg.value), []]
function applyField<S extends Record<string, unknown>, K extends keyof S>(
state: S,
field: K,
value: S[K],
): S
elSplit()
function elSplit(
tag: string,
staticFn: ((el: HTMLElement) => void) | null,
events: Array<[string, EventListener]> | null,
bindings: Array<[number, BindingKind, string, (state: never) => unknown]> | null,
children: Node[] | null,
): HTMLElement
elTemplate()
Clone a cached HTML template and apply a patch function.
The patch function receives the cloned root element and a bind helper
that registers reactive bindings in the current render context.
Per-item bindings (accessor.length === 0) are registered as direct
updaters on the scope — called by each() when item changes, bypassing
the Phase 2 binding scan entirely.
Fast path for each() rows — 1 cloneNode instead of N createElement.
function elTemplate(html: string, patch: (root: Element, bind: TemplateBind) => void): Element
_handleMsg()
Run a handler for a single message: call update(), reconcile blocks
with the given method, run Phase 2. Used by compiler-generated **handlers
to avoid duplicating boilerplate per message type.
@param method 0=reconcile, 1=reconcileItems, 2=reconcileClear, 3=reconcileRemove, -1=skip blocks
@public — used by compiler-generated **handlers
function _handleMsg(
inst: ComponentInstance,
msg: unknown,
dirty: number,
method: number,
): [unknown, unknown[]]
_runPhase2()
Phase 2: compact dead bindings + update live bindings.
Shared between genericUpdate and compiler-generated **update.
@public — used by compiler-generated **update functions
function _runPhase2(
state: unknown,
dirty: number,
bindings: Binding[],
bindingsBeforePhase1: number,
componentName?: string,
): void
Types
Send
export type Send<M> = (msg: M) => void
Props
Maps a value shape to a reactive-props shape: every field becomes an accessor
(s: S) => V. Use for Level-1 view function signatures.
type ToolbarData = { tools: Tool[]; theme: 'light' | 'dark' }
export function toolbar<S>(props: Props<ToolbarData, S>, send: Send<Msg>) {
return [div({ class: props.theme }, [each({ items: props.tools, ... })])]
}
// Caller — TypeScript enforces per-field accessors; passing a raw value errors:
toolbar({ tools: (s: State) => s.tools, theme: (s) => s.theme }, send)
export type Props<T, S> = {
[K in keyof T]: (s: S) => T[K]
}
BindingKind
export type BindingKind = 'text' | 'prop' | 'attr' | 'class' | 'style'
ItemAccessor
Per-item accessor. Two access forms:
item.field— shorthand, returns accessor foritem.current[field]item(t => t.expr)— computed expressions In both cases the returned value is a() => Vaccessor. Invoke it (item.field()) to read the current value imperatively.
export type ItemAccessor<T> = {
<R>(selector: (t: T) => R): () => R
} & {
[K in keyof T]-?: () => T[K]
}
ModuleState
Extract the state type from a component module's update function. Works with both property and method syntax.
export type ModuleState<T> = T extends {
update: (state: infer S, msg: infer _M) => [infer _S2, infer _E]
}
? S
: never
ModuleMsg
Extract the message type from a component module's update function.
export type ModuleMsg<T> = T extends {
update: (state: infer _S, msg: infer M) => [infer _S2, infer _E]
}
? M
: never
ModulesState
Given a record of component modules, derive the combined child state. Each key maps to its module's state type.
const children = { dialog, sort: sortable } as const
type CS = ModulesState<typeof children>
// → { dialog: DialogState; sort: SortableState }
export type ModulesState<T extends Record<string, unknown>> = {
[K in keyof T]: ModuleState<T[K]>
}
ModulesMsg
Given a record of component modules, derive the combined child message
union. Each module's messages are wrapped in { type: key; msg: SubMsg }.
const children = { dialog, sort: sortable } as const
type CM = ModulesMsg<typeof children>
// → { type: 'dialog'; msg: DialogMsg } | { type: 'sort'; msg: SortableMsg }
export type ModulesMsg<T extends Record<string, unknown>> = {
[K in keyof T]: { type: K; msg: ModuleMsg<T[K]> }
}[keyof T]
FieldMsg
Type utility for form field messages.
Generates a discriminated union where each field gets its own typed variant,
avoiding the need to define one message type per field.
Usage:
type Fields = { name: string; email: string; age: number }
type Msg = FieldMsg
export type FieldMsg<Fields extends Record<string, unknown>> = {
[K in keyof Fields]: { type: 'setField'; field: K; value: Fields[K] }
}[keyof Fields]
Interfaces
ComponentDef
export interface ComponentDef<S, M, E = never, D = void> {
name: string
init: (data: D) => [S, E[]]
update: (state: S, msg: M) => [S, E[]]
view: (h: View<S, M>) => Node[]
onEffect?: (ctx: { effect: E; send: Send<M>; signal: AbortSignal }) => void
/** @internal Compiler-injected — path-keyed reactivity prefix table.
* Replaces the legacy `__dirty` bitmask. Each entry is a stable
* closure `(s: S) => unknown` hoisted at module scope; the array
* position IS the bit position (0..30 → low word, 31..61 → high). */
__prefixes?: ReadonlyArray<(state: S) => unknown>
/** @internal Compiler-injected */
__renderToString?: (state: S) => string
/** @internal Compiler-injected */
__msgSchema?: object
/** @internal Compiler-injected — maps top-level state field → dirty-mask bit(s) */
__maskLegend?: Record<string, number>
/** @internal Compiler-injected — source-file location of the component() call */
__componentMeta?: { file: string; line: number }
/** @internal Compiler-injected — shape of the State type (for introspection) */
__stateSchema?: object
/** @internal Compiler-injected — Effect union schema (for introspection) */
__effectSchema?: object
/** @internal Compiler-injected — replaces generic Phase 1 + Phase 2 loop.
* Trailing `dirtyHi` carries the high-word mask for 32..61-prefix
* components; defaults to 0 for backward compat with stale 5-param bundles. */
__update?: (
state: S,
dirty: number,
bindings: Binding[],
blocks: StructuralBlock[],
bindingsBeforePhase1: number,
dirtyHi?: number,
) => void
/** @internal Compiler-injected — per-message-type specialized handlers.
* Bypass the entire processMessages pipeline for single-message updates. */
__handlers?: Record<string, (inst: object, msg: unknown) => [S, E[]]>
}
User-authored __dirty is no longer accepted — the compiler emits __prefixes automatically from accessor analysis, and the runtime throws at createComponentInstance if any user code sets def.__dirty. See Migration from v0.0.x for the migration recipe.
LazyDef
Type-erased component definition for use at module boundaries where the
loaded component's S, M, E are internal and invisible to the caller.
Only D (init data) survives because the caller provides it.
ComponentDef<S, M, E, D> is structurally assignable to LazyDef<D>
for any S, M, E — view: (h: unknown) => Node[] accepts any View via
contravariance, and all other fields widen to unknown return types.
Used by lazy() as the loader's return type.
export interface LazyDef<D = void> {
name: string
// Method syntax — TypeScript checks methods bivariantly, so
// ComponentDef<S, M, E, D>'s concrete (state: S, msg: M) => ...
// assigns here even though S/M ≠ unknown. Property syntax would
// be contravariant and reject the assignment.
init(data: D): [unknown, unknown[]]
update(state: unknown, msg: unknown): [unknown, unknown[]]
view(h: unknown): Node[]
onEffect?: unknown
__prefixes?: unknown
__renderToString?: unknown
__msgSchema?: unknown
__maskLegend?: unknown
__componentMeta?: unknown
__stateSchema?: unknown
__effectSchema?: unknown
__update?: unknown
__handlers?: unknown
}
AppHandle
export interface AppHandle {
dispose(): void
flush(): void
}
Scope
export interface Scope {
id: number
parent: Scope | null
children: Scope[]
disposers: Array<() => void>
bindings: Binding[]
/** Per-item updaters — called directly by each() when item changes, bypassing Phase 2 */
itemUpdaters: Array<() => void>
}
Binding
export interface Binding {
mask: number
accessor: (state: unknown) => unknown
lastValue: unknown
kind: BindingKind
node: Node
key?: string
ownerScope: Scope
perItem: boolean
dead: boolean
}
TransitionOptions
export interface TransitionOptions {
enter?: (nodes: Node[]) => void | Promise<void>
leave?: (nodes: Node[]) => void | Promise<void>
onTransition?: (ctx: { entering: Node[]; leaving: Node[]; parent: Node }) => void | Promise<void>
}
BranchOptions
export interface BranchOptions<S, M = unknown> extends TransitionOptions {
on: (s: S) => string | number | boolean
cases: Record<string | number, (h: View<S, M>) => Node[]>
}
ShowOptions
export interface ShowOptions<S, M = unknown> extends TransitionOptions {
when: (s: S) => boolean
render: (h: View<S, M>) => Node[]
fallback?: (h: View<S, M>) => Node[]
}
EachOptions
export interface EachOptions<S, T, M = unknown> extends TransitionOptions {
items: (s: S) => T[]
key: (item: T) => string | number
render: (opts: {
send: Send<M>
item: ItemAccessor<T>
/**
* Plain (non-Proxy) accessor factory. Compiler-output path; avoid in user code
* (use `item.field` / `item(fn)` — more ergonomic and bypasses Proxy when compiled).
*/
acc: <R>(selector: (t: T) => R) => () => R
index: () => number
/** @internal Compiler-injected — entry reference for row factory */
entry?: Record<string, unknown>
}) => Node[]
}
PortalOptions
export interface PortalOptions {
target: HTMLElement | string
render: () => Node[]
}
ForeignOptions
export interface ForeignOptions<S, M, T extends Record<string, unknown>, Instance> {
mount: (ctx: { container: HTMLElement; send: Send<M> }) => Instance
props: (s: S) => T
sync:
| ((ctx: { instance: Instance; props: T; prev: T | undefined }) => void)
| {
[K in keyof T]?: (ctx: { instance: Instance; value: T[K]; prev: T[K] | undefined }) => void
}
destroy: (instance: Instance) => void
container?: { tag?: string; attrs?: Record<string, string> }
}
View
Typed view helpers bound to a component's State / Msg. The sole
argument to view, so every state-bound primitive infers State from
the component definition — no per-call show<State>(...) annotation.
view: ({ send, show, text }) => [
...show({ when: s => s.count > 0, render: () => [...] }),
text(s => String(s.count)),
]
Tip: to view-function over a sub-slice of parent state, import slice
as a standalone helper:
import { slice } from '@llui/dom'
const form = slice(h, (s) => s.form) // returns View<FormState, Msg>
The Vite plugin's mask-injection pass recognizes all three call forms
equivalently: h.text(...) (member expression), text(...) (destructured
alias), and text(...) (bare import from @llui/dom). No per-binding
gating is lost when calling through h.
export interface View<S, M> {
send: Send<M>
show(opts: ShowOptions<S, M>): Node[]
branch(opts: BranchOptions<S, M>): Node[]
each<T>(opts: EachOptions<S, T, M>): Node[]
text(accessor: ((s: S) => string) | string, mask?: number): Text
memo<T>(accessor: (s: S) => T): (s: S) => T
selector<V>(field: (s: S) => V): SelectorInstance<V>
ctx<T>(c: Context<T>): (s: S) => T
}
MountOptions
export interface MountOptions {
devTools?: boolean
}
LluiDebugAPI
export interface LluiDebugAPI {
getState(): unknown
send(msg: unknown): void
flush(): void
getMessageHistory(opts?: { since?: number; limit?: number }): MessageRecord[]
evalUpdate(msg: unknown): { state: unknown; effects: unknown[] }
exportTrace(): {
lluiTrace: 1
component: string
generatedBy: string
timestamp: string
entries: Array<{ msg: unknown; expectedState: unknown; expectedEffects: unknown[] }>
}
clearLog(): void
validateMessage(msg: unknown): ValidationError[] | null
getBindings(): BindingDebugInfo[]
whyDidUpdate(bindingIndex: number): UpdateExplanation
searchState(query: string): unknown
/** Returns the compiled Msg schema (discriminant + variant field types). */
getMessageSchema(): MessageSchemaInfo | null
/** Returns the bit→field map injected by the compiler. Lets tools decode dirty-mask values. */
getMaskLegend(): Record<string, number> | null
/** Given a dirty mask, return the list of top-level fields it represents. */
decodeMask(mask: number): string[]
/** Component name + source location (file/line from compiler-injected metadata). */
getComponentInfo(): ComponentInfo
/** Returns the compiled State type shape (from TypeScript `type State = { … }`). */
getStateSchema(): object | null
/** Returns the compiled Effect schema (from TypeScript `type Effect = { … }` union). */
getEffectSchema(): object | null
/** Deep-clone the current state. Pair with restoreState() to checkpoint before risky operations. */
snapshotState(): unknown
/** Overwrite the current state with a previously-captured snapshot. Triggers a full re-render. */
restoreState(snap: unknown): void
/** Find all bindings whose target node matches or is a child of the selector. */
getBindingsFor(selector: string): BindingLocation[]
}
Context
export interface Context<T> {
readonly _id: symbol
readonly _default: T | undefined
readonly _name: string | undefined
}
VirtualEachOptions
export interface VirtualEachOptions<S, T, M = unknown> {
items: (s: S) => T[]
key: (item: T) => string | number
/** Fixed pixel height per item. Required — dynamic heights are not supported yet. */
itemHeight: number
/** Scrollable container height in pixels. */
containerHeight: number
/** Extra rows to render above/below the viewport for smooth scrolling. Default: 3. */
overscan?: number
/** Optional class for the scroll container. */
class?: string
render: (opts: {
send: Send<M>
item: ItemAccessor<T>
acc: <R>(selector: (t: T) => R) => () => R
index: () => number
}) => Node[]
}
LazyOptions
export interface LazyOptions<S, M, D> {
/** Async loader — typically `() => import('./MyComponent').then(m => m.default)`. */
loader: () => Promise<LazyDef<D>>
/** Nodes to render while loading. */
fallback: (h: View<S, M>) => Node[]
/** Nodes to render if the loader rejects. */
error?: (err: Error, h: View<S, M>) => Node[]
/** Props passed as init data to the loaded component. Evaluated once at resolution. */
data?: (s: S) => D
}
Constants
a
const a
abbr
const abbr
article
const article
aside
const aside
b
const b
blockquote
const blockquote
br
const br
button
const button
canvas
const canvas
code
const code
dd
const dd
details
const details
dialog
const dialog
div
const div
dl
const dl
dt
const dt
em
const em
fieldset
const fieldset
figcaption
const figcaption
figure
const figure
footer
const footer
form
const form
h1
const h1
h2
const h2
h3
const h3
h4
const h4
h5
const h5
h6
const h6
header
const header
hr
const hr
i
const i
iframe
const iframe
img
const img
input
const input
label
const label
legend
const legend
li
const li
main
const main
mark
const mark
nav
const nav
ol
const ol
optgroup
const optgroup
option
const option
output
const output
p
const p
pre
const pre
progress
const progress
section
const section
select
const select
small
const small
span
const span
strong
const strong
sub
const sub
summary
const summary
sup
const sup
table
const table
tbody
const tbody
td
const td
textarea
const textarea
tfoot
const tfoot
th
const th
thead
const thead
time
const time
tr
const tr
ul
const ul
video
const video
svg
const svg
g
const g
defs
const defs
symbol
const symbol
use
const use
circle
const circle
ellipse
const ellipse
line
const line
path
const path
polygon
const polygon
polyline
const polyline
rect
const rect
tspan
const tspan
textPath
const textPath
clipPath
const clipPath
linearGradient
const linearGradient
radialGradient
const radialGradient
stop
const stop
mask
const mask
pattern
const pattern
marker
const marker
filter
const filter
feBlend
const feBlend
feColorMatrix
const feColorMatrix
feComponentTransfer
const feComponentTransfer
feComposite
const feComposite
feConvolveMatrix
const feConvolveMatrix
feDiffuseLighting
const feDiffuseLighting
feDisplacementMap
const feDisplacementMap
feDropShadow
const feDropShadow
feFlood
const feFlood
feGaussianBlur
const feGaussianBlur
feImage
const feImage
feMerge
const feMerge
feMergeNode
const feMergeNode
feMorphology
const feMorphology
feOffset
const feOffset
feSpecularLighting
const feSpecularLighting
feTile
const feTile
feTurbulence
const feTurbulence
fePointLight
const fePointLight
feSpotLight
const feSpotLight
feDistantLight
const feDistantLight
feFuncR
const feFuncR
feFuncG
const feFuncG
feFuncB
const feFuncB
feFuncA
const feFuncA
image
const image
foreignObject
const foreignObject
animate
const animate
animateMotion
const animateMotion
animateTransform
const animateTransform
set
const set
mpath
const mpath
desc
const desc
title
const title
metadata
const metadata
math
const math
mi
const mi
mn
const mn
mo
const mo
ms
const ms
mtext
const mtext
mrow
const mrow
mfrac
const mfrac
msqrt
const msqrt
mroot
const mroot
msup
const msup
msub
const msub
msubsup
const msubsup
munder
const munder
mover
const mover
munderover
const munderover
mmultiscripts
const mmultiscripts
mprescripts
const mprescripts
mnone
const mnone
mtable
const mtable
mtr
const mtr
mtd
const mtd
mspace
const mspace
mpadded
const mpadded
mphantom
const mphantom
menclose
const menclose
merror
const merror
maction
const maction
semantics
const semantics
annotation
const annotation
annotationXml
const annotationXml