Overview
This project is a browser-based Todo App built using IndexedDB for data persistence. The app stores todos in the browser database so they remain available across reloads and browser sessions.
Why IndexedDB?
IndexedDB is a low-level client-side storage API for structured data, including objects and files. It is more powerful than localStorage and is well suited for apps that need:
- persistent data storage
- large amounts of data
- searchable and indexed records
- transactional updates
- structured objects with multiple fields
IndexedDB vs LocalStorage
| Feature | IndexedDB | localStorage |
|---|---|---|
| Storage size | Hundreds of MBs (browser-dependent) | ~5MB |
| Data type | Objects, arrays, binary blobs | Strings only |
| Performance | Optimized for many records and queries | Not optimized for many records |
| Querying | Indexes, cursors, object stores | No native querying |
| Asynchronous | Yes | No |
| Transaction support | Yes | No |
When to use IndexedDB
Use IndexedDB when your app requires more than simple key/value storage. It is ideal for offline apps, todo lists with categories, media, cache layers, and any client app with structured or large data.
When localStorage is enough
Use localStorage only for very small amounts of string data, simple settings, or feature flags.
App Features
This todo app demonstrates:
- creating and opening an IndexedDB database
- defining an object store called
todos - adding todo items with category, createdAt, and updatedAt fields
- querying todos by category through an index
- filtering todos by completion status and search text
- updating and deleting todos
- clearing all completed todos
- editing todo text and category
Code explanation
Database setup
The app opens IndexedDB using:
const request = indexedDB.open("TodoDatabase", 1);
-
TodoDatabaseis the database name. -
1is the version number.
The onupgradeneeded event creates the todos object store and adds indexes:
const store = db.createObjectStore("todos", { keyPath: "id", autoIncrement: true });
store.createIndex("by_category", "category", { unique: false });
store.createIndex("by_completed", "completed", { unique: false });
This stores each todo with a unique id and allows queries by category and completed status.
Adding todos
addTodo() reads the todo text and category from the form, then stores a record:
const request = objectStore.add({
text,
category,
completed: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
Querying and filtering
displayTodos() loads todos from IndexedDB and applies filters:
- category using the
by_categoryindex - status filter to show active or completed items
- search term matching on todo text
This shows how IndexedDB can return a subset of records efficiently.
Updating todos
toggleTodo(id) and editTodo(id) retrieve a todo by its id, modify fields, and save it back with put().
Deleting and clearing completed todos
-
deleteTodo(id)removes a single record. -
clearCompleted()uses a cursor to delete all completed records in a transaction.
How to use
- Open
index.htmlin a browser that supports IndexedDB. - Add a todo with a category.
- Filter by category or status.
- Edit or delete items as needed.
Notes
- IndexedDB operations are asynchronous, so the app uses event callbacks like
onsuccessandonerror. - The app stores structured todo objects, not just strings.
- The category index improves the app by allowing category filtering without scanning the entire store.
Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 700px;
margin: 0 auto;
padding: 20px;
}
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 10px 0;
padding: 10px;
background: #f8f8f8;
border-radius: 6px;
}
.todo-left {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.todo-text {
flex-grow: 1;
margin-left: 10px;
}
.todo-meta {
color: #555;
font-size: 0.9rem;
margin-left: 10px;
}
.completed {
text-decoration: line-through;
color: #888;
}
.add-button {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 14px;
cursor: pointer;
border-radius: 4px;
}
.delete-button,
.edit-button,
.clear-button {
color: white;
border: none;
padding: 6px 12px;
cursor: pointer;
border-radius: 4px;
}
.delete-button {
background-color: #f44336;
}
.edit-button {
background-color: #2196F3;
}
.clear-button {
background-color: #555;
}
button {
cursor: pointer;
}
input[type="text"],
select,
input[type="search"] {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
align-items: center;
}
.filters label {
display: flex;
align-items: center;
gap: 6px;
}
</style>
</head>
<body>
<h1>Todo App</h1>
<form id="todoForm" onsubmit="event.preventDefault(); addTodo();">
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
<input type="text" id="todoText" placeholder="Enter a new todo..." required>
<select id="todoCategory" required>
<option value="Personal">Personal</option>
<option value="Work">Work</option>
<option value="Shopping">Shopping</option>
<option value="Other">Other</option>
</select>
<button type="submit" class="add-button">Add Todo</button>
</div>
</form>
<div class="filters">
<label>
Category:
<select id="categoryFilter" onchange="displayTodos()">
<option value="all">All</option>
<option value="Personal">Personal</option>
<option value="Work">Work</option>
<option value="Shopping">Shopping</option>
<option value="Other">Other</option>
</select>
</label>
<label>
Status:
<select id="statusFilter" onchange="displayTodos()">
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
</label>
<input type="search" id="searchInput" placeholder="Search todos..." oninput="displayTodos()">
<button type="button" class="clear-button" onclick="clearCompleted()">Clear Completed</button>
</div>
<div id="todoList"></div>
<script>
let db;
const categories = ["Personal", "Work", "Shopping", "Other"];
const request = indexedDB.open("TodoDatabase", 1);
request.onerror = function(event) {
console.log("Error opening database:", event);
};
request.onsuccess = function(event) {
db = event.target.result;
displayTodos();
};
request.onupgradeneeded = function(event) {
db = event.target.result;
if (!db.objectStoreNames.contains("todos")) {
const store = db.createObjectStore("todos", { keyPath: "id", autoIncrement: true });
store.createIndex("by_category", "category", { unique: false });
store.createIndex("by_completed", "completed", { unique: false });
}
};
</script>
<script>
function displayTodos() {
const categoryFilter = document.getElementById("categoryFilter").value;
const statusFilter = document.getElementById("statusFilter").value;
const searchValue = document.getElementById("searchInput").value.trim().toLowerCase();
const transaction = db.transaction(["todos"], "readonly");
const objectStore = transaction.objectStore("todos");
let request;
if (categoryFilter !== "all") {
const index = objectStore.index("by_category");
request = index.getAll(categoryFilter);
} else {
request = objectStore.getAll();
}
request.onsuccess = function(event) {
let todos = event.target.result;
if (statusFilter !== "all") {
todos = todos.filter(todo => statusFilter === "completed" ? todo.completed : !todo.completed);
}
if (searchValue !== "") {
todos = todos.filter(todo => todo.text.toLowerCase().includes(searchValue));
}
let output = "<h2>Your Todos:</h2>";
if (todos.length === 0) {
output += "<p>No todos match the current filters.</p>";
} else {
todos.sort((a, b) => a.id - b.id);
todos.forEach(todo => {
const completedClass = todo.completed ? 'completed' : '';
output += `
<div class="todo-item">
<div class="todo-left">
<input type="checkbox" ${todo.completed ? 'checked' : ''} onchange="toggleTodo(${todo.id})">
<span class="todo-text ${completedClass}">${todo.text}</span>
<span class="todo-meta">${todo.category}</span>
</div>
<div>
<button type="button" class="edit-button" onclick="editTodo(${todo.id})">Edit</button>
<button type="button" class="delete-button" onclick="deleteTodo(${todo.id})">Delete</button>
</div>
</div>
`;
});
}
document.getElementById("todoList").innerHTML = output;
};
request.onerror = function(event) {
console.log("Error fetching todos:", event);
};
}
function addTodo() {
const text = document.getElementById("todoText").value.trim();
const category = document.getElementById("todoCategory").value;
if (text === "") return;
const transaction = db.transaction(["todos"], "readwrite");
const objectStore = transaction.objectStore("todos");
const request = objectStore.add({
text,
category,
completed: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
request.onsuccess = function() {
document.getElementById("todoForm").reset();
displayTodos();
};
request.onerror = function(event) {
console.log("Error adding todo:", event);
};
}
function toggleTodo(id) {
const transaction = db.transaction(["todos"], "readwrite");
const objectStore = transaction.objectStore("todos");
const getRequest = objectStore.get(id);
getRequest.onsuccess = function(event) {
const todo = event.target.result;
if (!todo) return;
todo.completed = !todo.completed;
todo.updatedAt = new Date().toISOString();
const updateRequest = objectStore.put(todo);
updateRequest.onsuccess = function() {
displayTodos();
};
};
}
function editTodo(id) {
const transaction = db.transaction(["todos"], "readwrite");
const objectStore = transaction.objectStore("todos");
const getRequest = objectStore.get(id);
getRequest.onsuccess = function(event) {
const todo = event.target.result;
if (!todo) return;
const newText = prompt("Edit todo text:", todo.text);
if (newText === null) return;
const newCategory = prompt("Edit category (Personal, Work, Shopping, Other):", todo.category);
if (newCategory === null) return;
todo.text = newText.trim() || todo.text;
todo.category = categories.includes(newCategory) ? newCategory : todo.category;
todo.updatedAt = new Date().toISOString();
const updateRequest = objectStore.put(todo);
updateRequest.onsuccess = function() {
displayTodos();
};
};
}
function deleteTodo(id) {
const transaction = db.transaction(["todos"], "readwrite");
const objectStore = transaction.objectStore("todos");
const request = objectStore.delete(id);
request.onsuccess = function() {
displayTodos();
};
request.onerror = function(event) {
console.log("Error deleting todo:", event);
};
}
function clearCompleted() {
const transaction = db.transaction(["todos"], "readwrite");
const objectStore = transaction.objectStore("todos");
const cursorRequest = objectStore.openCursor();
cursorRequest.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
if (cursor.value.completed) {
cursor.delete();
}
cursor.continue();
} else {
displayTodos();
}
};
cursorRequest.onerror = function(event) {
console.log("Error clearing completed todos:", event);
};
}
</script>
</body>
</html>
May this is useful for next application.
For more related type of post visit our company page.

Top comments (0)