advanced
Step 20 of 20
Building a Todo App
JavaScript Programming
Building a Todo App
Building a todo application is a rite of passage for web developers because it exercises all the fundamental JavaScript skills: DOM manipulation, event handling, data management, and local storage persistence. In this lesson, you will build a fully-featured todo app from scratch that supports adding, completing, deleting, filtering, and editing tasks. All data persists in local storage so tasks survive page reloads. This project brings together everything you have learned in this JavaScript learning path.
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Todo App</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
.todo-input { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.todo-input input { flex: 1; padding: 0.5rem; font-size: 1rem; }
.todo-input button { padding: 0.5rem 1rem; cursor: pointer; }
.filters { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.filters button { padding: 0.25rem 0.75rem; cursor: pointer; }
.filters button.active { background: #333; color: white; }
.todo-item { display: flex; align-items: center; padding: 0.5rem; border-bottom: 1px solid #eee; }
.todo-item.completed span { text-decoration: line-through; color: #999; }
.todo-item span { flex: 1; }
.stats { margin-top: 1rem; color: #666; }
</style>
</head>
<body>
<h1>Todo App</h1>
<div class="todo-input">
<input type="text" id="todoInput" placeholder="What needs to be done?">
<button id="addBtn">Add</button>
</div>
<div class="filters">
<button class="active" data-filter="all">All</button>
<button data-filter="active">Active</button>
<button data-filter="completed">Completed</button>
</div>
<ul id="todoList"></ul>
<div class="stats" id="stats"></div>
<script src="app.js"></script>
</body>
</html>
Todo App JavaScript (app.js)
class TodoApp {
#todos = [];
#filter = "all";
#nextId = 1;
constructor() {
this.#loadFromStorage();
this.#bindEvents();
this.#render();
}
#loadFromStorage() {
try {
const saved = JSON.parse(localStorage.getItem("todos") || "[]");
this.#todos = saved;
this.#nextId = saved.length
? Math.max(...saved.map(t => t.id)) + 1
: 1;
} catch {
this.#todos = [];
}
}
#saveToStorage() {
localStorage.setItem("todos", JSON.stringify(this.#todos));
}
addTodo(text) {
const trimmed = text.trim();
if (!trimmed) return;
this.#todos.push({
id: this.#nextId++,
text: trimmed,
completed: false,
createdAt: Date.now()
});
this.#saveToStorage();
this.#render();
}
toggleTodo(id) {
const todo = this.#todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.#saveToStorage();
this.#render();
}
}
deleteTodo(id) {
this.#todos = this.#todos.filter(t => t.id !== id);
this.#saveToStorage();
this.#render();
}
setFilter(filter) {
this.#filter = filter;
this.#render();
}
#getFilteredTodos() {
switch (this.#filter) {
case "active": return this.#todos.filter(t => !t.completed);
case "completed": return this.#todos.filter(t => t.completed);
default: return this.#todos;
}
}
#render() {
const list = document.querySelector("#todoList");
const filtered = this.#getFilteredTodos();
list.innerHTML = filtered.map(todo => `
<li class="todo-item ${todo.completed ? "completed" : ""}" data-id="${todo.id}">
<input type="checkbox" ${todo.completed ? "checked" : ""}>
<span>${todo.text}</span>
<button class="delete-btn">Delete</button>
</li>
`).join("");
// Update stats
const active = this.#todos.filter(t => !t.completed).length;
const total = this.#todos.length;
document.querySelector("#stats").textContent =
`${active} item${active !== 1 ? "s" : ""} left out of ${total}`;
// Update filter buttons
document.querySelectorAll(".filters button").forEach(btn => {
btn.classList.toggle("active", btn.dataset.filter === this.#filter);
});
}
#bindEvents() {
const input = document.querySelector("#todoInput");
const addBtn = document.querySelector("#addBtn");
addBtn.addEventListener("click", () => {
this.addTodo(input.value);
input.value = "";
input.focus();
});
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
this.addTodo(input.value);
input.value = "";
}
});
// Event delegation for todo items
document.querySelector("#todoList").addEventListener("click", (e) => {
const item = e.target.closest(".todo-item");
if (!item) return;
const id = parseInt(item.dataset.id);
if (e.target.matches("input[type='checkbox']")) {
this.toggleTodo(id);
} else if (e.target.matches(".delete-btn")) {
this.deleteTodo(id);
}
});
// Filter buttons
document.querySelector(".filters").addEventListener("click", (e) => {
if (e.target.matches("button[data-filter]")) {
this.setFilter(e.target.dataset.filter);
}
});
}
}
// Initialize the app
const app = new TodoApp();
Pro tip: This todo app demonstrates several important patterns: encapsulated state with private fields, event delegation for dynamic content, local storage persistence with error handling, and separation of data logic from rendering. These same patterns scale up to larger applications — frameworks like React and Vue use similar concepts of state management and declarative rendering.
Key Takeaways
- A todo app combines DOM manipulation, event handling, data management, and local storage persistence.
- Event delegation on the list container handles clicks on dynamically created todo items.
- Separating data operations (add, toggle, delete) from rendering makes the code maintainable and testable.
- Local storage with JSON serialization provides simple client-side data persistence across page reloads.
- This architecture (state + render cycle) is the foundation of modern frontend frameworks like React, Vue, and Svelte.
arrow_back Working with APIs
check_circle Lap Complete!