Skip to content

Directives ​

Directives are ui: attributes. They change the structure of what gets built rather than a single property.

DirectiveStatusWhat it does
ui:show="expr"WorksShows the element while the expression is truthy, hides it otherwise. Reactive.
ui:if="expr"Works, as ui:showToday it behaves exactly like ui:show: the element is always built, and only its visibility follows the expression.
ui:for="item in list"WorksBuilds one copy of the element for each item, and rebuilds the copies when the list changes.
ui:else-if="expr", ui:elseCompile errorui:else is not supported yet; use ui:show with the negated condition (and the same for ui:else-if). Use ui:show with the opposite condition.
ui:key="expr"Compile errorui:key is not supported yet; ui:for rebuilds rows on change.
ui:ref, anything elseCompile errorunknown directive ui:... (and ui:ref is rejected outright).

ui:show ​

xml
<Panel layout="vbox" spacing="8">
    <Button text="Show details" on:click="toggleDetails" />
    <Panel ui:show="detailsOpen" layout="vbox" padding="8">
        <Label text="Here are the details." />
    </Panel>
    <Label text="Details are hidden." ui:show="!detailsOpen" />
</Panel>

A hidden element keeps its place in the tree and its state (a text field keeps what was typed). Whether a hidden child still takes space depends on the parent's layout, so test the look you want.

ui:for ​

ui:for="name in expression" repeats the element. Inside it, name is the current item, name.field reads a field of it (a Map key, a ReactiveRecord field, or an object field), and index is the row's position, counting from 0.

xml
<Panel layout="vbox" spacing="4">
    <Label text="Party" fontSize="18" />
    <Label ui:for="hero in party" bind:text="hero.name + ' (level ' + hero.level + ')'" />
</Panel>

The expression may give a plain array or a ReactiveArray. With a ReactiveArray (or a Ref holding an array), adding, removing or replacing items rebuilds the copies. The rebuild disposes every old copy and builds new ones, so widget state inside the copies (a half-typed field, a scroll position) does not survive a change to the list.

Copies are inserted where the element sits, so siblings before and after it keep their order, and several ui:for elements can share one parent.

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

const TEMPLATE: string = `
<Panel layout="vbox" spacing="4" padding="16">
    <Button ui:for="task in tasks" bind:text="'Done: ' + task" on:click="finish(index)" />
    <Button text="Add task" on:click="addTask" />
</Panel>`;

class Tasks implements Controller {
    tasks: ReactiveArray<string> = new ReactiveArray<string>(["Write docs", "Check samples"]);

    onCreate(screen: Screen): void {
        const reg: TemplateRegistry = new TemplateRegistry();
        reg.registerXML("tasks", TEMPLATE);
        const root: Component = reg.instantiate("tasks", this, screen);
        screen.add(root);
    }
    onUpdate(dt: float): void {}
    onDispose(): void {}

    lookupRef(path: string): any {
        if (path == "tasks") { return this.tasks; }
        return null;
    }
    callMethod(name: string, args?: any[]): boolean {
        if (name == "addTask") {
            this.tasks.push("Task " + (this.tasks.length + 1));
            return true;
        }
        if (name == "finish" && args != null) {
            this.tasks.removeAt((args as any[])[0] as int);
            return true;
        }
        return false;
    }
}

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

For long lists (hundreds of rows), prefer a List, TreeList or DataTable, which recycle row views, over ui:for, which builds a full widget per item.

Combining ​

An element may carry several directives and bindings at once:

xml
<Panel layout="vbox">
    <Button ui:for="slot in slots" bind:text="slot.label" bind:enabled="slot.unlocked" on:click="useSlot(slot)" />
</Panel>

Each copy passes its own slot to useSlot, so the controller knows which one was clicked. See handlers with arguments.