# Todo list

Source: https://signy.dev/examples/todo-list/

Add, tick, filter and clear todos. They survive a page reload.

Each todo keeps its `done` flag in its own signal, so ticking one updates its row only. Computed values derive the visible list and the count, and an effect saves the list to `localStorage` after every change.

## Preview

[Open the page on its own](https://signy.dev/examples/todo-list/demo/index.html.md)

## Code

Save it as an `.html` file and open it in a browser. It loads Signy and Iris CSS from the CDN, so there is nothing to install or build.

**Show the full page**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <title>Todo list - Signy example</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/iriscss@1/dist/iris.min.css">
    <script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
  </head>
  <body>
    <main class="container" id="app"></main>

    <script>
      const { Component, Signal, Computed, application, html, For, Show, effect } = Signy;

      const STORAGE_KEY = "signy-todos";

      function load() {
        try {
          return JSON.parse(localStorage.getItem(STORAGE_KEY)) ?? [];
        } catch {
          return [];
        }
      }

      // done is a signal on each item, so ticking one updates its row only
      function createTodo({ id, text, done = false }) {
        return { id, text, done: new Signal(done) };
      }

      class TodoApp extends Component {
        todos = new Signal(load().map(createTodo));
        filter = new Signal("all");
        nextId = Math.max(0, ...this.todos.value.map((todo) => todo.id)) + 1;

        visible = new Computed(() => {
          const filter = this.filter.value;
          return this.todos.value.filter((todo) =>
            filter === "all" ? true : filter === "done" ? todo.done.value : !todo.done.value,
          );
        });
        remaining = new Computed(() => this.todos.value.filter((todo) => !todo.done.value).length);

        setup() {
          effect(() => {
            const plain = this.todos.value.map((todo) => ({ id: todo.id, text: todo.text, done: todo.done.value }));
            try {
              localStorage.setItem(STORAGE_KEY, JSON.stringify(plain));
            } catch {}
          });
        }

        add(form) {
          const text = form.elements.text.value.trim();
          if (!text) return;
          this.todos.set((list) => [...list, createTodo({ id: this.nextId++, text })]);
          form.reset();
        }

        remove(id) {
          this.todos.set((list) => list.filter((todo) => todo.id !== id));
        }

        clearDone() {
          this.todos.set((list) => list.filter((todo) => !todo.done.value));
        }

        filterButton(value, label) {
          return html`
            <button
              class="small"
              aria-pressed=${() => String(this.filter.value === value)}
              onClick=${() => (this.filter.value = value)}
            >${label}</button>
          `;
        }

        render() {
          return html`
            <article class="card">
              <header>
                <h1>Todos</h1>
                <p>${() => this.remaining.value} left</p>
              </header>

              <form class="group" onSubmit=${(e) => (e.preventDefault(), this.add(e.currentTarget))}>
                <input name="text" placeholder="What needs doing?" aria-label="New todo" autocomplete="off" />
                <button class="primary">Add</button>
              </form>

              <ul>
                ${For({
                  each: () => this.visible.value,
                  keyBy: "id",
                  fallback: html`<li>Nothing here.</li>`,
                  children: (todo) => html`
                    <li>
                      <label>
                        <input type="checkbox" checked=${todo.done.peek()}
                          onChange=${(e) => (todo.done.value = e.currentTarget.checked)} />
                        ${todo.text}
                      </label>
                      <button class="small outlined danger" aria-label=${`Delete ${todo.text}`}
                        onClick=${() => this.remove(todo.id)}>Delete</button>
                    </li>
                  `,
                })}
              </ul>

              <footer class="cluster">
                ${this.filterButton("all", "All")}
                ${this.filterButton("active", "Active")}
                ${this.filterButton("done", "Done")}
                ${Show({
                  when: () => this.todos.value.length > this.remaining.value,
                  children: html`<button class="small secondary outlined" onClick=${() => this.clearDone()}>Clear done</button>`,
                })}
              </footer>
            </article>
          `;
        }
      }

      application(TodoApp).mount("#app");
    </script>
  </body>
</html>

```

## What it uses

- [Signals](https://signy.dev/documentation/1/reactivity/signals/index.html.md)
- [Computed](https://signy.dev/documentation/1/reactivity/computed/index.html.md) for the filtered list and the count
- [Effect](https://signy.dev/documentation/1/reactivity/effects/index.html.md) that saves to localStorage
- [For](https://signy.dev/documentation/1/templates/for-and-index/index.html.md) with `keyBy`, and [Show](https://signy.dev/documentation/1/templates/show/index.html.md)
