Modern JavaScript and a project · Lesson 45 of 45
Project: a complete to-do app
Use everything you learned in a real app that saves its data.
15 minutes2 games
This app uses the DOM, events, arrays (map, filter), objects, localStorage, event delegation and the data → render idea. Read it line by line, then try the challenges below.
HTML
<style>
body { font-family: system-ui; max-width: 420px }
form { display: flex; gap: 6px }
input { flex: 1; padding: 8px }
li { display: flex; gap: 8px; align-items: center; padding: 6px 0 }
li.done span { text-decoration: line-through; color: #999 }
.filters button.active { font-weight: 700 }
</style>
<h2>My tasks</h2>
<form id="form"><input id="text" placeholder="New task" required><button>Add</button></form>
<p class="filters">
<button data-filter="all" class="active">All</button>
<button data-filter="open">Open</button>
<button data-filter="done">Done</button>
</p>
<ul id="list"></ul>
<p id="count"></p>
<script>
let tasks = []
let filter = "all"
try { tasks = JSON.parse(localStorage.getItem("tasks")) ?? [] } catch {}
const save = () => { try { localStorage.setItem("tasks", JSON.stringify(tasks)) } catch {} }
function render() {
const visible = tasks.filter((t) => filter === "all" || (filter === "done" ? t.done : !t.done))
const list = document.querySelector("#list")
list.replaceChildren(...visible.map((t) => {
const li = document.createElement("li")
li.dataset.id = t.id
li.className = t.done ? "done" : ""
li.innerHTML = '<input type="checkbox"> <span></span> <button data-action="delete">✕</button>'
li.querySelector("input").checked = t.done
li.querySelector("span").textContent = t.text
return li
}))
const open = tasks.filter((t) => !t.done).length
document.querySelector("#count").textContent = open + " task(s) left"
}
document.querySelector("#form").addEventListener("submit", (e) => {
e.preventDefault()
const input = document.querySelector("#text")
tasks.push({ id: Date.now(), text: input.value.trim(), done: false })
input.value = ""
save(); render()
})
document.querySelector("#list").addEventListener("click", (e) => {
const id = Number(e.target.closest("li")?.dataset.id)
if (e.target.matches('input[type="checkbox"]')) {
const task = tasks.find((t) => t.id === id)
task.done = !task.done
} else if (e.target.dataset.action === "delete") {
tasks = tasks.filter((t) => t.id !== id)
} else return
save(); render()
})
document.querySelector(".filters").addEventListener("click", (e) => {
if (!e.target.dataset.filter) return
filter = e.target.dataset.filter
document.querySelectorAll(".filters button").forEach((b) => b.classList.toggle("active", b === e.target))
render()
})
render()
</script>Add tasks, tick them, filter them and delete them.
Quick question
Why one listener on #list instead of one per checkbox?
Exercise
Logic challenge: write summary(tasks) returning text like 2 done, 1 open.
Expected output: 2 done, 1 open
JavaScript
const tasks = [
{ text: "Read", done: true },
{ text: "Code", done: false },
{ text: "Sleep", done: true },
]
Learn more from
Played 0 of 2 games
Sign in to save your progress and earn XP.