Tasks
A small task list built from a reified Task type. It puts several pieces together: a two-way input
for the new-task text, a button that adds it, a reactive collection that renders each task, a checkbox bound
straight into each task's done field, and a per-row delete. Every binding is declarative.
Live
The code
// a reified Task type, and a component for one row — defined once
Reactive.Module("Tasks", () => {
Reactive.Type("Task", () => {
Reactive.Field("title", Text);
Reactive.Field("done", Logic.Maybe);
});
Reactive.Module("Ui", () => {
Reactive.Function("Task", () => {
const task = Reactive.Argument(Tasks.Task);
const del = Reactive.Argument(Logic.Maybe); // a settable trigger
Dom.Tag("li", {}, () => {
Input.Checkbox(task.done); // two-way into this task's `done`
Content.Text(task.title);
Button.Set(del, true, () => {
Content.Text("✕");
});
});
});
});
});
const tasks = Reactive.State(Collection.Array(Tasks.Task))(Collection.Array.Empty(Tasks.Task)());
const draft = Reactive.State(Text)("");
Layout.Row(() => {
Input.Text(draft, "New task");
const add = Reactive.Reverse(() => {
const title = Text.Trim(draft);
const task = Tasks.Task(title, false); // construct via the type
const grown = Collection.Array.Push(tasks, task);
Reactive.Set(tasks, grown);
Reactive.Set(draft, "");
});
Button.Set(add, true, () => {
Content.Text("Add");
});
});
// iterate the array; each row gets the element and a delete trigger
Dom.Tag("ul", {}, () => {
Reactive.Each(tasks, () => {
const task = Reactive.Argument(Tasks.Task);
const del = Reactive.Reverse(() => {
const without = Collection.Array.Remove(tasks, task);
Reactive.Set(tasks, without);
});
Tasks.Ui.Task(task, del);
});
});
done flips; delete one and only
that row leaves. The rest of the page and the rest of the list stay untouched.What happens
Reactive.Type reifies a Task, a record with a title and a
done flag, and the type is its constructor, so Tasks.Task(title, false) builds
one. Tasks.Ui.Task is a second reified function: a component for a single row. Everything hangs off
the one reactive value model, so a task, the list, and the draft text are all
reactive values.
Inside a row, Input.Checkbox(task.done) binds the checkbox two-way to that task's field:
dotted access reaches into the record, and because the field carries its own writable half, ticking the box
writes straight back into that task through the field itself. The delete button carries no logic of its own; it
just Sets a settable, the del trigger the row was handed, whose
reverse the list supplies.
Reactive.Each runs its body once per task, handing it the element. Deleting
is then ordinary: Remove(tasks, task) builds the array without that element and sets it back.
The element keys the driver reconciles by stay hidden, so ticking one task, or removing another, keeps every
surviving row's DOM and checkbox exactly as it was (see Identity).