Skip to content

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 ​

ArgumentMeaning
typeNameThe 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.
settersOptional 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.