A to-do list with delegation

Build a to-do list that handles every task with a single click listener.

What to build

A list where tasks can be added, finished and deleted.

Requirements

  • Submitting the form adds an li to #todos holding a span with the class text and a button with the class delete and the word Delete. Trim the text, ignore a blank task and empty the box.
  • Clicking a task's text switches the class done on its li.
  • Clicking its delete button removes its li.
  • #left always shows how many tasks are not done: 2 tasks left, or 1 task left for exactly one.
  • Use one click listener on #todos, not one for each task.

Constraints

Work out the count from the page, not from a separate variable, so that the two can never disagree. Use textContent for the task's words.

Example

<li><span class="text">Buy rice</span><button class="delete">Delete</button></li>

Why it works

Events bubble, so the list hears every click inside it, even on tasks that did not exist when the listener was added. closest finds out which part of which task was clicked.

Hints

Hint 1

Put one click listener on #todos, and use event.target.closest(...) to see what was clicked.

Hint 2

Check for .delete first, then for the task text.

Hint 3

Count with querySelectorAll("#todos li:not(.done)").length, so that the count is always right.

Solution

Show one correct solution (try it yourself first)
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>To-do</title>
  <style>
    li.done .text { text-decoration: line-through; }
  </style>
</head>
<body>
  <form id="add">
    <label for="task">New task</label>
    <input id="task" autocomplete="off">
    <button>Add</button>
  </form>
  <ul id="todos"></ul>
  <p id="left">0 tasks left</p>
  <script>
    const form = document.querySelector("#add");
    const input = document.querySelector("#task");
    const list = document.querySelector("#todos");
    const left = document.querySelector("#left");

    function update() {
      const n = list.querySelectorAll("li:not(.done)").length;
      left.textContent = n + (n === 1 ? " task left" : " tasks left");
    }

    form.addEventListener("submit", (event) => {
      event.preventDefault();
      const words = input.value.trim();
      if (words === "") return;
      const li = document.createElement("li");
      const text = document.createElement("span");
      text.className = "text";
      text.textContent = words;
      const remove = document.createElement("button");
      remove.className = "delete";
      remove.textContent = "Delete";
      li.append(text, remove);
      list.append(li);
      input.value = "";
      update();
    });

    list.addEventListener("click", (event) => {
      const li = event.target.closest("li");
      if (!li) return;
      if (event.target.closest(".delete")) {
        li.remove();
      } else if (event.target.closest(".text")) {
        li.classList.toggle("done");
      }
      update();
    });
  </script>
</body>
</html>

Lesson: Events and forms →