Skip to content

data-bind

Two-way binding between an element’s value and a signal. Typing writes the signal; setting the signal updates the element. Each kind of element has its own rules for what “value” means.

data-bind:email
data-bind="email"
data-bind:user.name
data-bind:price__event.change
part
key or value: the signal path (camelCased, __case changes it)
value or key
modifier effect
__event.name[.name] the events that write the signal; default input for text-like fields, change for selects and unknown elements
__case.kebab keep the key kebab-cased

Text inputs bind strings. type=number and type=range bind numbers, and an empty number field binds ''.

input types

  
<div data-signals="{ name: 'Ada', age: 36, vol: 40 }">
<div class="row"><input data-bind:name placeholder="name"> <input type="number" data-bind:age style="width:5rem"> <input type="range" min="0" max="100" data-bind:vol></div>
<pre data-text="JSON.stringify({ name: $name, age: $age, vol: $vol })"></pre>
<div class="row"><button data-on:click="$name = 'Grace'; $age = 45; $vol = 80">set from the outside</button></div>
</div>

If the signal does not exist yet, the element’s current value seeds it, so server-rendered forms need no data-signals declaration.

seeded from markup

city = , floors = (a number)

<div>
<div class="row"><input data-bind:city value="Lisbon"> <input type="number" data-bind:floors value="3"></div>
<p>city = <b data-text="$city"></b>, floors = <b data-text="$floors"></b> (a number)</p>
</div>

A lone checkbox binds a boolean. Checkboxes that share a signal holding an array bind their value into that array. A checkbox with its own value and a non-array signal binds that value or ''.

boolean, group, own value

<div data-signals="{ agree: false, toppings: ['cheese'], plan: '' }">
<div class="row"><label><input type="checkbox" data-bind:agree> agree</label></div>
<div class="row"><label><input type="checkbox" value="cheese" data-bind:toppings> cheese</label><label><input type="checkbox" value="olives" data-bind:toppings> olives</label><label><input type="checkbox" value="basil" data-bind:toppings> basil</label></div>
<div class="row"><label><input type="checkbox" value="pro" data-bind:plan> pro plan (own value)</label></div>
<pre data-text="JSON.stringify({ agree: $agree, toppings: $toppings, plan: $plan })"></pre>
</div>

Radios bound to the same signal form a group; the checked one’s value is the signal. They get a shared name automatically. If the signal already holds a number, values are converted back to numbers.

radio group
numbers stay numbers

size = , stars = ()

<div data-signals="{ size: 'm', stars: 3 }">
<div class="row"><label><input type="radio" value="s" data-bind:size> S</label><label><input type="radio" value="m" data-bind:size> M</label><label><input type="radio" value="l" data-bind:size> L</label></div>
<div class="row"><label><input type="radio" value="1" data-bind:stars> 1</label><label><input type="radio" value="3" data-bind:stars> 3</label><label><input type="radio" value="5" data-bind:stars> 5</label> <span class="muted">numbers stay numbers</span></div>
<p>size = <b data-text="$size"></b>, stars = <b data-text="$stars"></b> (<span data-text="typeof $stars"></span>)</p>
</div>

A single select binds the chosen option’s value; multiple binds an array. Options with numeric values keep the signal’s type.

single and multiple

  
<div data-signals="{ colour: 'green', langs: ['js'] }">
<div class="row"><select data-bind:colour><option>red</option><option>green</option><option>blue</option></select>
<select multiple data-bind:langs size="3"><option value="js">JavaScript</option><option value="ts">TypeScript</option><option value="go">Go</option></select></div>
<pre data-text="JSON.stringify({ colour: $colour, langs: $langs })"></pre>
<div class="row"><button data-on:click="$langs = ['ts', 'go']">select ts and go</button></div>
</div>

Textareas bind like text inputs. Any other element with a value property (custom elements included) binds it on input and change; elements without one bind their value attribute on change.

textarea

characters

<div data-signals="{ note: 'Two-way.' }">
<textarea data-bind:note rows="2" style="width:100%"></textarea>
<p><b data-text="$note.length"></b> characters</p>
</div>

Bind on change (or blur) to update only when the user commits a value rather than on every keystroke.

__event.change

draft = , committed =

<div data-signals="{ draft: '', committed: '' }">
<div class="row"><input data-bind:draft placeholder="updates as you type"> <input data-bind:committed__event.change placeholder="updates when you leave the field"></div>
<p>draft = <b data-text="$draft"></b>, committed = <b data-text="$committed"></b></p>
</div>

type=file binds an array of { name, type, size, contents } where contents is the file’s base64 body, ready to send as JSON.

files as base64

<div data-signals="{ files: [] }">
<input type="file" multiple data-bind:files>
<pre data-text="JSON.stringify($files.map(f => ({ name: f.name, size: f.size, base64Length: f.contents.length })), null, 2)"></pre>
</div>

The key may be a dotted path into a namespace. When the path is awkward as an attribute key, put it in the value instead.

nested paths

<div data-signals="{ user: { first: 'Ada', last: 'Lovelace' } }">
<div class="row"><input data-bind:user.first> <input data-bind="user.last"></div>
<p data-text="$user.first + ' ' + $user.last"></p>
</div>

When a server patch morphs the element and changes its default value, checked or selected attribute, the morph updates the live field and dispatches sigmx-prop-change; bind listens for it and writes the new value to the signal, so server and browser never disagree. See Forms and binding for the full rules and for posting forms.

import { bind } from 'sigmx/plugins'

Registering bind adds 714 bytes brotli (793 gzip, 1806 minified) on top of the core. See bundle sizes.