reactive — DOM bindings
Fine-grained bindings: text, attributes, classes, forms, conditional and keyed subtrees.
Bindings attach behaviour to elements. Each one is a one-liner over
Effect — "re-run this and poke one property" — and updates
exactly the property it owns. The state model behind them is on reactive —
state.
Documentation
BindText, BindHTML, BindAttr, BindClass, BindStyle, BindShow
reactive.BindText(el, func() string { return strconv.Itoa(count.Get()) })
reactive.BindHTML(el, renderedMarkdown.Get) // unescaped — never user input
reactive.BindAttr(el, "disabled", func() string {
if busy.Get() { return "disabled" }
return "" // empty removes the attribute
})
reactive.BindClass(el, "active", isActive.Get)
reactive.BindStyle(el, "color", colour.Get)
reactive.BindShow(el, visible.Get)
BindShow toggles visibility and keeps the subtree's state; BindWhen (below)
builds and tears the subtree down.
BindValue, BindNumber, BindChecked
reactive.BindValue(input, draft) // string
reactive.BindNumber(input, amount) // float64, ignores unparseable input
reactive.BindChecked(box, done) // bool
Two-way: typing sets the signal, and the effect that echoes it back into the
field stops because Set ignores an equal write — they don't feed back on
themselves. Use reactive.On rather than Element.On inside an effect; it
registers the unbind with the enclosing effect, so re-renders don't stack
handlers.
Why two-way bindings don't loop
Typing fires an input event that sets the signal; the echo back writes the same
value into the field. Set compares against the current value and skips an
equal write, so the echo produces no change and no further re-run — the loop
dies after one pass.
BindWhen
reactive.BindWhen(parent, loggedIn.Get, func() wasmwrap.Element {
return renderDashboard()
})
Builds the subtree when the condition becomes true and removes it when it goes
false, so effects inside it are genuinely fresh each time. BindShow only
toggles visibility and keeps state — pick it when the subtree holds nothing
worth discarding.
BindList
reactive.BindList(list, items.Get,
func(t Todo) string { return t.ID },
func(t Todo) wasmwrap.Element {
row := wasmwrap.Create("li")
reactive.BindClass(row, "done", t.Done.Get)
return row.Append(…)
})
An element is rendered once per key and then reused. Adding, removing or reordering moves existing nodes, so input focus, scroll position and CSS transitions survive. Two consequences: changing a field of an existing item does not re-render it — make the parts that change signals — and the parent element should be dedicated to the list, because ordering is done by index against its children.
Why nodes are moved, not rebuilt
Rebuilding a row on every change would destroy whatever the row holds — focus
inside an input, scroll position, a transition mid-flight. BindList
reconciles by key instead: it figures out which rows to move, insert or detach
and touches only those, so everything else is untouched.
Testing your bindings — the jsdom fake DOM, running under node — is covered on
the Testing page.
Example
open := reactive.NewSignal(false)
reactive.BindClass(nav, "open", open.Get)
reactive.BindShow(panel, open.Get)
reactive.On(toggle, "click", func(wasmwrap.Event) {
open.Update(func(v bool) bool { return !v })
})
A signal, two one-property bindings, and a handler — the nav-toggle widget. For a complete widget hydrated onto build-time markup, see the Examples page.