Практичний проєкт -- Todo List
JavaScript Todo List — блокнот зі списком справ де завдання відмічені як виконані
Це фінальний урок Block 5. Ми застосуємо все, що вивчили: DOM-селектори, маніпуляцію DOM, події, делегування подій, форми та localStorage. Крок за кроком побудуємо повноцінний Todo List.
Todo List -- класичний проєкт для вивчення будь-якого фреймворку або мови. Він містить усі базові операції: створення, читання, оновлення та видалення (CRUD). Якщо ти зможеш побудувати Todo List -- ти зможеш побудувати щось більше.
Що ми побудуємо
Наш додаток буде вміти:
- Додавати нові задачі через форму
- Позначати задачі як виконані (toggle)
- Видаляти задачі
- Фільтрувати: всі / активні / виконані
- Зберігати дані у localStorage (дані не зникнуть після перезавантаження)
Крок 1: HTML-структура
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo List</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="todo-app">
<h1>Todo List</h1>
<form id="todo-form">
<input
type="text"
id="todo-input"
placeholder="Що потрібно зробити?"
required
/>
<button type="submit">Додати</button>
</form>
<div class="filters">
<button class="filter-btn active" data-filter="all">Всі</button>
<button class="filter-btn" data-filter="active">Активні</button>
<button class="filter-btn" data-filter="completed">Виконані</button>
</div>
<ul id="todo-list"></ul>
<p class="counter">
Залишилось: <span id="todo-count">0</span>
</p>
</div>
<script src="app.js"></script>
</body>
</html>
Крок 2: CSS (базові стилі)
Стилі не є головною темою, але допоможуть бачити результат:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
background: #f0f0f0;
display: flex;
justify-content: center;
padding: 40px 20px;
}
.todo-app {
background: white;
border-radius: 12px;
padding: 30px;
width: 100%;
max-width: 500px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 20px;
}
#todo-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#todo-input {
flex: 1;
padding: 10px 14px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
}
#todo-form button {
padding: 10px 20px;
background: #3182ce;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
}
.filters {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.filter-btn {
padding: 6px 14px;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
cursor: pointer;
}
.filter-btn.active {
background: #3182ce;
color: white;
border-color: #3182ce;
}
#todo-list {
list-style: none;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-bottom: 1px solid #eee;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-text {
flex: 1;
cursor: pointer;
}
.delete-btn {
background: none;
border: none;
color: #e53e3e;
cursor: pointer;
font-size: 18px;
}
.counter {
margin-top: 16px;
text-align: center;
color: #666;
}
Крок 3: JavaScript -- стан додатку
Починаємо з масиву задач -- це наш "стан" (state). Кожна задача -- об'єкт з id, текстом та статусом:
// Стан додатку
let todos = [];
let currentFilter = "all";
Зверни увагу на підхід: ми зберігаємо дані в масиві JavaScript, а DOM оновлюємо окремою функцією render(). Це важливий патерн -- дані окремо, відображення окремо. Він стане основою React у Block 8.
Крок 4: Функція render
Функція render() бере масив todos, фільтрує за поточним фільтром та відображає у DOM:
const todoList = document.querySelector("#todo-list");
const todoCount = document.querySelector("#todo-count");
function render() {
// Фільтруємо задачі
const filtered = todos.filter((todo) => {
if (currentFilter === "active") return !todo.completed;
if (currentFilter === "completed") return todo.completed;
return true; // "all"
});
// Генеруємо HTML
todoList.innerHTML = filtered
.map(
(todo) => `
<li class="todo-item ${todo.completed ? "completed" : ""}" data-id="${todo.id}">
<input type="checkbox" ${todo.completed ? "checked" : ""} />
<span class="todo-text">${todo.text}</span>
<button class="delete-btn">×</button>
</li>
`
)
.join("");
// Оновлюємо лічильник активних задач
const activeCount = todos.filter((t) => !t.completed).length;
todoCount.textContent = activeCount;
}
Крок 5: Додавання задачі
Обробляємо submit форми -- створюємо нову задачу та додаємо до масиву:
const form = document.querySelector("#todo-form");
const input = document.querySelector("#todo-input");
form.addEventListener("submit", (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
const newTodo = {
id: Date.now(), // унікальний id на основі часу
text: text,
completed: false,
};
todos.push(newTodo);
input.value = ""; // очищаємо поле
saveTodos(); // зберігаємо в localStorage
render(); // оновлюємо DOM
});
Крок 6: Toggle та Delete через делегування подій
Замість додавати обробник на кожну кнопку, використовуємо делегування подій (пам'ятаєш з уроку 5.4?):
todoList.addEventListener("click", (event) => {
const item = event.target.closest(".todo-item");
if (!item) return;
const id = Number(item.dataset.id);
// Toggle complete -- клік на checkbox або текст
if (
event.target.matches("input[type='checkbox']") ||
event.target.matches(".todo-text")
) {
const todo = todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
saveTodos();
render();
}
}
// Delete -- клік на кнопку видалення
if (event.target.matches(".delete-btn")) {
todos = todos.filter((t) => t.id !== id);
saveTodos();
render();
}
});
Ми шукаємо задачу за id з data-id атрибуту. dataset.id повертає рядок, тому конвертуємо його через Number(). Без цього порівняння === не спрацює, бо "123" !== 123.
Крок 7: Фільтрація
Обробляємо кліки по кнопках фільтрів:
const filtersContainer = document.querySelector(".filters");
filtersContainer.addEventListener("click", (event) => {
if (!event.target.matches(".filter-btn")) return;
// Оновлюємо активну кнопку
filtersContainer.querySelector(".active").classList.remove("active");
event.target.classList.add("active");
// Оновлюємо фільтр і перемальовуємо
currentFilter = event.target.dataset.filter;
render();
});
Крок 8: localStorage -- збереження та завантаження
Фінальний крок -- дані зберігаються у localStorage і відновлюються при завантаженні сторінки:
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
}
function loadTodos() {
const saved = localStorage.getItem("todos");
todos = saved ? JSON.parse(saved) : [];
}
Викликаємо loadTodos() при старті і робимо перший рендер:
// Ініціалізація при завантаженні
loadTodos();
render();
Повний код app.js
Ось весь JavaScript разом:
// === Стан ===
let todos = [];
let currentFilter = "all";
// === DOM-елементи ===
const form = document.querySelector("#todo-form");
const input = document.querySelector("#todo-input");
const todoList = document.querySelector("#todo-list");
const todoCount = document.querySelector("#todo-count");
const filtersContainer = document.querySelector(".filters");
// === Render ===
function render() {
const filtered = todos.filter((todo) => {
if (currentFilter === "active") return !todo.completed;
if (currentFilter === "completed") return todo.completed;
return true;
});
todoList.innerHTML = filtered
.map(
(todo) => `
<li class="todo-item ${todo.completed ? "completed" : ""}" data-id="${todo.id}">
<input type="checkbox" ${todo.completed ? "checked" : ""} />
<span class="todo-text">${todo.text}</span>
<button class="delete-btn">×</button>
</li>
`
)
.join("");
const activeCount = todos.filter((t) => !t.completed).length;
todoCount.textContent = activeCount;
}
// === localStorage ===
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
}
function loadTodos() {
const saved = localStorage.getItem("todos");
todos = saved ? JSON.parse(saved) : [];
}
// === Додавання ===
form.addEventListener("submit", (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
todos.push({
id: Date.now(),
text: text,
completed: false,
});
input.value = "";
saveTodos();
render();
});
// === Toggle та Delete (делегування) ===
todoList.addEventListener("click", (event) => {
const item = event.target.closest(".todo-item");
if (!item) return;
const id = Number(item.dataset.id);
if (
event.target.matches("input[type='checkbox']") ||
event.target.matches(".todo-text")
) {
const todo = todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
saveTodos();
render();
}
}
if (event.target.matches(".delete-btn")) {
todos = todos.filter((t) => t.id !== id);
saveTodos();
render();
}
});
// === Фільтри ===
filtersContainer.addEventListener("click", (event) => {
if (!event.target.matches(".filter-btn")) return;
filtersContainer.querySelector(".active").classList.remove("active");
event.target.classList.add("active");
currentFilter = event.target.dataset.filter;
render();
});
// === Старт ===
loadTodos();
render();
Ідеї для розширення
Якщо хочеш попрактикуватися більше, спробуй додати:
- Редагування -- подвійний клік на текст задачі перетворює його на input
- Очистити виконані -- кнопка, яка видаляє всі completed задачі
- Дедлайн -- додати поле дати та підсвічувати прострочені задачі
- Drag & Drop -- перетягування для зміни порядку задач
- Категорії -- теги або групи для задач (робота, навчання, особисте)
Не намагайся реалізувати все одразу. Вибери одну ідею, реалізуй її, потім переходь до наступної. Саме так працюють професійні розробники -- маленькими ітераціями.
Підсумок Block 5
Вітаю! Ти пройшов увесь Block 5: JavaScript & DOM. Ось що ти тепер вмієш:
- DOM-селектори -- знаходити будь-який елемент на сторінці
- Маніпуляція DOM -- створювати, змінювати та видаляти елементи
- Події -- реагувати на дії користувача (кліки, введення, submit)
- Делегування подій -- ефективно обробляти події на динамічних елементах
- Форми -- отримувати дані, валідувати, обробляти submit
- localStorage -- зберігати дані у браузері
Ці навички -- фундамент для будь-якого фронтенд-розробника. У наступному блоці (Block 6: JavaScript Advanced) ми вивчимо ES6+, промісі, async/await та роботу з API.
Корисні посилання:
- TodoMVC -- колекція Todo List на різних фреймворках, щоб побачити як ту саму задачу вирішують по-різному
- javascript.info: Делегування подій -- поглиблений матеріал (українською)
- Спробуй відкрити DevTools (F12) -> Application -> Local Storage і подивись, як зберігаються твої задачі