Building a Todo.txt web application with localstorage and service workers
The primary function of this web app is to provide a clean interface for interacting with tasks formatted according to the todo.txt specification. A typical task might look like this:
(A) Call Mom @Home +Family due:2023-12-31
My application allows users to:
- Add new tasks with optional priority, projects (
+Project), and contexts (@Context). - Mark tasks as complete or incomplete.
- Edit existing tasks.
- Delete tasks.
- Filter the task list based on priority, project, or context.
- Import multiple tasks by pasting plain text.
- Copy all tasks to the clipboard.
User interface and structure
I structured the user interface using HTML and styled it with Bootstrap 5, applying custom CSS for specific elements like the task list and dropdowns to create a consistent theme.
The main UI components are:
- Input fields and dropdowns for adding/editing tasks.
- Action buttons for filtering, copying, and importing.
- The dynamic list where tasks are displayed.
<div class="add-todo-section mb-3">
<div class="input-group mb-2">
<select class="form-select" id="prioritySelect" aria-label="Priority">...</select>
<select class="form-select" id="projectSelect" aria-label="Project">...</select>
<select class="form-select" id="contextSelect" aria-label="Context">...</select>
</div>
<div class="input-group mb-2">
<input type="text" class="form-control" placeholder="Enter todo item" id="todoInput">
<button class="btn btn-primary" type="button" id="addButton">Add Todo</button>
</div>
<div class="btn-group mt-2">
<button class="btn btn-primary" type="button" id="filterButton">Filter</button>
<button class="btn btn-primary ms-2" type="button" id="copyAllButton">Copy All</button>
<button class="btn btn-primary ms-2" type="button" id="importButton">Import</button>
</div>
<textarea class="form-control mt-2" id="importTextarea" rows="5" style="display:none;" placeholder="Paste todo items here, one per line"></textarea>
</div>
<ul class="list-group jsTodoTxt" id="todoList">
</ul>
</div>
JavaScript implementation
The application logic is handled by JavaScript, modularized into different files for better organization (storage, UI updates, event handling, etc.). I used jQuery for DOM manipulation and the jstodotxt.min.js library to parse and manipulate todo.txt strings.
// Example: Parsing a task string
import jsTodoTxt from '/assets/js/lib/jstodotxt.min.js';
const taskString = "(B) Review report +Work @Office";
const item = new jsTodoTxt.Item(taskString);
console.log(item.priority()); // Output: B
console.log(item.projects()); // Output: ['Work']
console.log(item.contexts()); // Output: ['Office']
Data Storage
To persist tasks between sessions without requiring a backend server, I utilized the browser’s localStorage. Tasks are stored as a JSON array of objects, where each object contains a unique ID generated by the application and the raw todo.txt string.
// Simplified storage structure in localStorage under the key 'todos'
[
{"id": "uid1", "text": "(A) First task +Project1"},
{"id": "uid2", "text": "x 2023-11-15 Completed task @ContextA"},
{"id": "uid3", "text": "Another task"}
]
This approach allows the application to load tasks quickly on startup and save changes instantly.
Sorting logic
Tasks are displayed in a specific order: incomplete tasks appear before completed tasks. Within the incomplete tasks, sorting is done by priority, from highest (A) to lowest (Z), followed by tasks without priority. Completed tasks are shown afterwards. If we assign numerical values to priorities, P_{(A)} < P_{(B)} < \dots < P_{(Z)}, the sort order prioritizes lower numerical values for incomplete tasks.
Offline capability via service worker
A key feature is offline support, implemented using a service worker. On the first visit, the service worker caches essential application assets (HTML, CSS, JavaScript files, icons). Subsequent visits load the application from the cache first, falling back to the network if necessary. This ensures the app remains functional even without an internet connection.
// Simplified Fetch Event Handler in service-worker.js
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
// Return cached response if found, otherwise fetch from network
return cachedResponse || fetch(event.request);
})
);
});
The service worker also includes logic to update the cache when a new version of the application is detected, ensuring users benefit from updates while maintaining offline access.
You can access the full source code, at the GitHub repository here.
For more insights into this topic, you can find the details here.