Skip to content

Forms and two-way binding

Two-way binding keeps an input and a client machine in sync. It’s a client component feature — bind to client state, never server state (you don’t want the network in the typing loop).

<input bind:value={draft.query} />
<input type="checkbox" bind:checked={draft.agree} />

bind:value is two halves: a state→DOM write (keeps the input current) and a DOM→state update via the engine’s built-in @set event (assigns one context key on input). You get both directions from one directive.

The writeback is suppressed when the value is unchanged, so the cursor isn’t reset mid-edit, and no update fires while an IME composition is in progress (isComposing). Composed input (CJK, accents) works correctly.

bind:checked reads .checked; an <input type="number"> coerces to a number. You get the native typed value, not a string.

A validation selector is a plain function of context, so the same selector runs server- and client-side. Bind its result back into the form:

select: { error: (s) => s.email.includes('@') ? null : 'Invalid email' }
<span bind:text={form.error}></span>

For commit-on-blur, debounce, or transform-before-store, drop bind:value and wire the two halves yourself:

<input value={read(draft, d => d.query)}
on:change={(e) => draft.send({ type: '@set', key: 'query', value: e.target.value })} />