Combine selecting, creating and removing elements with event listeners to build a working to-do list.
Why this matters
A to-do list is the classic first interactive app for a reason: it uses everything from this module. You read what the user typed, create new elements, react to clicks, and remove things from the page. Once you can build one from scratch, you understand the core loop behind most web interfaces: user does something, update the data, update the page.
The plan
Before writing code, describe the behaviour in plain sentences:
- The user types a task and clicks Add (or presses Enter).
- If the text is empty, nothing happens.
- Otherwise a new item appears in the list and the box is cleared.
- Clicking an item's text marks it done (and clicking again un-marks it).
- Clicking an item's Delete button removes it.
Each sentence will become a few lines of code. Writing the plan first is a habit worth keeping for every project.
The HTML
<input id="todo-input" type="text" aria-label="New task">
<button id="add-btn" type="button">Add</button>
<ul id="todo-list"></ul>
The list starts empty. Each item we add will look like this:
<li>
<span class="text">Buy milk</span>
<button class="delete" type="button">Delete</button>
</li>
Creating elements
document.createElement makes a new element that is not on the page yet. You fill it in, then attach it with append:
function addTodo(text) {
const li = document.createElement("li");
const span = document.createElement("span");
span.className = "text";
span.textContent = text; // textContent: safe for user input
const del = document.createElement("button");
del.type = "button";
del.className = "delete";
del.textContent = "Delete";
li.append(span, del);
list.append(li);
}
Building with createElement and textContent is safer than gluing together an HTML string for innerHTML, because whatever the user types is always shown as text, never run as HTML.
Reading and validating input
const input = document.querySelector("#todo-input");
const addBtn = document.querySelector("#add-btn");
const list = document.querySelector("#todo-list");
function handleAdd() {
const text = input.value.trim(); // remove spaces at both ends
if (text === "") return; // rule 2: ignore empty tasks
addTodo(text);
input.value = ""; // rule 3: clear the box
input.focus(); // ready for the next task
}
addBtn.addEventListener("click", handleAdd);
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") handleAdd();
});
trim() matters: without it, a task of three spaces would be added as a blank item.
Done and delete with one listener
Items are created after the page loads, so we cannot attach listeners to them in advance. Instead use event delegation: one listener on the list that checks what was clicked.
list.addEventListener("click", (event) => {
const li = event.target.closest("li");
if (!li) return;
if (event.target.matches("button.delete")) {
li.remove(); // rule 5
} else if (event.target.matches(".text")) {
li.classList.toggle("done"); // rule 4
}
});
event.targetis the exact element clicked.closest("li")walks up from there to the nearestli.matches(selector)checks whether an element fits a CSS selector.
The look of a finished task lives in CSS:
.done .text { text-decoration: line-through; opacity: 0.6; }
Keeping the list after a refresh (optional)
On a real site you would save the tasks so they survive a page reload, for example with localStorage:
localStorage.setItem("todos", JSON.stringify(["Buy milk"]));
JSON.parse(localStorage.getItem("todos") ?? "[]");
localStorage only stores strings, which is why the data goes through JSON.stringify and JSON.parse (more on JSON in module 3). The practice area on this site runs your code in a locked-down sandbox where localStorage is not available, so the activity skips this step. Try it in your own project, and wrap the calls in try/catch, because browsers can refuse storage in private windows.
Common mistakes
- Adding listeners to items that do not exist yet. Use delegation on the parent list.
- Skipping
trim(). Blank tasks slip through. - Using
innerHTMLwith the task text. UsecreateElementandtextContent. - Forgetting
type="button". Inside a form, a button defaults to submitting the form and reloading the page. - Removing the wrong element.
event.target.remove()removes just the button; remove itsliwithclosest("li").
Quick recap
- Plan the behaviour in sentences, then turn each into code.
createElement,textContentandappendbuild new elements safely.- Validate input with
trim()and clear the box after adding. - One delegated listener handles done and delete for every item, even new ones.
Try it
Open the Do tab and build the to-do list: add, ignore empty input, mark done and delete.