Skip to content

Recipe: lists ​

List shows rows of data in a scrolling column. It only makes views for the rows on screen and reuses them as you scroll, so a list of thousands of entries costs about the same as a list of twenty.

The easiest way to keep a list in step with your data is to put the data in a ReactiveArray<ListItem> and call bindItems. After that you never touch the list: push, remove or reorder the array and the rows follow, without losing the scroll position.

A list you can add to and remove from ​

ts
import { Button, Component, DEFAULT_STAGE, Label, List, ListItem, ReactiveArray, Ref, Screen } from "willowui";

class Quest {
    title: string;
    reward: int;
    constructor(title: string, reward: int) {
        this.title = title;
        this.reward = reward;
    }
}

const screen: Screen = new Screen();

const quests: ReactiveArray<ListItem> = new ReactiveArray<ListItem>();
quests.push(new ListItem(new Quest("Find the lantern", 50)));
quests.push(new ListItem(new Quest("Clear the cellar", 120)));
quests.push(new ListItem(new Quest("Deliver the letter", 20)));

const selected: Ref<int> = new Ref<int>(-1);

const list: List = new List("quests", 32.0);
list.setPosition(20.0, 20.0);
list.setSize(320.0, 300.0);
list.setSelectable(true);
// Each row: the data is the Quest inside the ListItem.
list.setRenderItem((index: int, data: any): Component => {
    const q: Quest = data as Quest;
    return new Label("row", q.title + "  (" + q.reward + " gold)", "default", 15.0);
});
list.bindItems(quests);
list.bindSelected(selected);
screen.add(list);

const detail: Label = new Label("detail", "Pick a quest", "default", 14.0);
detail.setPosition(360.0, 20.0);
screen.add(detail);
list.setOnChange((): void => {
    const item: any = list.selectedItem();
    if (item != null) { detail.setText("Reward: " + (item as Quest).reward + " gold"); }
});

const add: Button = new Button("add", "Add quest", "default", 14.0);
add.setPosition(20.0, 340.0);
add.setOnClick((): void => {
    quests.push(new ListItem(new Quest("New quest " + (quests.length + 1), 10)));
});
screen.add(add);

const remove: Button = new Button("remove", "Abandon", "default", 14.0);
remove.setPosition(160.0, 340.0);
remove.setOnClick((): void => {
    const i: int = selected.peek();
    if (i >= 0 && i < quests.length) {
        quests.removeAt(i);
        list.clearSelection();
    }
});
screen.add(remove);

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

Notes:

  • setRenderItem gets the row index and the ListItem's data, and returns the row's widget. Without it, each row shows its data as text.
  • selectedItem() returns that same data, or null when nothing is selected.
  • With focus on the list, Up and Down move the selection. selectNext(), selectPrevious(), selectFirst() and selectLast() do the same from code, and scroll the row into view.
  • setItems([...]) replaces everything at once and scrolls back to the top. Use it for lists you rebuild rather than edit.

A short list in XML ​

For a handful of rows, a template's ui:for is enough. Each row's on: handler can pass the row's item and index to your controller, so one method serves every row:

xml
<Panel layout="vbox" spacing="4">
    <Button ui:for="quest in quests" bind:text="quest.title" on:click="abandon(quest, index)" />
</Panel>

Clicking a row calls callMethod("abandon", [quest, index]). ui:for builds a full widget per row and rebuilds them all when the array changes, so switch to a List for long lists. Directives has a complete controller, and Bindings and events covers handler arguments.

Other list widgets ​

WidgetUse it for
TileListA grid of tiles, such as an inventory or a level select. Same bindItems and setRenderItem.
SortableListA list the player reorders by dragging or with the keyboard.
TreeList, SortableTreeListNested items that open and close.
DataTable, TreeTableRows with columns, sortable by column.

See Containers and Composites. In XML, <List> takes itemHeight, selected and size; give it its rows from code with bindItems, after finding it with screen.findByName (see Bindings and events).

See also ​