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

Александр Мальцев
Александр Мальцев
2K
0
Создание Todo List на чистом JavaScript
Содержание:
  1. Что такое Todo List?
  2. Исходные коды SimpleTodoList
  3. Описание процесса создания SimpleTodoList
  4. Преобразование JavaScript для запуска в Internet Explorer 11
  5. Задачи
  6. Комментарии

В этой статье попрактикуемся на чистом на JavaScript на примере создания программы «Списка дел (Todo List)». При написании кода будем использовать современный синтаксис, но также сделаем так чтобы он работал в старых браузерах, включая Internet Explorer 11.

Что такое Todo List?

Todo List – это список дел, которые вам нужно выполнить или того, что вы хотите сделать.

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

Но такой список можно вести не только на листке бумаги, но и в электронном виде, например, браузере.

Исходные коды SimpleTodoList

SimpleTodoList – это название проекта, который мы создадим в рамках данной статьи для ведения списка задач. Напишем его он на HTML, CSS и чистом JavaScript.

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

Todo List на чистом JavaScript

Демо SimpleTodoList

Исходные коды 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. Создание файловой структуры

Файловая структура проекта:

Файловая структура Todo List

Шаг 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.

Скриншот того, что у нас вышло:

Скриншот Todo List после выполнения разметки

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. Написание стилей

Написать стили можно по-разному. Пример того, что получилось:

Скриншот Todo List после выполнения разметки

Конечный 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:

Преобразование исходного синтаксиса в такой, который поддерживает Internet Explorer 11

В 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 переписать весь код с использованием функций этой библиотеки.

Комментарии:

  1. IG
    IG
    16.10.2021, 14:54
    Здравствуйте, Александр. Спасибо, за подробный пример. Очень наглядно.
    У меня есть вопрос: какие есть способы выгрузки todo-списка, скажем, на компьютер в формат.тхт?

    п.с. небольшое замечание по работе сайта. При нажатии ссылки «сменить аватар» в личном кабинете, происходит просто переход на главную страницу.
    1. Александр Мальцев
      Александр Мальцев
      21.10.2021, 11:32
      Привет!
      Можно, например, так (также добавил на Github):
      <button class="btn" id="save"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"></path><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"></path></svg> Сохранить</button>
      <script>
        document.querySelector('#save').addEventListener('click', (e) => {
          var a = document.createElement('a');
          a.download = 'tasks.txt';
          let content = '1. Активные задачи';
          let items = document.querySelectorAll('.todo__item[data-todo-state="active"]');
          items.forEach((item, index) => {
            content += '\n1.' + (index + 1) + '. ' + item.querySelector('.todo__task').textContent;
          });
          content += '\n\n2. Завершённые задачи';
          items = document.querySelectorAll('.todo__item[data-todo-state="completed"]');
          items.forEach((item, index) => {
            content += '\n2.' + (index + 1) + '. ' + item.querySelector('.todo__task').textContent;
          });
          content += '\n\n3. Удалённые задачи';
          items = document.querySelectorAll('.todo__item[data-todo-state="deleted"]');
          items.forEach((item, index) => {
            content += '\n3.' + (index + 1) + '. ' + item.querySelector('.todo__task').textContent;
          });
          a.href = window.URL.createObjectURL(new Blob([content], { type: 'text/plain' }));
          a.click();
        });
      </script>
      
      Спасибо, момент на сайте поправил.
    Войдите, пожалуйста, в аккаунт, чтобы оставить комментарий.