Directives
Directives are ui: attributes. They change the structure of what gets built rather than a single property.
| Directive | Status | What it does |
|---|---|---|
ui:show="expr" | Works | Shows the element while the expression is truthy, hides it otherwise. Reactive. |
ui:if="expr" | Works, as ui:show | Today it behaves exactly like ui:show: the element is always built, and only its visibility follows the expression. |
ui:for="item in list" | Works | Builds one copy of the element for each item, and rebuilds the copies when the list changes. |
ui:else-if="expr", ui:else | Compile error | ui: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 error | ui:key is not supported yet; ui:for rebuilds rows on change. |
ui:ref, anything else | Compile error | unknown 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.