Создание 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():