Custom widgets
registerWidget adds your own tag to one registry. The tag then works like a built-in one: it takes the common attributes, your own attributes (static or bound), and on: events your widget fires.
ts
import { AttrSetter, Component, Controller, DEFAULT_STAGE, LAYOUT_HBOX, Label, Ref, Screen, TemplateRegistry } from "willowui";
// A row of stars. Clicking a star sets the rating and fires "change".
class StarRating extends Component {
stars: Label[] = [];
value: int = 0;
constructor(name: string) {
super(name);
this.layout = LAYOUT_HBOX;
this.spacing = 4.0;
this.setMaxStars(5);
}
setMaxStars(n: int): void {
for (const s of this.stars) { this.removeChild(s); s.dispose(); }
this.stars = [];
for (let i: int = 0; i < n; i++) {
const star: Label = new Label(this.name + "-star-" + i, "*", "default", 24.0);
const index: int = i + 1;
star.setInteractable(true);
star.setOnActivate((): void => { this.setValue(index); this.fireEvent("change"); });
this.stars.push(star);
this.addChild(star);
}
this.setValue(this.value);
}
setValue(v: int): void {
this.value = v;
for (let i: int = 0; i < this.stars.length; i++) {
this.stars[i].setColor(i < v ? Color.rgbaf(1.0, 0.8, 0.2, 1.0) : Color.rgbaf(0.4, 0.4, 0.4, 1.0));
}
}
}
const TEMPLATE: string = `
<Panel layout="vbox" spacing="8" padding="16">
<StarRating name="stars" maxStars="5" bind:value="rating" on:change="rated" />
<Label bind:text="'Rating: ' + rating" />
</Panel>`;
class RatingScreen implements Controller {
rating: Ref<int> = new Ref<int>(3);
screen: Screen | null = null;
onCreate(screen: Screen): void {
this.screen = screen;
const reg: TemplateRegistry = new TemplateRegistry();
const setters: Map<string, AttrSetter> = new Map<string, AttrSetter>();
setters.set("value", (c: Component, v: any): void => { (c as StarRating).setValue(v as int); });
setters.set("maxStars", (c: Component, v: any): void => { (c as StarRating).setMaxStars(parseInt("" + v) as int); });
reg.registerWidget("StarRating", (name: string): Component => new StarRating(name), setters);
reg.registerXML("rating", TEMPLATE);
screen.add(reg.instantiate("rating", this, screen));
}
onUpdate(dt: float): void {}
onDispose(): void {}
lookupRef(path: string): any {
if (path == "rating") { return this.rating; }
return null;
}
callMethod(name: string): boolean {
if (name == "rated") {
const w: StarRating = (this.screen as Screen).findByName("stars") as StarRating;
this.rating.set(w.value);
return true;
}
return false;
}
}
DEFAULT_STAGE.setScene(engine.root2d);
DEFAULT_STAGE.add(new Screen().withController(new RatingScreen()));
App.onFrame((dt: float): void => { DEFAULT_STAGE.update(dt); });What registerWidget takes
| Argument | Meaning |
|---|---|
typeName | The tag name. It must not be a built-in tag, or registerWidget throws. Register before registerXML, so the compiler knows the name. |
factory | (name: string) => Component. Called once per element. name is the element's name attribute, or the tag name when there is none. |
setters | Optional Map<string, AttrSetter>, where AttrSetter is (c: Component, v: any) => void. Each entry handles one attribute. They are merged over the common setters, so width, layout, variant and the rest keep working. |
A setter receives the attribute's value: a string for a static attribute (maxStars="5" gives "5"), or the evaluated value for a bind: (a number, a boolean, a string). Convert accordingly.
Events
on:anything on a custom tag registers the handler with setOnEvent(name, handler). Fire it from your widget with this.fireEvent(name). on:click is wired to the component's activate callback, as on every tag.
Compiling to .xmlui
xmluic learns your custom tags from a widget manifest passed with --widgets. See Binary templates.
Children
Child elements of a custom tag are instantiated and added with addChild, so a custom container works with no extra code. If your widget keeps its content in an inner component (as Window keeps a body), override contentTarget() to return it.