Back to roadmaps vanilla-js Course

Project 1: Persistent Todo App

In this project, we will build a complete Todo application from scratch. We will cover DOM operations, form submission handling, event delegation for item removal, and state persistence using localStorage.

1. Project Features

  • Add new tasks.
  • Toggle task completion states (strike-through).
  • Delete tasks.
  • Auto-save tasks to localStorage (persists on page reloads).

2. The HTML Structure

Create a basic layout with an input form and an empty list:

<!-- index.html -->
<div class="todo-card">
  <h2>Task Manager</h2>

  <form id="todo-form">
    <input type="text" id="todo-input" placeholder="Add a new task..." required autocomplete="off">
    <button type="submit">Add</button>
  </form>

  <ul id="todo-list">
    <!-- Tasks will be injected dynamically -->
  </ul>
</div>

3. The JavaScript Implementation

Here is the step-by-step logic written in Vanilla JS:

// app.js

// 1. Core DOM Element Selection
const form = document.querySelector('#todo-form');
const input = document.querySelector('#todo-input');
const list = document.querySelector('#todo-list');

// 2. Load Existing Tasks or Initialize Empty Array
let tasks = JSON.parse(localStorage.getItem('todos')) || [];

// 3. Render Tasks to DOM
function renderTodos() {
  list.innerHTML = ""; // Clear existing list to prevent duplication

  tasks.forEach((task, index) => {
    const li = document.createElement('li');
    li.className = `todo-item ${task.completed ? 'completed' : ''}`;

    // Inject HTML layout containing checkboxes and delete buttons
    li.innerHTML = `
      <label>
        <input type="checkbox" data-index="${index}" ${task.completed ? 'checked' : ''}>
        <span>${task.text}</span>
      </label>
      <button class="delete-btn" data-index="${index}">Delete</button>
    `;
    list.appendChild(li);
  });
}

// 4. Save Task Array to LocalStorage
function saveTodos() {
  localStorage.setItem('todos', JSON.stringify(tasks));
}

// 5. Handling Form Submission (Adding Task)
form.addEventListener('submit', (e) => {
  e.preventDefault(); // Stop standard page reload

  const text = input.value.trim();
  if (text) {
    tasks.push({ text: text, completed: false });
    input.value = ""; // Clear input text

    saveTodos();
    renderTodos();
  }
});

// 6. Interactive Actions via Event Delegation
list.addEventListener('click', (e) => {
  // A. Handle Completion Checkbox clicks
  if (e.target.type === 'checkbox') {
    const index = e.target.getAttribute('data-index');
    tasks[index].completed = e.target.checked;
    saveTodos();
    renderTodos();
  }

  // B. Handle Delete Button clicks
  if (e.target.classList.contains('delete-btn')) {
    const index = e.target.getAttribute('data-index');
    tasks.splice(index, 1); // Remove item from array
    saveTodos();
    renderTodos();
  }
});

// 7. Initial Render on Load
renderTodos();

4. Basic CSS Styling

To make it look clean:

#todo-list {
  list-style: none;
  padding: 0;
}

.todo-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #ddd;
}

.todo-item.completed span {
  text-decoration: line-through;
  color: #888;
}

.delete-btn {
  background-color: #ef4444;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  padding: 4px 8px;
}

In the next project, we will build an interactive Weather Dashboard using APIs and fetch().

Published on Last updated: