Bindings

The relationship lives in your HTML. applyBindings switches it on.

Markup your server already rendered

applyBindings(viewModel, element) walks existing HTML and activates its binding attributes in place. It never rewrites the page: the server's text stays until the data says otherwise, so there is no flash of empty content and nothing to hydrate.

Contact by

The view model, live

{}

Caret in the name field: false

Thank you. Nothing is actually sent.

markup/basics-bindings.html - the markup, as servedview whole file
<div class="demo-stage demo-split">
    <div class="demo-form">
        <label class="form-label">Name
            <input class="form-input" data-model="person.name.value" data-focus="focused.value">
        </label>

        <label class="form-label">Age
            <input class="form-input" type="number" min="0" max="120" data-model="person.age.value">
        </label>

        <label class="form-label">Home town
            <select class="form-select" data-model="person.town.value" data-options="towns" data-options-caption="'Anywhere'"></select>
        </label>

        <fieldset class="demo-fieldset">
            <legend>Contact by</legend>
            <label><input type="radio" name="contact" value="email" data-model="person.contact.value"> Email</label>
            <label><input type="radio" name="contact" value="phone" data-model="person.contact.value"> Phone</label>
            <label><input type="radio" name="contact" value="post" data-model="person.contact.value"> Post</label>
        </fieldset>

        <label class="demo-switch">
            <input type="checkbox" data-model="person.newsletter.value"> Send me the newsletter
        </label>
    </div>

    <div class="demo-mirror">
        <p class="demo-mirror-title">The view model, live</p>
        <pre class="demo-json" data-bind-text="asJson.value">{}</pre>
        <p class="demo-note">Caret in the name field: <b data-bind-text="focused.value">false</b></p>
        <p class="demo-note" data-if="person.newsletter.value">Thank you. Nothing is actually sent.</p>
    </div>
</div>
apps/basics.js - the view modelview whole file
/**
 * Two-way binding, with no change handler anywhere.
 *
 * Text, number, radio group, checkbox and a `<select>` populated by
 * `data-options` all write straight back into the view model; `data-focus`
 * reports where the caret is. The JSON panel is a `computed` reading the same
 * observables the inputs write to, so it cannot disagree with them.
 */
function bindings() {
    const person = {
        name: observable('Ada Lovelace'),
        age: observable(36),
        town: observable('Harrogate'),
        contact: observable('email'),
        newsletter: observable(false)
    };

    const focused = observable(false);

    return {
        person,
        focused,
        towns: ['Harrogate', 'Knaresborough', 'Ripon', 'Wetherby', 'Otley', 'Ilkley'],

        asJson: computed(() => JSON.stringify({
            name: person.name.value,
            age: person.age.value,
            town: person.town.value || null,
            contact: person.contact.value,
            newsletter: person.newsletter.value
        }, null, 2))
    };
}

The twelve kinds

Attribute or tag Does
data-bind-text, data-bind-<attr> Text, any attribute or DOM property (disabled, hidden, checked...)
data-bind-class Adds and removes only the classes it set, so static classes survive
data-bind-style-<prop>, data-bind-style One CSS property, or an object of them
data-model Two-way: text, number, checkbox, radio, select and select multiple
data-on-<event> Any DOM event; returning false prevents the default
data-if The element is in the document or it is not - the same node comes back
data-each="rows key=id" A keyed list - see lists
data-options Fill a <select> from data
data-focus Two-way between a value and which element has focus
data-component Render a registered component - see components
{{ }}, {{#if}}, {{#each}} Mustache, inside templates and list bodies
<!-- dm if: x -->, <!-- dm each -->, <!-- dm text --> Virtual bindings, for markup with no element to spare

There is no data-bind-html, on purpose: assigning innerHTML from data is the shortest route to a cross-site scripting hole. {{{triple-stache}}} in a template is the explicit, greppable opt-out.

Virtual bindings

A table row or a run of list items has no element to spare for an attribute. A comment does:

<ul>
    <li>Always shown</li>
    <!-- dm if: showExtras.value -->
        <li>Only when showExtras</li>
    <!-- /dm -->
</ul>

The tutorial's contacts page uses <!-- dm text --> and <!-- dm if --> for its summary line and empty state.

Next: keyed lists All features