Flight Manual
Complete reference for every API, directive, and pattern in rdbl. Start at the top — the whole library builds on three primitives.
Signals
The entire reactive model. A signal holds a value and notifies observers when it changes.
import { signal } from 'rdbl'
const count = signal(0)
count() // → 0 read (tracks dependencies)
count.set(5) // write
count.peek() // → 5 read WITHOUT tracking
.peek()?
Use it when you need the current value inside a computed or effect but don't want that signal to be a dependency. Common when writing a value back to itself.
Computed
Derived state. Lazy — only re-evaluates when its dependencies change. Cached between reads.
import { signal, computed } from 'rdbl'
const first = signal('Neil')
const last = signal('Armstrong')
const full = computed(() => `${first()} ${last()}`)
full() // → 'Neil Armstrong'
first.set('Buzz')
full() // → 'Buzz Armstrong' (re-evaluated)
Effect
Runs immediately on creation, then re-runs when any dependency changes. Returns an optional cleanup function.
import { signal, effect } from 'rdbl'
const status = signal('STANDBY')
effect(() => {
console.log('Status is now:', status())
return () => console.log('cleanup') // optional teardown
})
Batch
Defer effect execution until all mutations are complete. Multiple signal updates coalesce into one re-render.
import { signal, batch } from 'rdbl'
const x = signal(1), y = signal(2)
batch(() => {
x.set(10)
y.set(20)
// effects fire once, after this block
})
Directives
Plain HTML attributes — no special syntax, no magic prefixes. A directive value is a dot-separated path into your scope.
text="path"
Sets textContent. Safe — no HTML injection.
If the element already has text content when bind() is called, rdbl preserves it on the first render. Once the signal changes, the DOM updates normally. This means server-rendered text requires no special attributes — it just works.
<!-- Server-rendered: "Dev Hub" is preserved on bind, then updates reactively -->
<span text="name">Dev Hub</span>
<!-- Client-only: starts empty, signal sets the value immediately -->
<span text="user.name"></span>
html="path"
Sets innerHTML. Use for trusted rich content. Same auto-preservation behavior as text — existing content is kept on first bind.
<div html="article.body"></div>
show="path"
Toggles the hidden attribute. On <dialog> elements, calls showModal() / close() automatically.
<div show="isVisible">Revealed when true</div>
<dialog show="modalOpen">A native modal</dialog>
cls="path"
String form replaces className. Object form toggles individual classes by truthy value.
<div cls="statusClass"></div>
// String form
const statusClass = computed(() => isActive() ? 'active' : 'inactive')
// Object form — toggles individual classes
const statusClass = computed(() => ({
active: isActive(),
disabled: isDisabled(),
loading: isFetching(),
}))
attr="name:path; name2:path2"
Sets arbitrary element attributes. Removes the attribute when the value is false or null.
<button attr="aria-label:label; disabled:isDisabled; data-id:item.id">
Click me
</button>
model="path"
Two-way binding for form elements. The path must resolve to a signal — model writes back to it on change.
<input model="query">
<input type="checkbox" model="isChecked">
<select model="selectedOption"></select>
<textarea model="notes"></textarea>
You typed: "" — length:
each="path" key="idPath"
Keyed list rendering. Requires a <template> child with the item markup. Efficiently diffs and reorders DOM nodes using the key.
Inside list items, the scope includes all item properties, plus $item (the full item object) and $index (zero-based position).
<ul each="todos" key="id">
<template>
<li>
<input type="checkbox" model="done">
<span text="text"></span>
<button onclick="remove">✗</button>
</li>
</template>
</ul>
SSR-rendered lists — data-key
For server-rendered lists, add data-key on each row with its item key value. rdbl adopts those DOM nodes into the live map on bind — no replacement on first render. The <template> is used only when new items are added after bind.
<ul each="products" key="id">
<li data-key="1" text="name">Widget A</li>
<li data-key="2" text="name">Widget B</li>
<template><li text="name"></li></template>
</ul>
on<event>="path"
Event binding. The path resolves to a function in your scope. The handler receives (event, element, context). Return false to call both preventDefault() and stopPropagation().
<button onclick="handleClick">Launch</button>
<form onsubmit="handleSubmit"></form>
<input oninput="liveSearch">
const state = {
handleClick(event, el, ctx) {
// event → the DOM event
// el → the element
// ctx → Context.read(el)
},
handleSubmit(event) {
return false // preventDefault + stopPropagation
}
}
bind()
const app = bind(rootElement, scope, options)
Walks the DOM from rootElement, wiring up all directive attributes it finds to the given scope. Returns an app instance.
Options
| Option | Default | Description |
|---|---|---|
dev | true | Log warnings for missing paths, bad values, missing keys |
autoBind | false | Use MutationObserver to auto-bind dynamically added DOM |
ignoreSelector | [data-no-bind] | CSS selector to opt subtrees out of binding |
app.bindSubtree(element, scope)
Bind a sub-element with its own scope. The sub-scope falls back to the parent scope for unresolved paths.
const app = bind(root, parentScope)
app.bindSubtree(cardElement, cardScope)
app.dispose()
Tear down all effects and event listeners created by this binding. Essential for cleanup when removing DOM.
app.dispose()
getItemContext()
Inside each list event handlers, use this to access the item data for the row that was interacted with. Walk up any depth — rdbl attaches context to the nearest list item ancestor.
import { signal, getItemContext } from 'rdbl'
const todos = signal([{ id: 1, text: 'Launch rocket', done: false }])
const state = {
todos,
toggleTodo(event, el) {
const { item, index } = getItemContext(el)
todos.set(
todos().map(t => t.id === item.id ? { ...t, done: !t.done } : t)
)
}
}
createScope()
Create a child scope that falls back to a parent for unresolved properties. Useful for composing nested views.
import { createScope } from 'rdbl'
const child = createScope(parentScope, {
localTitle: signal('Local Override'),
// everything else falls through to parentScope
})
bind(element, child)
Context
DOM-scoped context — looked up by walking the element tree. Avoids prop drilling for shared services like routers, loggers, or stores.
import { Context } from 'rdbl'
// Provide at any ancestor element
Context.provide(document.querySelector('#app'), {
router: { go: path => history.pushState({}, '', path) },
log: console.log,
})
// Read from any descendant (walks up to find nearest provider)
const ctx = Context.read(someChildElement)
// Available as 3rd argument in all event handlers
function handleNav(event, el, ctx) {
ctx.router.go('/dashboard')
}
Islands
Islands are independently bound components. Each element with an [island] attribute is its own binding root —
rdbl stops traversal at nested island boundaries so parent and child islands never interfere.
The island attribute value is a module path. The module's default export is a factory function.
Island module
// /components/SyncStatus.js
import { computed, Context } from 'rdbl'
export default function syncStatus(root, window) {
const { pageState } = Context.read(document.body)
return {
statusText: computed(() => pageState.connected() ? 'Synced' : 'Connecting...'),
indicatorCls: computed(() => pageState.connected() ? 'dot synced' : 'dot pending'),
}
}
Island HTML
<section island="/components/SyncStatus.js">
<span cls="indicatorCls"></span>
<span text="statusText"></span>
</section>
init() — binding all islands at startup
import { bind, Context } from 'rdbl'
async function init(window) {
const roots = [...document.querySelectorAll('[island]')]
const instances = {}
let i = 0
for await (const root of roots) {
const key = root.getAttribute('island')
try {
const scopeFactory = (await import(key)).default
const scope = scopeFactory(root, window)
instances[`${key}:${i++}`] = bind(root, scope, { dev: true })
} catch (err) {
console.error(`Failed to load island "${key}":`, err)
}
}
return instances
}
Context.provide(document.body, { pageState })
const app = await init(window)
Server-Side Rendering
rdbl is designed for server-rendered HTML. Plain server-rendered content requires no special attributes — the library detects it automatically.
Text and HTML — auto-preserved
If a text or html element has content when bind() is called, rdbl skips the first render and leaves the server-rendered content in place. Once the signal changes, reactive updates apply normally.
<!-- No special attributes — content preserved automatically on bind -->
<div id="app" island="/components/Dashboard.js">
<h1 text="greeting">Hello, Joey</h1>
<p text="summary">3 of 10 tasks complete</p>
</div>
// Initialize signals to match server-rendered values
export default function dashboard(root, window) {
const greeting = signal('Hello, Joey')
const summary = signal('3 of 10 tasks complete')
return { greeting, summary }
}
Lists — data-key
For server-rendered lists, add data-key on each row matching its item key value. rdbl adopts those DOM nodes into its live map on bind — the existing rows stay in place and become reactive. The <template> is used only when new items are added after bind.
<ul each="products" key="id">
<li data-key="1" text="name">Widget A</li>
<li data-key="2" text="name">Widget B</li>
<template><li text="name"></li></template>
</ul>
Passing data from server to client
Embedded JSON — embed state as JSON, read it before binding:
<!-- Server renders -->
<script type="application/json" id="page-data">
{ "user": { "name": "Joey", "plan": "pro" } }
</script>
<div id="app" island="/components/Dashboard.js">
<h1 text="greeting">Hello, Joey</h1>
</div>
import { signal, computed } from 'rdbl'
export default function dashboard(root, window) {
const raw = JSON.parse(document.getElementById('page-data').textContent)
const user = signal(raw.user)
const greeting = computed(() => `Hello, ${user().name}`)
return { user, greeting }
}
Data attributes — for smaller per-component values:
<section island="/components/UserBadge.js"
data-name="Joey"
data-plan="pro">
<span text="name">Joey</span>
<span cls="badgeCls" text="plan">pro</span>
</section>
import { signal, computed } from 'rdbl'
export default function userBadge(root, window) {
const name = signal(root.dataset.name)
const plan = signal(root.dataset.plan)
const badgeCls = computed(() => `badge badge-${plan()}`)
return { name, plan, badgeCls }
}
data-ssr
For show, attr, cls, and model directives there is no content to inspect, so auto-detection doesn't apply. Use data-ssr on those elements to skip the first render when you can't initialize the signal to match the server-rendered state.
<div show="panelOpen" data-ssr>...</div>
<a attr="href:url" data-ssr>Read more</a>
Hydrating from localStorage
const theme = signal('light')
const tasks = signal([])
// Hydrate before bind()
const stored = localStorage.getItem('app-state')
if (stored) {
const snap = JSON.parse(stored)
theme.set(snap.theme ?? 'light')
tasks.set(snap.tasks ?? [])
}
// Persist on every change
effect(() => {
localStorage.setItem('app-state', JSON.stringify({
theme: theme(),
tasks: tasks(),
}))
})
bind(document.querySelector('#app'), { theme, tasks })
bind() wires the already-correct state to the DOM — plain server-rendered HTML, no special attributes required.