ФронтендJavaScriptHTML / CSS

Todo-лист на JavaScript: задачи, фильтры и сохранение

Создаём Todo-лист на чистом JavaScript: добавление и удаление задач, отметка выполнения, фильтры, localStorage и безопасный вывод текста.

Кодик

Автор

5 мин чтения

Todo-лист становится понятным, когда массив tasks является единственным источником истины. Интерфейс не хранит отдельную копию состояния: каждое действие меняет массив, сохраняет JSON и заново рисует подходящие задачи.

Этот проект тренирует формы, массив объектов, события, фильтрацию и локальное хранилище. Главная ловушка не в кнопке удаления, а в синхронизации. Если менять HTML отдельно от данных, после перезагрузки появятся расхождения.

1Меняем

Каждое действие создаёт новое состояние массива.

2Сохраняем

После изменения записываем JSON в localStorage.

3Рисуем

Фильтр выбирает данные, render строит список через textContent.

Какие данные нужны одной задаче

Объект задачи содержит id, text и done. id связывает кнопку в интерфейсе с объектом, text хранит ввод, done отвечает за статус. Текущий filter можно оставить в памяти: после перезагрузки приложение откроется со всеми задачами.

ПолеТипПример
idstringcrypto.randomUUID()
textstringПроверить мобильное меню
donebooleanfalse
filterstringall, active, done

После этого шага у каждой части есть одна роль. Интерфейс собирает действие, логика меняет состояние, а вывод показывает признак успеха. Это проще отлаживать, чем один большой обработчик.

Одно изменение задачи
Данные проходят одинаковый путь. HTML никогда не становится источником истины

Сначала соберите минимальный сценарий. Не добавляйте оформление, хранение и дополнительные режимы одновременно. Добейтесь одного правильного прохода от входных данных до результата, а затем проверяйте каждое улучшение отдельно.

Пишем добавление, переключение и удаление

Ниже ядро приложения. JSON.parse обёрнут в try/catch, потому что хранилище может содержать старые или повреждённые данные. Текст задачи назначается через textContent, а не вставляется как HTML.

const KEY='codik.tasks';
let filter='all';
let tasks=load();

function load(){
  try{const value=JSON.parse(localStorage.getItem(KEY) ?? '[]');return Array.isArray(value)?value:[]}
  catch{return []}
}
function save(){localStorage.setItem(KEY,JSON.stringify(tasks))}
function change(next){tasks=next;save();render()}

function add(text){
  const clean=text.trim(); if(!clean) return;
  change([...tasks,{id:crypto.randomUUID(),text:clean,done:false}]);
}
function toggle(id){change(tasks.map(task=>task.id===id?{...task,done:!task.done}:task))}
function remove(id){change(tasks.filter(task=>task.id!==id))}

function render(){
  const list=document.querySelector('#tasks'); list.replaceChildren();
  const visible=tasks.filter(task=>filter==='all'||(filter==='done')===task.done);
  for(const task of visible){
    const item=document.createElement('li');
    const label=document.createElement('button'); label.textContent=task.text;
    label.className=task.done?'done':''; label.onclick=()=>toggle(task.id);
    const del=document.createElement('button'); del.textContent='Удалить';del.onclick=()=>remove(task.id);
    item.append(label,del);list.append(item);
  }
}
render();
Что должно работать после первого запуска

Массив tasks является единственным состоянием задач. Пустая строка не добавляется. Текст выводится через textContent. Повреждённый localStorage безопасно даёт пустой список.

Как вывести текст
Пользовательский ввод остаётся данными. Сравнение показывает, какой вариант легче поддерживать и проверять.

Как безопасно хранить JSON в браузере

localStorage хранит только строки и работает синхронно. Для десятков учебных задач это нормально. Большие данные и сложный поиск лучше переносить в IndexedDB или на сервер. Важно не путать сохранение с аккаунтом: задачи останутся только в этом браузере и профиле.

ДействиеОперация массиваПосле неё
Добавитьspread + новый объектsave, render
Переключитьmapsave, render
Удалитьfiltersave, render
Показать активныеfilter для отображениятолько render

Сохраняйте в одном месте то, что может измениться. Если одно и то же значение скопировано в три функции, после первой правки они начнут расходиться. Один объект состояния и одна функция отрисовки снимают эту проблему.

Смотрите на состояние, а не только на красивый экран. Перезагрузка, повторный клик, пустой ввод и медленный ответ часто открывают ошибки, которые не видны в идеальном демонстрационном проходе.

Практика: проверяем фильтры и перезапуск

Добавьте три задачи, выполните среднюю, включите каждый фильтр и перезагрузите страницу. Удалите одну задачу из режима «Выполненные». Затем вручную испортите значение codik.tasks в DevTools. Приложение должно запуститься с пустым списком. Проверьте две задачи с одинаковым текстом: переключение и удаление должны затрагивать только выбранный id. Затем вставьте строку с угловыми скобками и убедитесь, что она показана как текст, а не созданный HTML-элемент. Откройте приложение во второй вкладке и измените список в первой. Учебная версия не обязана синхронизироваться мгновенно, но после обновления должна прочитать последнее сохранённое состояние. Это хороший мостик к событию storage и серверной синхронизации.

Проверка своими руками
  1. Добавьте пустую строку и строку с пробелами.
  2. Создайте три нормальные задачи.
  3. Переключите одну два раза.
  4. Удалите задачу в отфильтрованном списке.
  5. Перезагрузите страницу.
  6. Испортите JSON в localStorage и снова откройте приложение.
Готово, если выполняются все пункты

Пустые задачи не появляются. Статус и удаление работают во всех фильтрах. После F5 список восстанавливается. Текст с угловыми скобками остаётся текстом.

Проверка Todo
Четыре сценария состояния. Пройдите все четыре сценария перед публикацией проекта.

Доводим Todo-лист до готового приложения

После базовой версии добавьте редактирование и очистку выполненных. Все новые действия проводите через change, чтобы не забыть save и render. Если появится сервер, интерфейсный контракт останется похожим: состояние меняется после успешного запроса.

HTML считается состоянием

После фильтра и F5 данные расходятся.

innerHTML с пользовательским текстом

Строка может превратиться в разметку.

JSON.parse без try/catch

Повреждённое значение ломает запуск.

Нет стабильного id

Текст не подходит для точного удаления.

Сверьтесь с первичным источником. Поведение используемых функций и ограничения примера проверяйте по документацией MDN по localStorage. Документация особенно важна, когда меняются версии, политика платформы или формат ответа API.

Что получится в итоге

Готовый результат проекта: Todo-лист на JavaScript: задачи, фильтры и сохранение
Готовый Todo-лист показывает активные и выполненные задачи, считает остаток и сохраняет всё между перезагрузками.

Короткие ответы
Что является источником истины?

Массив объектов tasks, а HTML лишь отображает его.

Почему нужен id?

Он позволяет точно найти задачу независимо от текста и фильтра.

Чем безопаснее выводить текст?

Свойством textContent.

Как понять, что проект готов?

Он повторяет целевой сценарий несколько раз подряд, понятно реагирует на ошибочный ввод и не теряет состояние при обычных действиях.

Можно ли копировать код целиком?

Можно как стартовую точку, но сначала замените значения из примера, затем запустите проект по шагам. Если вы не можете объяснить одну строку, её лучше разобрать до следующего шага.

Сделайте приложение, которое помнит ваши задачи

Массивы, формы и DOM разбираются в курсе JavaScript. Если элемент не найден, поможет разбор ошибки null.

Для синхронизации с сервером изучите Fetch API, а внешний вид дополните тёмной темой. В Кодике удобно сохранять рабочие этапы: добавление, фильтры, хранилище.