Complete & delete
Lesson 3 of 4 — make each row interactive with one clever listener.
Event delegation, immutable-ish array updates (filter to delete), and keeping a derived count in sync.
Prerequisites
Lesson 2 (todos render from an array).
Step 314 min
Toggle done and delete — with one listener
GoalEvery row responds to clicks, and the count updates, without a listener per row.
Instead of attaching a handler to every row (which breaks the moment you add a new one), we attach one listener to the <ul> and read event.target to decide what was clicked. Clicking the × removes the todo; clicking elsewhere toggles its done flag. Then re-render and recompute "items left".
DetailWhy one listener beats one-per-row
Rows come and go. A listener bound to a row dies with it, and a new row has none. A single listener on the parent survives every re-render and handles clicks for rows that didn’t even exist when it was attached. That pattern is called event delegation.
const list = document.getElementById('list');const form = document.getElementById('new-todo');const input = document.getElementById('title');const count = document.getElementById('count'); let todos = []; form.addEventListener('submit', (e) => { e.preventDefault(); const title = input.value.trim(); if (!title) return; todos.push({ id: Date.now(), title, done: false }); input.value = ''; render();}); // One listener on the list handles every row (event delegation).list.addEventListener('click', (e) => { const li = e.target.closest('li'); if (!li) return; const id = Number(li.dataset.id); if (e.target.matches('.remove')) { todos = todos.filter((t) => t.id !== id); } else { const t = todos.find((t) => t.id === id); if (t) t.done = !t.done; } render();}); function render() { list.innerHTML = ''; for (const t of todos) { const li = document.createElement('li'); li.dataset.id = t.id; li.className = t.done ? 'done' : ''; li.innerHTML = '<span class="check"></span>' + '<span class="title">' + esc(t.title) + '</span>' + '<button class="remove" aria-label="Delete">\u00d7</button>'; list.appendChild(li); } const left = todos.filter((t) => !t.done).length; count.textContent = left + ' left';} function esc(s) { return s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c], );} render();<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Todo</title> <link rel="stylesheet" href="styles.css" /> </head> <body> <main class="app"> <h1>Today</h1> <form id="new-todo"> <input id="title" name="title" placeholder="What needs doing?" autocomplete="off" /> <button type="submit">Add</button> </form> <ul id="list" class="list"></ul> <footer class="bar"> <span id="count">1 left</span> <span class="filters"> <button data-filter="all" class="active">All</button> <button data-filter="active">Active</button> <button data-filter="done">Done</button> </span> </footer> </main> <script src="app.js"></script> </body></html>* { box-sizing: border-box; }body { margin: 0; min-height: 100vh; display: grid; place-items: start center; padding: 6vh 16px; font-family: system-ui, -apple-system, sans-serif; background: #0b0e14; color: #e7ecf3;}.app { width: 100%; max-width: 460px; }h1 { font-size: 22px; letter-spacing: -0.01em; margin: 0 0 16px; }#new-todo { display: flex; gap: 8px; margin-bottom: 18px; }#title { flex: 1; padding: 11px 14px; border-radius: 10px; border: 1px solid #232a36; background: #11151c; color: inherit; font-size: 15px;}#title:focus { outline: none; border-color: #6ea8fe; }#new-todo button { padding: 0 18px; border-radius: 10px; border: 1px solid #2b3340; background: #6ea8fe; color: #0b0e14; font-weight: 600; cursor: pointer;}.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }.list li { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: 10px; background: #11151c; border: 1px solid #1c2330;}.check { width: 18px; height: 18px; flex: none; border-radius: 999px; border: 1.5px solid #3a4456; cursor: pointer;}.list li.done .check { background: #4ade80; border-color: #4ade80; }.list li.done .title { color: #5b6675; text-decoration: line-through; }.title { flex: 1; }.remove { border: none; background: none; color: #5b6675; font-size: 20px; line-height: 1; cursor: pointer;}.remove:hover { color: #f87171; }.bar { display: flex; justify-content: space-between; align-items: center; margin-top: 16px; font-size: 13px; color: #8b93a3;}.filters { display: flex; gap: 4px; }.filters button { border: 1px solid transparent; background: none; color: inherit; padding: 4px 10px; border-radius: 8px; cursor: pointer;}.filters button.active { border-color: #2b3340; color: #e7ecf3; }Checkpoint
Add a confirm() before deleting, so a misclick does not lose a task.
Concepts covered · 2
More like this
Review what comes back like a senior
Models produce confident, plausible, well-formatted code — which is exactly what makes their mistakes dangerous. This lesson is the review rubric: the specific failure classes to hunt for, because "it looks right" is not review.
Orchestrate agents without the codebase rotting
Putting it together: context, guardrails, and a verification loop that lets you run coding agents across a real codebase without it drifting into an incoherent pile. This is the senior loop at full speed.
Structure & style the app
Lay down the HTML skeleton and a calm dark style for a todo app. No JavaScript yet — just the shape, so the behaviour you add next has somewhere to live.
Liked this one?
Pass it on
Discussion
Be the first to ask
Your questions stay private with the team. We can pin answers to share with everyone.
Sign in to ask a question privately on this tutorial.
Sign inNo pinned answers yet for this tutorial.