Search KetJS

Search titles, descriptions, and section headings.

    Diagram

    100%
    Drag to pan · Scroll or pinch to zoom · + / − to zoom · Arrow keys to pan · 0 to fit · 1 to reset
    ← All lessons

    STEP 05 / Build the frontend

    Grow the counter into a todo list

    Handle form submission, derive counts, and preserve item identity with keyed lists.

    Course outline · lesson 5 of 33

    Before you start: complete Build a counter with signals, or make sure you can pass its checkpoint.

    Build the smallest useful workflow#

    Choose Todo · keyed lists in the playground. Add two tasks, mark one complete, and observe the remaining count. State exists only for the current preview instance; persistence comes later.

    The local reference project adds this island in src/islands/todo.tsx:

    // File: learn-view/src/islands/todo.tsx
    import { signal, each } from '@ketvietlab/ketjs-view'
    
    export default function Todo(_props: Record<string, unknown>) {
      const tasks = signal([{ id: 1, title: 'Learn signals', done: false }])
      const draft = signal('')
      let nextId = 2
      function add(event: Event) {
        event.preventDefault()
        if (!draft().trim()) return
        tasks.set(rows => [...rows, { id: nextId++, title: draft().trim(), done: false }])
        draft.set('')
      }
      return () => <section>
        <h1>Your first todo app</h1>
        <form onSubmit={add}>
          <label for="task-title">Task title</label>
          <input id="task-title" value={draft()} onInput={(e: Event) => draft.set((e.target as HTMLInputElement).value)} />
          <button type="submit">Add task</button>
        </form>
        <ul>{each(tasks(), task => task.id, task => <li>
          <label><input type="checkbox" checked={task.done} onChange={() => tasks.set(rows => rows.map(row => row.id === task.id ? { ...row, done: !row.done } : row))} /> {task.title}</label>
        </li>)}</ul>
        <p>{tasks().filter(task => !task.done).length} tasks remaining. Reloading resets this in-memory example.</p>
      </section>
    }

    Understand the three state decisions#

    tasks is the array signal. Every change returns a new array, making the update explicit. draft is the input signal. nextId is an instance-local counter; this educational UI does not need a database-generated ID yet.

    each(tasks(), task => task.id, ...) identifies each rendered record by a stable ID. A task title can change and two tasks can share a title, so neither title nor the entire task content is a suitable identity key. Array positions also change when items move.

    Add a page for the island#

    Register todo: 'src/islands/todo.tsx' in the config's islands map. Then create:

    // File: learn-view/src/pages/todo.tsx
    import { definePage, island } from '@ketvietlab/ketjs-view-tools'
    import todo from '../islands/todo.tsx'
    export default definePage({
     head: { title: 'Todo lab | learn-view', description: 'Add and complete tasks with signals and keyed lists.', lang: 'en' },
     view: () => <><header class="site-header"><a href="/">learn-view</a><nav><a href="/todo/">Todo</a><a href="/about/">About</a></nav></header><main>{island('todo', todo, {}, { key: [] })}</main></>,
    })

    Test the interactions deliberately#

    Submit with Enter as well as the button. Submit whitespace and confirm that it adds nothing. Mark a task complete, add another, and ensure the first task's state stays attached to its ID. The remaining count is derived from the array; storing a second mutable count would create a synchronization problem.

    Know what this example does not persist#

    Reloading the page recreates the initial task. That is expected for this lesson. Later, the backend becomes the source of saved tasks, and a browser island owns pending/error state around API calls.

    Checkpoint#

    Whitespace is ignored, Enter submits, completion updates the count, and stable IDs keep each row attached to its data.

    Practice on your own#

    Add “Remove completed.” Use a new filtered array and verify that an unfinished task keeps its checkbox state and identity.

    Reference#

    For the complete API contract, read Rendering.