CodingNic

Events

Mini Project: Interactive To-Do List

Events 35 min read

Mini Project: Interactive To-Do List

This Chapter Introduces No New Concepts

Everything here uses what you already have: selecting elements, creating elements, listening for events, and delegation. This lesson builds all of it into one working, fully styled page, one small piece at a time.

💡 Why this matters: Real interactive pages combine several small techniques at once. This is where “I know the DOM and I know events” turns into “I can build something.”

What You’re Building

A to-do list where a user can type a task and add it, click a task to mark it complete, and click a delete button to remove it. No page reload, no server, everything happens live in the DOM.

The Starter Page

The design is already done. Save this as index.html and open it in a browser, everything below is about filling in the <script> tag at the bottom.

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Todo List</title>
<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: Arial, Helvetica, sans-serif;
}
body {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: linear-gradient(135deg, #6a11cb, #2575fc);
  padding: 20px;
}
.container {
  width: 100%;
  max-width: 420px;
  background: rgba(255, 255, 255, .15);
  backdrop-filter: blur(15px);
  border: 1px solid rgba(255, 255, 255, .2);
  border-radius: 20px;
  padding: 25px;
  box-shadow: 0 15px 35px rgba(0, 0, 0, .3);
}
h1 {
  text-align: center;
  color: #fff;
  margin-bottom: 20px;
}
.input-box {
  display: flex;
  gap: 10px;
}
.input-box input {
  flex: 1;
  padding: 14px;
  border: none;
  outline: none;
  border-radius: 10px;
  font-size: 16px;
}
.input-box button {
  padding: 14px 20px;
  border: none;
  border-radius: 10px;
  background: #00c6ff;
  color: #fff;
  font-size: 16px;
  font-weight: bold;
  cursor: pointer;
  transition: .3s;
}
.input-box button:hover {
  background: #0096c7;
}
ul {
  list-style: none;
  margin-top: 20px;
}
li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: rgba(255, 255, 255, .2);
  color: #fff;
  padding: 12px 15px;
  border-radius: 12px;
  margin-bottom: 12px;
  animation: fadeIn .3s ease;
}
.task {
  flex: 1;
  cursor: pointer;
  word-break: break-word;
}
.completed {
  text-decoration: line-through;
  opacity: .6;
}
.delete {
  width: 38px;
  height: 38px;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: #ff5d5d;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: .3s;
}
.delete svg {
  width: 18px;
  height: 18px;
}
.delete:hover {
  background: #ff4d4d;
  color: #fff;
  transform: rotate(10deg) scale(1.1);
  box-shadow: 0 8px 15px rgba(255, 77, 77, .4);
}
.delete:active {
  transform: scale(.9);
}
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
</style>
</head>
<body>
  <div class="container">
    <h1>📝 My Todo List</h1>
    <div class="input-box">
      <input type="text" id="taskInput" placeholder="Enter your task...">
      <button id="addBtn">Add</button>
    </div>
    <ul id="taskList"></ul>
  </div>

  <script>
    // Your JavaScript goes here
  </script>
</body>
</html>

#taskList starts empty. Every task gets added to it by JavaScript. Notice there’s no onclick="..." anywhere in this markup, every listener in this project gets attached the way Module 2 taught: with .addEventListener() in the <script> tag, not scattered across HTML attributes.

Step 1: Adding a Task

Start with what you need

Before writing any logic, think about what elements this feature actually touches: the input (to read what the user typed), the “Add” button (to know when to act), and the list (to add the new task to). Get references to all three first.

javascript
const input = document.getElementById("taskInput");
const addBtn = document.getElementById("addBtn");
const taskList = document.getElementById("taskList");

Decide where the logic should live

A task can be added two different ways: clicking “Add”, or pressing Enter in the input. Both need to do the exact same thing. Rather than writing that logic twice, put it in one function that both triggers can call later.

javascript
function addTask() {
  // logic goes here
}

Read what the user typed

The first thing addTask() needs is the text currently in the input.

javascript
function addTask() {
  const text = input.value.trim();
}

.trim() strips leading and trailing whitespace, so a task like " Write lesson " becomes "Write lesson", and a submission that’s only spaces becomes an empty string.

Handle the empty case before building anything

If there’s nothing to add, stop right here. Deciding this early, before creating any elements, means the rest of the function never has to think about the empty case again.

javascript
function addTask() {
  const text = input.value.trim();
  if (!text) return;
}

Build the list item

Now that you know there’s real text to work with, build what one task looks like in the DOM. Start with the outer container, an <li>.

javascript
function addTask() {
  const text = input.value.trim();
  if (!text) return;

  const li = document.createElement("li");
}

Add the task’s text

The <li> needs a <span> inside it to hold the task text, matching the .task class already styled in the CSS above.

javascript
  const task = document.createElement("span");
  task.className = "task";
  task.textContent = text;

Add the delete button

Each task also needs its own delete button, matching the .delete class from the CSS. The icon inside it is a small, fixed SVG string, set with .innerHTML. This is safe here (unlike the security warning back in Module 1) because you wrote this exact string yourself, it isn’t text a user typed in.

javascript
  const deleteBtn = document.createElement("button");
  deleteBtn.className = "delete";
  deleteBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="M9 3h6l1 2h5v2H3V5h5l1-2zm1 7h2v8h-2v-8zm4 0h2v8h-2v-8zM7 10h2v8H7v-8zm-1 11h12a2 2 0 0 0 2-2V7H4v12a2 2 0 0 0 2 2z"/></svg>`;

Put the piece together and add it to the page

The task text and delete button both need to go inside the <li>, and the <li> needs to go inside #taskList.

javascript
  li.append(task, deleteBtn);
  taskList.appendChild(li);

Reset the input for the next task

The last thing addTask() needs to do is clear the input and put the cursor back in it, so the user can immediately type the next task.

javascript
  input.value = "";
  input.focus();

Put together, addTask() looks like this:

javascript
function addTask() {
  const text = input.value.trim();
  if (!text) return;

  const li = document.createElement("li");

  const task = document.createElement("span");
  task.className = "task";
  task.textContent = text;

  const deleteBtn = document.createElement("button");
  deleteBtn.className = "delete";
  deleteBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="M9 3h6l1 2h5v2H3V5h5l1-2zm1 7h2v8h-2v-8zm4 0h2v8h-2v-8zM7 10h2v8H7v-8zm-1 11h12a2 2 0 0 0 2-2V7H4v12a2 2 0 0 0 2 2z"/></svg>`;

  li.append(task, deleteBtn);
  taskList.appendChild(li);

  input.value = "";
  input.focus();
}

Each task’s markup, once addTask() runs, looks like this on the page:

text
<li>
  <span class="task">Write lesson</span>
  <button class="delete">
    <svg>...</svg>
  </button>
</li>

Wire up the two triggers

addTask() exists now, but nothing calls it yet. Both the button and the Enter key should call it, and both listeners are simple because all the real work already lives inside the function.

javascript
addBtn.addEventListener("click", addTask);

input.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    addTask();
  }
});

Clicking “Add” and pressing Enter both call the same addTask() function, so the two ways of submitting a task can never fall out of sync with each other.

Step 2: Completing and Deleting, with Delegation

Think about what’s missing

Right now, tasks can be added, but clicking one does nothing. Clicking a task’s text should toggle it complete, and clicking its delete button should remove it. The tempting first instinct is to attach a listener inside addTask(), directly on the new task and deleteBtn elements, right when they’re created.

That would work for the moment, but recall Module 2, lesson 4: any listener attached only to elements that exist right now misses every task added afterward, unless you remembered to re-attach it inside addTask() every single time. Delegation avoids that problem entirely: attach one listener to #taskList, once, and it handles every task, current and future.

Attach one listener to the list

javascript
taskList.addEventListener("click", (event) => {
  // figure out what was actually clicked
});

Check for a delete click first

Inside the listener, event.target is whatever the user actually clicked, which could be the delete button itself, or the <svg> icon inside it, or the <path> inside that. .closest(".delete") walks upward from wherever the click landed until it finds the button.

javascript
taskList.addEventListener("click", (event) => {
  const deleteBtn = event.target.closest(".delete");
  if (deleteBtn) {
    deleteBtn.closest("li").remove();
    return;
  }
});

The return matters: once a delete click is handled, there’s nothing left to check for this click, no need to also test whether it was a task-text click.

Check for a task click second

If the click wasn’t on a delete button, check whether it landed on a task’s text instead.

javascript
taskList.addEventListener("click", (event) => {
  const deleteBtn = event.target.closest(".delete");
  if (deleteBtn) {
    deleteBtn.closest("li").remove();
    return;
  }

  const task = event.target.closest(".task");
  if (task) {
    task.classList.toggle("completed");
  }
});

The .completed class is already defined in the stylesheet on the starter page, toggling it is all the JavaScript needs to do, the strikethrough itself is handled entirely by CSS.

💡 A note on scope: the original design this page is based on faded each task out before removing it, using setTimeout(). Timers are covered in the next module, Asynchronous JavaScript, so this project removes a task immediately instead. Once you’ve finished that module, come back and add the fade-out yourself, it’s a small change.

Putting It Together

With both pieces in place: typing a task and clicking “Add” (or pressing Enter) adds it to the list, clicking a task’s text toggles it complete, and clicking its delete button removes it, all without a single page reload.

javascript
// Simulating a user adding two tasks, one at a time:
input.value = "Write lesson";
addBtn.click();
// list now has 1 task: "Write lesson"

input.value = "Test code";
addBtn.click();
// list now has 2 tasks

Try It

Build the full to-do list yourself, following the steps above in order:

  1. Add a task by clicking “Add”, then add another by pressing Enter inside the input. Confirm both appear in #taskList in the order added.
  2. Click a task’s text once. Confirm it gains the completed class and shows a strikethrough. Click it again and confirm the class is removed.
  3. Click a task’s delete button (including clicking directly on the icon inside it). Confirm it’s removed from #taskList and the other tasks are unaffected.
  4. Try clicking “Add” with the input empty, or containing only spaces. Confirm nothing is added.
  5. Add a task, then immediately click its delete button. Confirm delegation handles it correctly, even though no listener was ever attached to that specific task.

Recap

  • This project combined createElement/append (Module 1) with event listeners and delegation (this module) into one working, styled page.
  • Building addTask() step by step: read the input, bail out if empty, build the elements, attach them, reset the input, showed how to break one feature into small, ordered pieces rather than writing it all at once.
  • Delegation was the key piece for completing and deleting: one listener on #taskList, attached once, correctly handles every task, including ones added long after the page first loaded.

Next module: Asynchronous JavaScript, timers, promises, async/await, and fetching real data, including that fade-out effect this project skipped.