Skip to content

Bindings and events ​

The data provider ​

A template talks to one object, the data provider, through two methods:

ts
interface DataProvider {
    lookupRef(path: string): any;      // answer a name used in an expression
    callMethod(name: string): boolean; // run an event handler; true when handled
}

An on: handler with arguments (below) calls callMethod(name, args) with a second, array argument, so a controller that takes arguments declares callMethod(name: string, args?: any[]): boolean. The one-argument form still works for handlers without arguments.

Pass it as the second argument of instantiate. Your screen's controller is the usual choice: give it both methods and pass this. An object that lacks either method is treated as no provider, so bindings show nothing and events do nothing.

bind: ​

bind:attr="expression" evaluates the expression, applies the result through the attribute's setter, and does it again whenever a ref the expression read changes. Updates land at the next frame boundary.

xml
<Panel layout="vbox" spacing="8">
    <Label bind:text="playerName" />
    <ProgressBar width="200" range="0,100" bind:value="health" />
    <Button text="Attack" bind:enabled="health > 0" />
    <Label bind:color="danger ? '#ff5555' : '#d4d4d4'" text="Status" />
</Panel>

Any settable attribute in the tag reference can be bound, including the common ones (enabled, visible, width, variant, padding and so on). A static-only attribute (one the widget reads when it is built, such as Select options or List itemHeight) ignores bind:.

For the controller this means: return a Ref (or computed) from lookupRef for anything that changes, and set the ref. A plain value works too, but the binding then never updates.

ts
import { Component, Controller, DEFAULT_STAGE, Ref, Screen, TemplateRegistry, computed } from "willowui";

const TEMPLATE: string = `
<Panel layout="vbox" spacing="8" padding="16">
    <Label bind:text="summary" />
    <ProgressBar width="200" range="0,100" bind:value="health" />
    <Button text="Hit" on:click="hit" bind:enabled="health > 0" />
</Panel>`;

class Hud implements Controller {
    health: Ref<float> = new Ref<float>(100.0);
    summary: Ref<any> = new Ref<any>("");

    onCreate(screen: Screen): void {
        this.summary = computed((): any => "HP: " + this.health.value);
        const reg: TemplateRegistry = new TemplateRegistry();
        reg.registerXML("hud", TEMPLATE);
        const root: Component = reg.instantiate("hud", this, screen);
        screen.add(root);
    }
    onUpdate(dt: float): void {}
    onDispose(): void {}

    lookupRef(path: string): any {
        if (path == "health") { return this.health; }
        if (path == "summary") { return this.summary; }
        return null;
    }
    callMethod(name: string): boolean {
        if (name == "hit") {
            this.health.set(Math.max(0.0, this.health.peek() - 15.0));
            return true;
        }
        return false;
    }
}

DEFAULT_STAGE.setScene(engine.root2d);
DEFAULT_STAGE.add(new Screen().withController(new Hud()));
App.onFrame((dt: float): void => { DEFAULT_STAGE.update(dt); });

Two-way bindings ​

bind:value, bind:checked, bind:selected and bind:rawValue write back. When the expression is just the name of a ref (a name lookupRef answers with a Ref, or a ui:for variable holding one), the template calls the widget's own bindValue, bindSelected or bindRawValue with that ref: the widget shows the ref's value, follows its changes, and writes the user's edits into it. bind:checked uses bindValue.

xml
<Panel layout="vbox" spacing="8">
    <TextInput bind:value="name" width="220" />
    <Select options="Warrior,Mage,Rogue" bind:selected="classIndex" />
    <Checkbox text="Hardcore" bind:checked="hardcore" />
</Panel>

Any other expression (count + 1, name ? name : 'Hero', a plain value) has no ref to write into, so it binds one way through the attribute's setter. The forms recipe shows a two-way form end to end.

on: ​

on:event="method" calls callMethod("method") when the widget fires that event. The widget's new value is not passed along, so read it from your bound ref or from the widget.

on:event="method(arg, ...)" passes arguments. Each argument is an expression, evaluated when the event fires, in the same scope as bind:. Inside a ui:for row that includes the row's item and index, so each copy can say which row it is:

xml
<Panel layout="vbox" spacing="4">
    <Button ui:for="item in inventory" bind:text="item.name" on:click="select(item, index)" />
    <Button text="Save" on:click="save" />
</Panel>

Clicking the second row calls callMethod("select", [inventory[1], 1]). The bare on:click="save" still calls callMethod("save"), and save() with empty parentheses passes an empty array.

EventFired by
clickButton and IconButton when clicked. On any other tag, the component's activate callback, which a click on the component or any child bubbles up to.
changeToggle, Checkbox, TextInput, MaskedInput, SearchBox, InputField, Slider, NumberStepper, TabBar, ToggleButtonBar, Select, OptionRotator, Radio, TimePicker, List, TileList, SortableList, SortableTreeList, ColorPicker, StatWeb, GradientEditor
submitTextInput, MaskedInput, SearchBox, InputField (Enter)
blurTextInput, MaskedInput, SearchBox, InputField (focus left)
clearSearchBox (the clear button)
selectList, TileList, SortableList, SortableTreeList
reorderSortableList, SortableTreeList
closeWindow, NavDrawer, Popover
openPopover
commitColorPicker
removeTag (its remove button)
toggleAccordion (a section opened or closed), Tag (selected state)
linkClickRichText (a <link> was clicked)
cropImageCropper
captureKeybindInput (a new binding was captured)

Any other event name, or a known name on a tag that does not fire it, is registered with Component.setOnEvent(name, handler). A custom widget fires it with this.fireEvent(name). See Custom widgets.

Without a provider (instantiateStatic, or a controller missing callMethod), on: attributes do nothing.

Lifetime ​

Each binding is an effect owned by its component, and also tracked by the screen you pass to instantiate. Disposing the component or the screen stops it. You never stop bindings by hand.