DEV Community

forgeora
forgeora

Posted on

Most useful things for javascript developers

 # IndexedDB Todo App

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);
Enter fullscreen mode Exit fullscreen mode
  • TodoDatabase is the database name.
  • 1 is 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 });
Enter fullscreen mode Exit fullscreen mode

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()
});
Enter fullscreen mode Exit fullscreen mode

Querying and filtering

displayTodos() loads todos from IndexedDB and applies filters:

  • category using the by_category index
  • 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

  1. Open index.html in a browser that supports IndexedDB.
  2. Add a todo with a category.
  3. Filter by category or status.
  4. Edit or delete items as needed.

Notes

  • IndexedDB operations are asynchronous, so the app uses event callbacks like onsuccess and onerror.
  • 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>
Enter fullscreen mode Exit fullscreen mode

May this is useful for next application.

For more related type of post visit our company page.

Forgeora

Welcome to Forgeora, your go-to platform for image tools and services.

favicon forgeora.com

Top comments (0)