# How to Implement a Todo List with Add, Edit, Delete, and Filter Functionality in Vanilla JavaScript

> Build a vanilla JavaScript todo list with add edit delete and filter features using native DOM APIs Clone the jisan mia dom projects repository for a dependency free solution

- Repository: [Jisan Mia/dom-projects](https://github.com/jisan-mia/dom-projects)
- Tags: how-to-guide
- Published: 2026-03-04

---

**You can build a fully functional todo application using only native DOM APIs and `localStorage` by cloning the template-based architecture from the `jisan-mia/dom-projects` repository, which implements add, edit, delete, toggle, and filter features without any external dependencies.**

The **Dom-Projects** repository provides a production-ready reference implementation located in `projects/advanced-todo/`. This version demonstrates how to manage state persistence, manipulate the DOM using HTML templates, and handle user interactions through event delegation. Below is a complete technical breakdown of the implementation, including file paths, function signatures, and runnable code examples.

## Project Structure and File Overview

The application is contained within three files in the `projects/advanced-todo/` directory:

- **[`index.html`](https://github.com/jisan-mia/dom-projects/blob/main/index.html)** – Defines the layout, filter controls, input form, and an HTML `<template>` element for list items.
- **[`script.js`](https://github.com/jisan-mia/dom-projects/blob/main/script.js)** – Contains all business logic: state management, rendering, and event handling (lines 1–125).
- **[`style.css`](https://github.com/jisan-mia/dom-projects/blob/main/style.css)** – Provides minimal styling for layout, checkboxes, and button states.

## Setting Up the HTML Template and Layout

The markup uses a `<template>` element to define the structure of a single todo item. This avoids manual string concatenation and ensures consistent DOM nodes.

Key elements in [`index.html`](https://github.com/jisan-mia/dom-projects/blob/main/index.html):

- **List container**: `<ul class="list"></ul>` receives cloned template instances.
- **Filter bar**: Buttons with `data-role` attributes (`all`, `active`, `completed`) control visibility.
- **Input form**: A `.todo-form` with a text input and submit button handles new items and edits.
- **Template**: The `#list-item-template` contains a checkbox (`data-list-item-checkbox`), text span (`data-list-item-text`), and action buttons.

## Implementing Data Persistence with localStorage

State is synchronized to `localStorage` under the key `ADVANCE_TODO_LIST-todos` (lines 63–71 of [`script.js`](https://github.com/jisan-mia/dom-projects/blob/main/script.js)).

```javascript
const LOCAL_STORAGE_KEY = "ADVANCE_TODO_LIST-todos";
let todoList = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [];

function saveTodo() {
  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList));
}

```

- **`getTodo()`** (implicitly handled by the initial parse) retrieves the JSON array or returns an empty array.
- **`saveTodo()`** serializes the current `todoList` array back to storage after every mutation.

## Rendering Todos from the DOM Template

The rendering engine clones the HTML template for each todo object.

```javascript
function renderTodo(todo) {
  const tmpl = document.querySelector('#list-item-template').content.cloneNode(true);
  const li = tmpl.querySelector('.list-item');
  li.dataset.todoId = todo.id;
  tmpl.querySelector('[data-list-item-text]').textContent = todo.name;
  tmpl.querySelector('[data-list-item-checkbox]').checked = todo.isCompleted;
  document.querySelector('.list').appendChild(tmpl);
}

function renderAllTodo() {
  todoList.forEach(renderTodo);
}

```

- **`renderTodo(todo)`** (lines 76–81) populates the template and appends it to the `.list` container.
- **`renderAllTodo()`** (lines 12–14) iterates over the loaded array on initial page load.

## Adding New Todos

The form submit handler creates a new todo object, pushes it to the state, and renders it immediately (lines 27–48).

```javascript
document.querySelector('.todo-form').addEventListener('submit', e => {
  e.preventDefault();
  const input = document.getElementById('todo-input');
  if (!input.value) return;
  
  const newTodo = { 
    id: Date.now(), 
    name: input.value, 
    isCompleted: false 
  };
  
  todoList.push(newTodo);
  renderTodo(newTodo);
  saveTodo();
  input.value = '';
});

```

The `id` is generated using `Date.now()` to ensure uniqueness.

## Editing Existing Todos

Editing uses a two-phase flow: selecting the item and updating the state.

```javascript
let selectedEditTodo = null;

// Phase 1: Select for editing
document.querySelector('.list').addEventListener('click', e => {
  if (!e.target.matches('.edit-todo')) return;
  const li = e.target.closest('.list-item');
  selectedEditTodo = todoList.find(t => t.id == li.dataset.todoId);
  document.getElementById('todo-input').value = selectedEditTodo.name;
});

// Phase 2: Submit edit
function editTodo(newName) {
  todoList = todoList.map(t => 
    t.id === selectedEditTodo.id ? { ...t, name: newName } : t
  );
  const li = document.querySelector(`[data-todo-id="${selectedEditTodo.id}"]`);
  li.querySelector('[data-list-item-text]').textContent = newName;
  saveTodo();
  selectedEditTodo = null;
  document.getElementById('todo-input').value = '';
}

```

The `editTodo` function (referenced in lines 55–74 of [`script.js`](https://github.com/jisan-mia/dom-projects/blob/main/script.js)) updates the array immutably using `map`, mutates the specific DOM node to avoid a full re-render, and clears the edit state.

## Deleting and Toggling Completion

Event delegation handles both deletion and completion toggling without binding listeners to dynamic elements.

**Toggle Completion** (lines 14–27):

```javascript
document.querySelector('.list').addEventListener('change', e => {
  if (!e.target.matches('[data-list-item-checkbox]')) return;
  const li = e.target.closest('.list-item');
  const todo = todoList.find(t => t.id == li.dataset.todoId);
  todo.isCompleted = e.target.checked;
  saveTodo();
});

```

**Delete** (lines 29–39):

```javascript
document.querySelector('.list').addEventListener('click', e => {
  if (!e.target.matches('.delete-todo')) return;
  const li = e.target.closest('.list-item');
  todoList = todoList.filter(t => t.id != li.dataset.todoId);
  li.remove();
  saveTodo();
});

```

Both handlers use `closest('.list-item')` to traverse from the event target to the parent node containing the `data-todo-id` attribute.

## Filtering Todos by Status

The filter implementation re-renders the list based on the `activeFilter` state (lines 85–125).

```javascript
let activeFilter = 'all';

document.querySelector('.filter-section').addEventListener('click', e => {
  if (!e.target.matches('button')) return;
  activeFilter = e.target.dataset.role; // 'all', 'active', or 'completed'
  
  const list = document.querySelector('.list');
  list.innerHTML = '';
  
  const filtered = activeFilter === 'all' 
    ? todoList 
    : activeFilter === 'active' 
      ? todoList.filter(t => !t.isCompleted) 
      : todoList.filter(t => t.isCompleted);
      
  filtered.forEach(renderTodo);
});

```

This approach clears the DOM and re-clones the template for the filtered subset, ensuring the UI stays synchronized with the `activeFilter` variable.

## Summary

- **Architecture**: The `jisan-mia/dom-projects` repository uses a template-based rendering system with `localStorage` persistence, eliminating external dependencies.
- **State Management**: The `todoList` array serves as the single source of truth, synchronized to `ADVANCE_TODO_LIST-todos` in `localStorage` via `saveTodo()`.
- **CRUD Operations**: 
  - **Create**: Form submission generates a new object with `Date.now()` ID.
  - **Read**: `renderTodo()` clones the HTML template and populates data attributes.
  - **Update**: `editTodo()` uses immutable array mapping and targeted DOM updates.
  - **Delete**: Event delegation removes items from both the array and DOM.
- **Filtering**: The `activeFilter` state drives conditional array filtering before re-rendering the list container.

## Frequently Asked Questions

### How does the todo list persist data between page refreshes?

The application stores the `todoList` array in the browser's `localStorage` under the key `ADVANCE_TODO_LIST-todos`. The `saveTodo()` function (lines 63–71 of [`script.js`](https://github.com/jisan-mia/dom-projects/blob/main/script.js)) serializes the array to JSON on every mutation, while the initial load uses `JSON.parse()` to hydrate the state or defaults to an empty array.

### What is the purpose of the HTML template element in this implementation?

The `<template id="list-item-template">` in [`index.html`](https://github.com/jisan-mia/dom-projects/blob/main/index.html) defines the structure of a single todo item (checkbox, text span, and action buttons) without rendering it immediately. The `renderTodo()` function clones this template's content using `document.querySelector('#list-item-template').content.cloneNode(true)`, then populates the `data-todo-id`, text content, and checkbox state before appending it to the live DOM. This approach keeps markup DRY and avoids string concatenation.

### How does the filtering mechanism work without reloading the page?

Filtering is handled entirely client-side by the `showFilteredTodos()` logic (lines 85–125 of [`script.js`](https://github.com/jisan-mia/dom-projects/blob/main/script.js)). When a filter button is clicked, the `activeFilter` variable updates to `all`, `active`, or `completed`. The handler clears the `.list` container's innerHTML, filters the `todoList` array based on the `isCompleted` property, and re-invokes `renderTodo()` for each matching item. This re-renders only the filtered subset instantly without a server request.

### Can multiple todo items be edited simultaneously in this implementation?

No, the implementation restricts editing to one item at a time using the `selectedEditTodo` variable. When the **Edit** button is clicked, the corresponding todo object is stored in `selectedEditTodo` and its text populates the input field. The form submit handler checks if `selectedEditTodo` exists; if so, it calls `editTodo()` and clears the variable. This prevents race conditions and ensures the UI only shows one item in edit mode at a time.