31. JavaScript Practical Project: Task Tracker
What you will build
A task tracker: add tasks, tick them off, delete them, filter by status, and see a live count. It uses variables, arrays of objects, functions, the DOM, events, delegation, and safe text handling.
Before you start
- Build it step by step in the Try it Yourself editor, or save the finished file as
tracker.htmlon your computer. - Run the code after every step and check what happens.
The plan
- State: one array holds all the tasks. Each task is an object
{ id, text, done }. - Render: one function draws the whole list from the state.
- Events: clicking changes the state, then calls render again.
The golden rule: change the data, then redraw. Never try to keep the page and the data in step by hand.
Step 1: The HTML
Step 1: HTML
<h2>Task tracker</h2>
<form id="form">
<label for="task">New task</label>
<input id="task" type="text" autocomplete="off">
<button type="submit">Add</button>
</form>
<p id="message" role="status"></p>
<div id="filters">
<button type="button" data-filter="all" aria-pressed="true">All</button>
<button type="button" data-filter="open" aria-pressed="false">To do</button>
<button type="button" data-filter="done" aria-pressed="false">Done</button>
</div>
<ul id="list"></ul>
Step 2: The state
Step 2: JavaScript
let tasks = [
{ id: 1, text: "Finish the JavaScript course", done: false },
{ id: 2, text: "Build a project", done: false }
];
let nextId = 3;
let filter = "all";
Step 3: The render function
It builds list items with createElement and textContent, so text typed by a visitor can never run as code.
Step 3: JavaScript
function visibleTasks() {
if (filter === "open") { return tasks.filter((t) => !t.done); }
if (filter === "done") { return tasks.filter((t) => t.done); }
return tasks;
}
function render() {
const list = document.getElementById("list");
list.textContent = "";
visibleTasks().forEach(function (task) {
const li = document.createElement("li");
li.dataset.id = task.id;
const check = document.createElement("input");
check.type = "checkbox";
check.checked = task.done;
check.id = "task-" + task.id;
const label = document.createElement("label");
label.htmlFor = check.id;
label.textContent = task.text;
const del = document.createElement("button");
del.type = "button";
del.textContent = "Delete";
del.setAttribute("aria-label", "Delete task: " + task.text);
li.append(check, " ", label, " ", del);
list.appendChild(li);
});
const left = tasks.filter((t) => !t.done).length;
document.getElementById("message").textContent =
left + " of " + tasks.length + " tasks left";
}
Step 4: The events
Step 4: JavaScript
// Add a task
document.getElementById("form").addEventListener("submit", function (event) {
event.preventDefault();
const input = document.getElementById("task");
const text = input.value.trim();
if (text === "") { input.focus(); return; }
tasks.push({ id: nextId++, text: text, done: false });
input.value = "";
input.focus();
render();
});
// Tick or delete: one listener on the list (event delegation)
document.getElementById("list").addEventListener("click", function (event) {
const li = event.target.closest("li");
if (!li) { return; }
const id = Number(li.dataset.id);
if (event.target.matches("button")) {
tasks = tasks.filter((t) => t.id !== id);
render();
} else if (event.target.matches("input[type=checkbox]")) {
const task = tasks.find((t) => t.id === id);
task.done = event.target.checked;
render();
}
});
// Filters
document.getElementById("filters").addEventListener("click", function (event) {
const button = event.target.closest("button");
if (!button) { return; }
filter = button.dataset.filter;
document.querySelectorAll("#filters button").forEach(function (b) {
b.setAttribute("aria-pressed", b === button ? "true" : "false");
});
render();
});
render();
The complete file
Press Try it Yourself to run the finished tracker.
Example: complete page
<style>
body { font-family: system-ui, sans-serif; max-width: 520px; margin: 0 auto; padding: 12px; color: #17345c; }
form { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
input[type="text"] { flex: 1 1 160px; padding: 8px; border: 1px solid #cfe6ff; border-radius: 6px; font: inherit; }
button { padding: 8px 14px; border: 1px solid #0d6efd; background: #0d6efd; color: #fff; border-radius: 6px; font: inherit; cursor: pointer; }
#filters { display: flex; gap: 6px; margin: 10px 0; }
#filters button { background: #fff; color: #0d6efd; }
#filters button[aria-pressed="true"] { background: #082b59; color: #fff; border-color: #082b59; }
ul { list-style: none; padding: 0; }
li { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #cfe6ff; }
li label { flex: 1; }
li input:checked + label { text-decoration: line-through; color: #4f6b8f; }
li button { background: #fff; color: #b02a37; border-color: #b02a37; padding: 4px 10px; }
:focus-visible { outline: 3px solid #0d6efd; outline-offset: 2px; }
</style>
<h2>Task tracker</h2>
<form id="form">
<label for="task">New task</label>
<input id="task" type="text" autocomplete="off">
<button type="submit">Add</button>
</form>
<p id="message" role="status"></p>
<div id="filters">
<button type="button" data-filter="all" aria-pressed="true">All</button>
<button type="button" data-filter="open" aria-pressed="false">To do</button>
<button type="button" data-filter="done" aria-pressed="false">Done</button>
</div>
<ul id="list"></ul>
<script>
let tasks = [
{ id: 1, text: "Finish the JavaScript course", done: false },
{ id: 2, text: "Build a project", done: false }
];
let nextId = 3;
let filter = "all";
function visibleTasks() {
if (filter === "open") { return tasks.filter((t) => !t.done); }
if (filter === "done") { return tasks.filter((t) => t.done); }
return tasks;
}
function render() {
const list = document.getElementById("list");
list.textContent = "";
visibleTasks().forEach(function (task) {
const li = document.createElement("li");
li.dataset.id = task.id;
const check = document.createElement("input");
check.type = "checkbox";
check.checked = task.done;
check.id = "task-" + task.id;
const label = document.createElement("label");
label.htmlFor = check.id;
label.textContent = task.text;
const del = document.createElement("button");
del.type = "button";
del.textContent = "Delete";
del.setAttribute("aria-label", "Delete task: " + task.text);
li.append(check, " ", label, " ", del);
list.appendChild(li);
});
const left = tasks.filter((t) => !t.done).length;
document.getElementById("message").textContent =
left + " of " + tasks.length + " tasks left";
}
document.getElementById("form").addEventListener("submit", function (event) {
event.preventDefault();
const input = document.getElementById("task");
const text = input.value.trim();
if (text === "") { input.focus(); return; }
tasks.push({ id: nextId++, text: text, done: false });
input.value = "";
input.focus();
render();
});
document.getElementById("list").addEventListener("click", function (event) {
const li = event.target.closest("li");
if (!li) { return; }
const id = Number(li.dataset.id);
if (event.target.matches("button")) {
tasks = tasks.filter((t) => t.id !== id);
render();
} else if (event.target.matches("input[type=checkbox]")) {
const task = tasks.find((t) => t.id === id);
task.done = event.target.checked;
render();
}
});
document.getElementById("filters").addEventListener("click", function (event) {
const button = event.target.closest("button");
if (!button) { return; }
filter = button.dataset.filter;
document.querySelectorAll("#filters button").forEach(function (b) {
b.setAttribute("aria-pressed", b === button ? "true" : "false");
});
render();
});
render();
</script>
How it works
- All data lives in
tasks. The page is only a picture of it. render()clears the list and draws it again from the data and the current filter.- Every action changes the data first, then calls
render(). - One click listener on the list handles every checkbox and Delete button, using
event.target. It also works for tasks added later. - Task text goes in with
textContent, so typing<b>shows the characters instead of running them.
Saving tasks on a real page
The editor blocks localStorage, but on your own website you can add saving with a few lines:
Syntax
// 1. Replace the starting tasks with saved ones
let tasks = JSON.parse(localStorage.getItem("tasks")) || [];
let nextId = tasks.reduce((max, t) => Math.max(max, t.id), 0) + 1;
// 2. At the end of render(), save them
localStorage.setItem("tasks", JSON.stringify(tasks));
Wrap both in try...catch, in case storage is unavailable. Remember that this data stays only in that one browser.
Challenges
- Add a Clear completed button.
- Let the visitor edit a task by double-clicking it.
- Add a due date to each task and sort by it.
- Add saving with
localStorageandtry...catch. - Split the code into modules:
state.js,render.js,main.js.
Common mistakes
- Changing the page without changing the data, so they get out of step.
- Using
innerHTMLwith the task text. - Forgetting
preventDefault()on the form. - Forgetting to call
render()after changing the data. - Duplicate task ids, which break the delete and tick logic.
Summary
- You built a complete interactive app with state, rendering, and events.
- The rule “change the data, then redraw” keeps apps reliable.
- You used safe text handling, delegation, filters, and accessible controls.