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
Persist & filter
Save the list to localStorage so it is still there tomorrow, and wire the All / Active / Done filters. This is the lesson that turns a toy into something you would actually use.
Write the spec the model can hit
A prompt is a specification. The difference between a junior and a senior prompt is the constraints a senior names that a junior leaves to chance. This lesson turns your architecture sketch into a prompt that produces reviewable code.
Add todos with JavaScript
Wire the form so typing and pressing Add creates a real item. You will hold the todos in an array and render the list from it — the pattern every UI framework formalises later.
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.