Skip to content

Reactive state ​

Without reactive state, every time the score changes you have to remember each label that shows it and call setText on it. With it, you keep the score in a Ref and bind the label to the ref once. When the ref changes, every widget bound to it updates.

The primitives come from ThicketScript's reactivity library. willowui re-exports them from its package root, next to a few helpers, so one import covers both.

Refs ​

A Ref<T> holds one value.

MemberDoes
new Ref<T>(initial)Makes a ref.
ref.valueReads the value (and records it as a dependency inside computed or watchEffect). Assigning it writes.
ref.set(v)Same as assigning value.
ref.peek()Reads without recording a dependency.
ref.on(fn)Calls fn(newValue, oldValue) after each change.
ref.silent(v)Writes without notifying anyone.
ref.reset()Goes back to the value it was made with.

Writes are immediate, notifications are batched. After hp.value = 50, reading hp.value gives 50 at once. Listeners, effects and bound widgets run once, at the next frame boundary. Several writes in one frame reach each listener as one change, from the first old value to the last new one. Writing the value a ref already holds notifies nobody.

ts
import { Ref, computed, watchEffect } from "reactivity";

class Health {
    static async main(): Promise<void> {
        const hp: Ref<float> = new Ref<float>(75.0);
        const maxHp: Ref<float> = new Ref<float>(100.0);
        const percent: Ref<any> = computed((): any => hp.value / maxHp.value * 100.0);

        const stop: Function = watchEffect((): void => {
            console.log("HP " + hp.value + " (" + percent.value + "%)");
        });
        hp.on((now: any, before: any): void => {
            console.log("changed " + before + " -> " + now);
        });

        hp.value = 60.0;
        hp.value = 50.0;
        console.log("read back at once: " + hp.value);

        await new Promise<void>((done: Function): void => { setTimeout((): void => { done(); }, 50); });
        stop();
    }
}

Output:

text
HP 75 (75%)
read back at once: 50
HP 50 (50%)
changed 75 -> 50

The effect ran once when it was made and once more after the frame, not once per write. The listener saw one change, 75 to 50.

Derived values: computed ​

computed(fn) makes a read-only ref whose value is fn(). It tracks every ref fn reads and works its value out again only when one of them has changed and someone reads it. Bind a widget to a computed exactly as you would to a ref.

Effects: watchEffect ​

watchEffect(fn) runs fn now, records every ref it reads, and runs it again after any of them changes. It returns a stop function. Call it when you no longer need the effect, or the effect keeps running and keeps whatever it touches alive.

watchValue(ref, fn) from willowui watches one ref and calls fn(oldValue, newValue): once at the start with the current value twice, then after each change. It returns a stop function too.

Binding widgets ​

Most widgets have bind* methods that take a ref. The widget shows the ref's value now and follows it from then on.

MethodWidgets
bindText(Ref<string>)Label, Button
bindValue(ref)TextInput, TextArea, MaskedInput, SearchBox, InputField (string); Slider, NumberStepper, MeterBar (float); Checkbox, Toggle (boolean); OptionRotator, ColorPicker, GradientEditor, KeybindInput
bindSelected(Ref<int>)List, TileList, SortableList, SortableTreeList, Select, Radio, TabBar, ToggleButtonBar, OptionRotator, ToolGroup; TreeList takes a Ref<TreeNode | null>
bindItems(ReactiveArray)List, TileList, DataTable
bindOptions(ReactiveArray)Select, OptionRotator
bindEnabled(Ref<boolean>), bindVisible(Ref<boolean>)Every component

On an input widget, bindValue and bindSelected go both ways: what the user types, drags or picks is written into the ref, and code that sets the ref updates the widget. bindText goes one way, from the ref to the widget.

This settings panel has no setText and no change handlers. The readout follows the slider, and the Reset button is enabled only when something differs from the defaults:

ts
import { BoundString, Button, Checkbox, Component, DEFAULT_STAGE, Label, Ref, Screen, Slider, bindFormatterf, computed, newVBox } from "willowui";

const volume: Ref<float> = new Ref<float>(0.8);
const muted: Ref<boolean> = new Ref<boolean>(false);

const screen: Screen = new Screen();
const column: Component = newVBox("settings");
column.spacing = 12.0;
column.setPosition(24.0, 24.0);
screen.add(column);

const slider: Slider = new Slider("volume");
slider.setRange(0.0, 1.0);
slider.setSize(240.0, 24.0);
slider.bindValue(volume);
column.addChild(slider);

const readout: Label = new Label("readout", "", "default", 16.0);
const text: BoundString = bindFormatterf(volume, "Volume: %.2f");
readout.bindText(text.ref);
readout.own(text.stop);
column.addChild(readout);

const mute: Checkbox = new Checkbox("mute", "Mute", "default", 16.0);
mute.bindValue(muted);
column.addChild(mute);

const reset: Button = new Button("reset", "Reset", "default", 16.0);
reset.setSize(120.0, 32.0);
reset.bindEnabled(computed((): any => volume.value != 0.8 || muted.value));
reset.setOnClick((): void => {
    volume.value = 0.8;
    muted.value = false;
});
column.addChild(reset);

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

Helpers ​

These return small functions, handy as click handlers:

HelperReturns or does
increment(ref, n)A function that adds n to a number ref: button.setOnClick(increment(clicks, 1)).
toggle(ref)A function that flips a boolean ref.
setTo(ref, v)A function that sets the ref to v.
update(ref, fn)Sets the ref to fn(current) now.
bindFormatterf(ref, format)A BoundString: .ref is a string ref that follows ref, formatted with %d, %s, %v, %f, %.Nf or %%; .stop stops it following.
bindFormatter(ref)The same with %v.

Lists and records ​

ReactiveArray<T> is an array that notifies on change: push, insert, removeAt, move, swap, sort, set(items) and the rest. Bind it to a List, TileList or DataTable with bindItems, and the widget adds, removes and moves rows as the array changes. Wrap many edits in array.batch(fn) to send one notification instead of one per edit. onAdded, onRemoved, onMoved and onItemChanged let your own code react to one kind of change.

ReactiveRecord<V> is a map of string keys that notifies on change, for form data and settings: set(key, v), get(key), onFieldChange(key, fn), and ref(key) for a ref to one field that you can bind to a widget.

Lifetime ​

A widget's own bindings stop when the widget is disposed. Anything else you start, a watchEffect, a watchValue or a bindFormatterf, keeps running until you stop it. Give the stop function to the component it belongs to with component.own(stop), as the readout does above, and it stops when that component goes. A ref you never stop watching keeps its listeners, and the widgets they touch, alive.

XML templates use the same refs: a controller's lookupRef returns them, and bind: attributes bind to them. See Bindings and events.

Next ​