bind:

Normally data flows from stores to renderables: when the value of a store updates, the page is updated. Bound data flows the other way: changes to the page will update the store.

Subscribing to the store will run a callback whenever a bound value changes. The callback receives the new value and the previous one. Using subscribe will only notify you when the value changes. To be notified of the initial value as well, use watch instead.

Normal reactivity still happens, so changing the store will still update the page (though some binds are readonly).

const tooSlow = $store(false);
const value = $store("");
value.subscribe(console.log);

$element("input", { type: "text", disabled: tooSlow, bind: { value } })
.mount(document.body);

setTimeout(() => {
    // Updating a bound store will still update the renderable.
    value.value = "Time's up!";
    tooSlow.value = true;
}, 5000);

Input value

The value property of inputs can be bound using bind: { value }. That’s what we used on the first example. Input values get bound as a string.

const value = $store("");
value.subscribe(console.log);

$element("input", { type: "text", bind: { value } })
.mount(document.body);

Input checked

Binding to checkboxes can be done with bind: { checked }, and the value gets bound as a boolean.

const checked = $store(true);
checked.subscribe(console.log);

$element("input", { type: "checkbox", bind: { checked } })
.mount(document.body);

Select value

You can also bind the value of a select element using bind: { value }.

const tubers = [
    { label: "Ceres Fauna", value: "ceres_fauna" },
    { label: "Gawr Gura", value: "gawr_gura" },
    { label: "Mori Calliope", value: "mori_calliope" },
    { label: "Ouro Kronii", value: "ouro_kronii" },
    { label: "Tsukumo Sana", value: "tsukumo_sana" }
];
const value = $store(tubers[0].value);
value.subscribe(console.log);

$element("select", { bind: { value }},
    $each(tubers, ({ label, value }) => $element("option", { value }, label))
)
.mount(document.body);

Dimension binds

The dimensions of an element can be bound using one of the following:

These binds are readonly.

const n = $store(0);
const factorial = n.derive((n) => fact(n));

const clientWidth = $store(1);
clientWidth.subscribe(console.log);

function fact(n: number): number {
    return n === 0 ? 1 : n * fact(n - 1);
}
setInterval(() => ++n.value, 1000);

$element("span", {
    style: { display: "inline-block" },
    bind: { clientWidth }
}, $format`${n}! = ${factorial}`)
.mount(document.body);

this

Binding to this sets the bound store to the DOM node created by the renderable. If the element is not on the DOM, the store gets set to null.

const canvas = $store<HTMLCanvasElement | null>(null);
canvas.subscribe((next) => {
    const ctx = next?.getContext("webgpu");
    console.log("Fancy 3D graphics coming soon...", ctx);
});

$element("canvas", { bind: { this: canvas }})
.mount(document.body);

Bind transformers

A bound store will receive values as they come from the page. You can transform the value by using a bind object instead. Bind objects have a store and two transformers:

For example you could transform an input bind so the value gets turned to uppercase:

const value = $store("");
value.subscribe(console.log);

$element("input", { type: "text", bind: { value: {
    store: value,
    toBind: (next) => next.toUpperCase(),
    fromBind: (next) => next
}}})
.mount(document.body);

ceres has some builtin transformers that can be applied with the $transform wrapper.

integer

Parses the value of a number input and binds it as a number rather than a string.

const value = $store(0);
value.subscribe(console.log);

$element("input", { type: "number", bind: { value: $transform("integer", value) }})
.mount(document.body);

multiselect

Binds the value of a select multiple to an array of selected values.

const tubers = [
    { label: "Ceres Fauna", value: "ceres_fauna" },
    { label: "Gawr Gura", value: "gawr_gura" },
    { label: "Mori Calliope", value: "mori_calliope" },
    { label: "Ouro Kronii", value: "ouro_kronii" },
    { label: "Tsukumo Sana", value: "tsukumo_sana" }
];
const selected = $store([tubers[0].value]);
selected.subscribe(console.log);

$element("select", { multiple: true, bind: { value: $transform("multiselect", selected) }},
    $each(tubers, ({ label, value }) => $element("option", { value }, label))
)
.mount(document.body);

checkGroup

Binds the value of multiple checkboxes to a an array of selected values.

const tubers = [
    { label: "Ceres Fauna", value: "ceres_fauna" },
    { label: "Gawr Gura", value: "gawr_gura" },
    { label: "Mori Calliope", value: "mori_calliope" },
    { label: "Ouro Kronii", value: "ouro_kronii" },
    { label: "Tsukumo Sana", value: "tsukumo_sana" }
];
const selected = $store([tubers[0].value]);
selected.subscribe(console.log)

$element("fieldset", undefined, $each(
    tubers,
    (entry) => $fragment(
        $element("label", { htmlFor: entry.value }, entry.label),
        $element("input", {
            id: entry.value,
            value: entry.value,
            type: "checkbox",
            bind: { checked: $transform("checkGroup", selected)}})
    )
))
.mount(document.body);

radioGroup

Binds the value of a radio group.

const tubers = [
    { label: "Ceres Fauna", value: "ceres_fauna" },
    { label: "Gawr Gura", value: "gawr_gura" },
    { label: "Mori Calliope", value: "mori_calliope" },
    { label: "Ouro Kronii", value: "ouro_kronii" },
    { label: "Tsukumo Sana", value: "tsukumo_sana" }
];
const selected = $store(tubers[0].value);
selected.subscribe(console.log)

$element("fieldset", undefined, $each(
    tubers,
    (entry) => $fragment(
        $element("label", { htmlFor: entry.value }, entry.label),
        $element("input", {
            id: entry.value,
            value: entry.value,
            type: "radio",
            bind: { checked: $transform("radioGroup", selected)}})
    )
))
.mount(document.body);