Создание Todo List на чистом JavaScript

В этой статье попрактикуемся на чистом на JavaScript на примере создания программы «Списка дел (Todo List)». При написании кода будем использовать современный синтаксис, но также сделаем так чтобы он работал в старых браузерах, включая Internet Explorer 11.
Что такое Todo List?
Todo List – это список дел, которые вам нужно выполнить или того, что вы хотите сделать.
Традиционно их пишут на листке бумаги и организовывают в порядке приоритета. При выполнении задачи, её обычно вычеркивают из списка.
Но такой список можно вести не только на листке бумаги, но и в электронном виде, например, браузере.
Исходные коды SimpleTodoList
SimpleTodoList – это название проекта, который мы создадим в рамках данной статьи для ведения списка задач. Напишем его он на HTML, CSS и чистом JavaScript.
Пошаговый процесс его создания приведён в следующем разделе этой статьи, а в этом его демо и исходные коды.

Исходные коды SimpleTodoList расположены в соответствующей папки проекта «ui-components» на GitHub.
Состоит SimpleTodoList из 3 файлов: «index.html» (вёрстки), «simple-todo-list.css» (стилей) и «simple-todo-list.js» (скрипта).
SimpleTodoList использует localStorage для хранения задач. Это позволяет при повторном открытии этой страницы или её обновлении считывать данные с веб-хранилища и на их основе воссоздавать последнее состояние списка.
Сохранение данных в хранилище происходит всякий раз, когда изменяется состояния этого списка. Это необходимо для того, чтобы в localStorage всегда находились актуальные сведения.
SimpleTodoList позволяет:
- добавлять новые задачи в список;
- отмечать выполненные задачи (при этом они сразу исключаются из списка активных задач и переводятся в завершённый);
- удалять элементы в корзину;
- удалять окончательно задачи в корзине, а также при необходимости восстанавливать их;
- переключаться между делами (активными, завершёнными и удалёнными);
- автоматически сохранять списки дел в localStorage (необходимо для восстановления последнего состояния списка при повторном открытии страницы).
Описание процесса создания SimpleTodoList
Разработку SimpleTodoList выполним за 5 шагов.
Шаг 1. Создание файловой структуры
Файловая структура проекта:

Шаг 2. Добавление в index.html базовой структуры
Откроем «index.html», добавим в него базовую разметку, а также подключим файлы со стилями и JavaScript.
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Todo List on pure JavaScript</title> <link rel="stylesheet" href="simple-todo-list.css"> <script defer src="simple-todo-list.js"></script> <style> *, *::before, *::after { box-sizing: border-box; } body { margin: 0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #212529; background-color: #fff; -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } </style> </head> <body> </body> </html>
Шаг 3. Выполнение разметки самого todo
Разметим блок todo:
<div class="todo"> <div class="todo__input"> <input class="todo__text" type="text"> <span class="todo__add"></span> </div> <select class="todo__options"> <option value="active">активные</option> <option value="completed">завершённые</option> <option value="deleted">удалённые</option> </select> <ul class="todo__items" data-todo-option="active"></ul> </div>
.todo
содержит:
- текстовое поле (
<input>
с классомtodo__text
) для ввода новой задачи; - кнопку
.todo__add
для добавления новой задачи в.todo__items
; - элемент управления
<select>
с выпадающим список задач для переключения между ними; - список задач
.todo__items
, добавлять их в него будем с помощью JavaScript.
Скриншот того, что у нас вышло:

HTML-код самой задачи:
<li class="todo__item" data-todo-state="active"> <span class="todo__task">Текст задачи</span> <span class="todo__action todo__action_restore" data-todo-action="active"></span> <span class="todo__action todo__action_complete" data-todo-action="completed"></span> <span class="todo__action todo__action_delete" data-todo-action="deleted"></span> </li>
Его формирование и вставку в .todo__items
будем осуществлять с помощью JavaScript.
Значение атрибута data-todo-state
будет определять состояние задачи:
active
– активная;completed
– выполненная;deleted
– удалённая.
.todo__task
– это элемент, который содержит текст задачи, а .todo__action
– это кнопки для выполнения действий над задачей. С помощью них мы можем восстановить задачу (перевести её в активное состояние), отметить задачу как завершённую и удалить. Действие, которое выполняет кнопка .todo__action
определяется значением атрибута data-todo-action
.
Шаг 4. Написание стилей
Написать стили можно по-разному. Пример того, что получилось:

Конечный CSS код можно посмотреть на GitHub.
Разберём некоторые интересные моменты в этом коде.
1. Переключение отображение задач в .todo__items
будем выполнять в зависимости от выбранной опции <select>
. Для этого в вышеприведённом файле прописаны следующие стили:
[data-todo-option="active"] .todo__item:not([data-todo-state="active"]), [data-todo-option="completed"] .todo__item:not([data-todo-state="completed"]), [data-todo-option="deleted"] .todo__item:not([data-todo-state="deleted"]) { display: none; }
Устанавливать значение атрибуту data-todo-option
элемента .todo__items
будем с помощью JavaScript при изменении выбранной опции <select>
.
2. Скрытие кнопок для задач, которые не должны показываться для определённых состояний, осуществляется следующим образом:
[data-todo-state="active"] .todo__action_restore, [data-todo-state="completed"] .todo__action_complete, [data-todo-state="deleted"] .todo__action_complete { display: none; }
Например, кнопку восстановить .todo__action_restore
не будем отображать для активных задач [data-todo-state="active"]
. Т.к. зачем переводить задачу в активное состояние, если она и так активна.
Шаг 5. Напишем JavaScript
Написание кода начнём с создания объекта todo
:
const todo = {};
Он нужен только для того, чтобы лучше организовать наш код и не создавать кучу отдельных функций.
Поместим в todo
следующие методы:
const todo = { action(e) {}, add() {}, create(text) {}, init() {}, update() {}, save() {} };
Начнём с init()
. Данный метод будет осуществлять инициализацию Todo List.
Он выполняет следующие вещи:
- получает из localStorage сохранённый список дел и если он есть, то вставляет его в
.todo__items
; - назначает обработчик события
change
на элемент.todo__options
; в качестве обработчика используетсяupdate
; - назначает обработчик события
click
наdocument
; в качестве обработчика выступаетaction
.
Код метода init()
:
init() { const fromStorage = localStorage.getItem('todo'); if (fromStorage) { document.querySelector('.todo__items').innerHTML = fromStorage; } document.querySelector('.todo__options').addEventListener('change', this.update); document.addEventListener('click', this.action.bind(this)); }
Когда мы указываем в качестве обработчика функцию или метод объекта, то нужно просто передать ссылку, а не вызов.
При указании this.action
мы с помощью bind
установили в качестве this
текущий контекст. Это нужно чтобы мы могли в action()
получить объект todo
с помощью this
.
Метод create()
очень простой, он будет просто возвращать HTML код самой задачи с указанным текстом:
create(text) { return `<li class="todo__item" data-todo-state="active"> <span class="todo__task">${text}</span> <span class="todo__action todo__action_restore" data-todo-action="active"></span> <span class="todo__action todo__action_complete" data-todo-action="completed"></span> <span class="todo__action todo__action_delete" data-todo-action="deleted"></span></li>`; }
save()
получает содержимое .todo__items
и устанавливает его в localStorage:
save() { localStorage.setItem('todo', document.querySelector('.todo__items').innerHTML); }
update() используется в качестве обработчика:
update() { const option = document.querySelector('.todo__options').value; document.querySelector('.todo__items').dataset.todoOption = option; document.querySelector('.todo__text').disabled = option !== 'active'; }
При вызове устанавливает атрибуту data-todo-option
элемента .todo__items
значение выбранной опции <select>
, а также значение свойству disabled
элемента .todo__text
:
add()
добавляет при нажатии на кнопку задачу в список .todo__items
. Для создания HTML-кода задачи используется create()
:
add() { const elemText = document.querySelector('.todo__text'); if (elemText.disabled || !elemText.value.length) { return; } document.querySelector('.todo__items').insertAdjacentHTML('beforeend', this.create(elemText.value)); elemText.value = ''; }
action
вызывается, когда происходит событие click
на документе:
action(e) { const target = e.target; if (target.classList.contains('todo__action')) { const action = target.dataset.todoAction; const elemItem = target.closest('.todo__item'); if (action === 'deleted' && elemItem.dataset.todoState === 'deleted') { elemItem.remove(); } else { elemItem.dataset.todoState = action; } this.save(); } else if (target.classList.contains('todo__add')) { this.add(); this.save(); } }
Параметр e
– это объект события event
. Его создаёт браузер и передаёт его в качестве первого аргумента action
.
В коде e.target
– это элемент, по которому кликнули. Так как нам нужны не любые клики, а только по определённым элементам, то используем следующие условия:
if (target.classList.contains('todo__action')) { // ... } else if (target.classList.contains('todo__add')) { // ... }
Если пользователь кликнул по .todo__add
, то выполним следующие действия:
this.add(); this.save();
add()
добавляет в список .todo__items
новую задачу, а save()
сохраняет все задачи (содержимое .todo__items
) в localStorage.
Когда пользователь кликнул на .todo__action
выполняется следующий код:
const action = target.dataset.todoAction; const elemItem = target.closest('.todo__item'); if (action === 'deleted' && elemItem.dataset.todoState === 'deleted') { elemItem.remove(); } else { elemItem.dataset.todoState = action; } this.save();
Первое – получаем действие, которое нужно выполнить. Оно у нас находится в атрибуте data-todo-action
. Далее находим элемент .todo__item
и сохраняем его в переменную elemItem
. Этот элемент нам понадобится дальше. После этого, если действие delete
и состояние задачи delete
, то удаляем элемент. В противном случае установим атрибуту data-todo-state
элемента .todo__item
значение action
. В конце сохраним все изменения в localStorage с помощью save()
.
Последнее что нужно сделать чтобы Todo работал это вызвать init
:
todo.init();
Преобразование JavaScript для запуска в Internet Explorer 11
1. Выполним транспилирование, т.е. преобразуем исходный синтаксис в такой, который понимают старые браузеры, включая Internet Explorer 11.
Для этого воспользуемся онлайн инструментом Babel REPL:

В targets
указали: defaults, ie 11
.
Полученный код скопируем и вставим в файл simple-todo-list.es5.js
.
Теперь полученный синтаксис понимает Internet Explorer 11. Но этого не достаточно, т.к. в коде у нас остались два метода, который данный браузер не поддерживает. Это closest
и remove
.
2. Выполним полифилинг, т.е. добавим эти недостающие методы к старым браузерам путем предоставления им собственной версии.
// polyfill closest if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; } if (!Element.prototype.closest) { Element.prototype.closest = function(s) { var el = this; do { if (Element.prototype.matches.call(el, s)) return el; el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); return null; }; } // polyfill remove if (!('remove' in Element.prototype)) { Element.prototype.remove = function() { if (this.parentNode) { this.parentNode.removeChild(this); } }; }
В результате получился следующий код: simple-todo-list.es5.js
.
Задачи
1. Добавить возможность создавать задачу при нажатии Enter.
2. Переписать код так, чтобы в localStorage сохранялись задачи не в виде кода HTML, а в формате массива объектов:
[ { task: 'Задача 1', state: 'active' }, { task: 'Задача 2', state: 'completed' }, ... ];
3. Внести в код возможность сортировки задач посредством перетаскивания (drag и drop).
4. Добавить всплывающие сообщения для информирования пользователя при выполнении действий над задачами.
5. Для любителей jQuery переписать весь код с использованием функций этой библиотеки.
Ниже от даты создания добавить поле для комментария с ограничением (например) в 100 символов касаемо только выбранного пункта.
Или, добавить выпадающий список, в котором человек выбирает один из вариантов, например: купить картошку, а в выпадающем выбрать «на рынке». Здесь, я так полагаю Todo должен иметь какое-то определенное направление. Хотя мне кажется, что первый вариант более практичней.
Возможно ли каким-то образом добавлять текущую дату к элементам списка.
Получилось добавлять дату при добавлении, но не могу понять как сделать так, что-бы эта дата менялась тогда, когда я из «активного» списка помечаю элемент как завершённый или удалённый?!
То есть, когда добавил в список, то дата например: «добавлено: 19.07.2022, 14:35». Когда пометил как «завершённый», то «завершено: 20.07.2022, 10:00», если удаляю, то: «удаленно: 21.07.2022, 18:20».
(даты разумеется для примера)
Если из (например) «завершённые» я отмечаю как «возобновить», то «завершено: 20.07.2022, 10:00» меняется на «добавлено: 21.07.2022, 16:12».
То, что я сделал:
Так же пытался сделать по другому, добавив вызов функции при клике по кнопкам «завершено» и «удалить», но если я нажму «завершено» (или «удалить») на два элемента по очереди, то «завершено: 20.07.2022, 10:00» добавится только к первому элементу, в области которого я совершил клик. То, что я сделал:
Возможно ли вообще это сделать?!
Если нужно как хотели именно вы, то закомментируйте указанную строчку и добавьте после неё новую:
Новая версия уже на GitHub.
У меня есть вопрос: какие есть способы выгрузки todo-списка, скажем, на компьютер в формат.тхт?
п.с. небольшое замечание по работе сайта. При нажатии ссылки «сменить аватар» в личном кабинете, происходит просто переход на главную страницу.
Можно, например, так (также добавил на Github):
Спасибо, момент на сайте поправил.
Код, который использовал:
Выполняться код будет при наступлении события todo-item-add. Для генерирования этого события, его нужно прописать в add():