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!