Как создать To‑Do список на JavaScript и DOM

TL;DR
Краткое пошаговое руководство по созданию простого To‑Do приложения на чистом JavaScript с манипуляцией DOM и хранением в localStorage. Показываю базовую разметку с TailwindCSS, обработку событий, добавление/редактирование/удаление задач, а также рекомендации по безопасности, тестам и альтернативным подходам.
Введение
DOM (Document Object Model) — это модель представления структуры и содержимого веб‑страницы в виде объектов. С помощью JavaScript вы можете получать доступ ко всем элементам DOM и динамически создавать, читать, обновлять и удалять (CRUD) их.
Эта статья объясняет, как выполнять CRUD‑операции для простого To‑Do списка, управляя DOM. Предполагается, что вы знакомы с основами HTML и JavaScript.
Краткое определение
- DOM: представление HTML в виде объектов, доступных через window.document.
Понимание базовой работы с DOM
Разберём простой пример (исходный код сохранён):
В этом примере переменная submitButton даёт доступ к HTML‑кнопке. Мы добавляем обработчик события click: когда пользователь кликает — появляется окно с текстом “The form has been submitted.”.
Важно: в реальных приложениях для пользовательских сообщений лучше использовать элементы интерфейса, а не alert(), чтобы не блокировать поток выполнения.
Улучшенный вариант с корректными кавычками и более чистым кодом:
Разметка и TailwindCSS
Ниже — HTML‑макет проекта. Поля и кнопки имеют id, чтобы получить к ним доступ из JavaScript.
Для стилизации в статье использован TailwindCSS с CDN:
Исходный HTML (сохранён для полноты):
To-Do List App
Так выглядит приложение на этой стадии:

Получение элементов и хранение состояния
В JavaScript первым шагом мы получаем элементы по id и инициализируем массив задач:
const text = document.getElementById("text");
const addTaskButton = document.getElementById("add-task-btn");
const saveTaskButton = document.getElementById("save-todo-btn");
const listBox = document.getElementById("listBox");
const saveInd = document.getElementById("saveIndex");
let todoArray = []; todoArray хранит задачи в памяти, а для сохранения между перезагрузками используется localStorage.
Добавление задач
При клике на кнопку “Add” задача добавляется в массив и сохраняется в localStorage. Исходный обработчик (сохранён):
addTaskButton.addEventListener("click", (e) => {
e.preventDefault();
let todo = localStorage.getItem("todo");
if (todo === null) {
todoArray = [];
} else {
todoArray = JSON.parse(todo);
}
todoArray.push(text.value);
text.value = "";
localStorage.setItem("todo", JSON.stringify(todoArray));
displayTodo();
}); Ключевые моменты:
- Всегда валидируйте text.value (пустые строки, длину, пробелы).
- localStorage синхронен и имеет ограничения по объёму (~5–10 МБ в большинстве браузеров).
Улучшение: проверка на пустую строку и триминг:
addTaskButton.addEventListener("click", (e) => {
e.preventDefault();
const value = text.value.trim();
if (!value) return; // не добавляем пустые задачи
const todo = JSON.parse(localStorage.getItem("todo") || "[]");
todo.push(value);
localStorage.setItem("todo", JSON.stringify(todo));
text.value = "";
displayTodo();
});Отображение списка
Функция displayTodo собирает HTML‑код и вставляет его в listBox через innerHTML (как в исходнике):
function displayTodo() {
let todo = localStorage.getItem("todo");
if (todo === null) {
todoArray = [];
} else {
todoArray = JSON.parse(todo);
}
let htmlCode = "";
todoArray.forEach((list, ind) => {
htmlCode += `
${list}
`;
});
listBox.innerHTML = htmlCode;
} Важно: вставка пользовательских данных в innerHTML без очистки может привести к XSS‑у. См. раздел безопасность ниже.
Удаление задач
Исходная реализация удаления использует splice и перезаписывает localStorage:
function deleteTodo(ind) {
let todo = localStorage.getItem("todo");
todoArray = JSON.parse(todo);
todoArray.splice(ind, 1);
localStorage.setItem("todo", JSON.stringify(todoArray));
displayTodo();
} 
Редактирование задач
Кнопка редактирования заполняет поле ввода значением задачи и переключает видимость кнопок “Add” и “Save”:
function edit(ind) {
saveInd.value = ind;
let todo = localStorage.getItem("todo");
todoArray = JSON.parse(todo);
text.value = todoArray[ind];
addTaskButton.style.display = "none";
saveTaskButton.style.display = "block";
} После редактирования сохраняем изменения:
saveTaskButton.addEventListener("click", () => {
let todo = localStorage.getItem("todo");
todoArray = JSON.parse(todo);
let id = saveInd.value;
todoArray[id] = text.value;
addTaskButton.style.display = "block";
saveTaskButton.style.display = "none";
text.value = "";
localStorage.setItem("todo", JSON.stringify(todoArray));
displayTodo();
}); 


Безопасность и устойчивость к ошибкам
Important: использование innerHTML с пользовательскими данными небезопасно. Возможные риски и mitigations:
- Риск: XSS — пользователь может вставить HTML/скрипт как задачу.
- mitigations: использовать textContent вместо innerHTML при вставке содержимого задач; явная очистка (sanitize) входных данных; ограничение длины.
- Риск: конфликт индексов — если одновременно несколько вкладок изменяют localStorage, возможна рассинхронизация.
- mitigations: подписка на событие window.addEventListener(‘storage’, …) для синхронизации между вкладками.
- Ограничения localStorage: объём и синхронный API. Для больших или сложных приложений выбирайте IndexedDB.
Пример безопасного рендера с createElement (без innerHTML):
function safeDisplayTodo() {
const todo = JSON.parse(localStorage.getItem('todo') || '[]');
listBox.innerHTML = '';
todo.forEach((item, index) => {
const row = document.createElement('div');
row.className = 'flex mb-4 items-center';
const p = document.createElement('p');
p.className = 'w-full text-grey-darkest';
p.textContent = item; // безопасно
const editBtn = document.createElement('button');
editBtn.textContent = 'Edit';
editBtn.className = '...';
editBtn.addEventListener('click', () => edit(index));
const delBtn = document.createElement('button');
delBtn.textContent = 'Delete';
delBtn.className = '...';
delBtn.addEventListener('click', () => deleteTodo(index));
row.appendChild(p);
row.appendChild(editBtn);
row.appendChild(delBtn);
listBox.appendChild(row);
});
}Альтернативные подходы
- Использовать фреймворки: React, Vue, Svelte — помогут упростить управление состоянием и рендеринг.
- Хранение данных: localStorage (простой), IndexedDB (асинхронный, для больших объёмов), серверное хранилище (API + база данных) для синхронизации между устройствами.
- Стили: Tailwind (утилитарный), Bootstrap (компоненты) — выбор зависит от предпочтений и команды.
Тестирование и критерии приёмки
Критерии приёмки
- Пользователь может добавить новую задачу; она появляется в списке и сохраняется после перезагрузки.
- Пользователь может отредактировать задачу; изменения сохраняются и видны сразу.
- Пользователь может удалить задачу; элемент исчезает и больше не сохраняется.
- Введённые данные отображаются безопасно (нет выполнения скриптов).
Примеры тест‑кейсов
- Добавление: ввести “Купить хлеб”, нажать Add, проверить наличие задачи и localStorage.
- Пустая строка: попытка добавить строку из пробелов — задача не создаётся.
- Редактирование: изменить задачу и проверить изменение в localStorage.
- Удаление: удалить задачу и убедиться, что index смещается корректно.
Руководство для ролей (коротко)
Developer
- Реализовать валидацию, использовать textContent либо sanitize.
- Обработать событие storage для мультивкладочной синхронизации.
QA
- Проверить граничные значения, XSS‑вставки, поведение при переполнении localStorage.
Designer
- Проверить доступность (контраст, размеры кнопок), фокус‑стейты и адаптивность.
Ментальные модели и эвристики
- «Single source of truth»: храните состояние в одном месте (например, в localStorage или в state фреймворка).
- «Render as data»: сначала подготовьте структуру данных, затем рендерьте DOM на её основе (не наоборот).
- Предпочитайте делегирование событий при большом количестве элементов вместо назначения обработчика каждому элементу.
Быстрый чек‑лист по улучшению приложения
- Валидация ввода (trim, maxlength).
- Защита от XSS (textContent / sanitize).
- Обработка storage для синхронизации вкладок.
- Тесты для добавления/редактирования/удаления.
- Подумать о миграции на IndexedDB или сервер для синхронизации между устройствами.
Decision flowchart
flowchart TD
A[Начало] --> B{Есть localStorage?}
B -- Да --> C[Загрузить массив задач]
B -- Нет --> D[Создать пустой массив]
C --> E[Показать список]
D --> E
E --> F{Действие пользователя}
F -- Добавить --> G[Валидация и push]
F -- Редактировать --> H[Поставить в input и переключить кнопки]
F -- Удалить --> I[splice и сохранить]
G --> J[Сохранить в localStorage]
H --> J
I --> J
J --> EСовместимость и миграция
- localStorage доступен в большинстве современных браузеров, но имеет лимит и синхронный API.
- Для офлайн‑первых и больших объёмов используйте IndexedDB (асинхронно).
- Для многопользовательских приложений храните задачи на сервере (REST/GraphQL + БД).
Короткое объявление (100–200 слов)
Новый учебный проект: простое To‑Do приложение на чистом JavaScript и DOM. В статье разобраны все базовые операции: добавление, редактирование, удаление задач и сохранение состояния в localStorage. Также показаны практические улучшения — валидация ввода, безопасный рендер без innerHTML, обработка мультивкладочной синхронизации и рекомендации по миграции на IndexedDB или серверное хранилище. Полезно как для начинающих, так и для тех, кто хочет освежить подходы к безопасности и архитектуре в небольших фронтенд‑проектах.
Итог
- To‑Do приложение — отличный учебный проект: покрывает DOM, события, хранение состояния и UX‑паттерны.
- Обратите особое внимание на безопасность: избегайте вставки пользовательского HTML через innerHTML.
- При росте проекта рассматривайте IndexedDB или серверную синхронизацию.
Спасибо за чтение — начинайте с малого, затем расширяйте функционал: отметки выполнения, фильтры, приоритеты и синхронизация между устройствами.
Похожие материалы
Gmail и настольные клиенты: выбор и настройка
SketchUp бесплатно: как начать 3D‑моделирование
Как создать аккаунт PlayStation Network (PSN)
Почему iPhone и iPad нагреваются и как это исправить
Как искать жильё на Airbnb для отпуска