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

Type a task and press Add. Tick a task to mark it done; press ✕ to delete it.

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);
  });
});
Add a task and it appears; tick it and only that task's 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).

← Counter Back to examples