Представим себе, что вы пишите таск-менеджер. Приложение должно работать как нативное: работать в офлайне также как в онлайне.


Основная концепция


Наш главный инструмент в построении такого приложения – localstorage.


С другими способами реализовать это я не знаком. Способ о котором я расскажу я придумал сам и мне он показался удачным.


Как это будет работать?


Итак, у нас есть список тасков:


<ul class="tasks">
  <li class="task">
    <p class="task__text" contenteditable="true">Eggs</p>
    <input type="checkbox">
  </li>
  <li class="task">
    <p class="task__text" contenteditable="true">Breads</p>
    <input type="checkbox">
  </li>
</ul>
<button class="task__add-btn" type="button">New task</button>

Нам нужно хранить и обновлять текст задачи и статус выполнена/нет, а также реализовать добавление новой задачи.


Мы будем генерировать случайный ключ для каждой задачи и использовать его как идентификатор для каждой связанной хранимой информации в localstorage. По аналогии похоже на базы данных.


С чего начать?


Прежде всего мы будем смотреть есть ли уже какие-либо задачи в памяти. Все ключи будет храниться в одной строке


const savedTokens = localStorage.getItem('tokens');
const root = document.querySelector('.tasks');
let tokens = [];

if (savedTokens != null) {
  tokens = savedTokens.split(', ');
  // ...
}

Разберёмся пока с добавлением новой задачи, чтобы понимать концепцию хранения и использования хранимых данных.


При клике на добавить задачу в списке появится задача-шаблон, текст задачи можно редактировать. Неперь нужно описать всё это.


Создадим функцию, которая создаёт элемент с его содержимым по переданным в неё данным.


function getTemplate(token, data) {
  const task = document.createElement('li');

  task.classList.add('task');
  task.setAttribute('data-token', token);

  task.innerHTML = `
    <p class="task__text" contenteditable="true">${data.text}</p>
    <input type="checkbox" ${data.input == 'true' ? 'checked' : ''}>
  `;

  return task;
}

Опишем события для кнопки и для редактирования задачи:


const btnAddTask = document.querySelector('.task__add-btn');

btnAddTask.addEventListener('click', function(e) {
  e.preventDefault();

  const newToken = randomToken();

  tokens.push(newToken);

  const taskData = {
    text: 'Task`s title',
    input: false
  };

  root.appendChild(getTemplate(newToken, taskData));

  updateTokens();

  updateEvent(newToken);
});

Реализуем недостающие функции updateEvent и randomToken.


function updateEvent(token) {
  const task = root.querySelector(`[data-token="${token}"]`);
  const text = task.querySelector(`.task__text`);
  const input = task.querySelector(`input`);

  localStorage.setItem(`text-${token}`, text.innerHTML);
  localStorage.setItem(`input-${token}`, input.checked);

  text.addEventListener('input', function() {
    localStorage.setItem(`text-${token}`, text.innerHTML);
  });

  input.addEventListener('click', function() {
    localStorage.setItem(`input-${token}`, input.checked);
  });
}

function updateTokens() {
  localStorage.setItem('tokens', tokens.join(', '));
}

function randomToken() {
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}

Отлично! Мы реализовали добавление задачи и сохранение их в localStorage. Осталось сделалть вывод задачь, которые уже сохранены.


Вывод сохранённых тасков


Для вывода тасков достаточно получить все ключи, распарсить их, вытянуть по каждому ключи текст таска и его статус. Далее просто создаём каждый таск в root: получаем таск с помощью getTemplate и вставляем его в root.


 tokens.forEach(function(token) {
  const text = localStorage.getItem(`text-${token}`);
  const input = localStorage.getItem(`input-${token}`);

  root.appendChild(getTemplate(token, {
    text: text,
    input: input
  }));

  updateEvent(token);
});

В общем, наш простейший таск-менеджер готов. Остаётся собрать всё в кучу.


Рабочий таск менеджер доступен по ссылке.

Комментарии (14)


  1. baxxter
    03.02.2019 00:33
    +1

    if (savedTokens != null)
    Используйте лучше строгое сравнение и вот почему — dorey.github.io/JavaScript-Equality-Table

    data.input == 'true' ? 'checked' : ''
    Здесь ошибка(сравниваете булево со строкой), проще сделать так
    data.input ? 'checked' : ''

    Так же Вы выбрали не очень удобную структуру данных для хранения данных задач(строки), лучше было бы использовать массив объектов и использовать всего один ключ в localStorage, вообще без возни с генерацией токенов(ID?):

    localStorage.setItem(`tasks`, JSON.stringify([ {a:1,b:2},{a:2, b:4}]));
    JSON.parse(localStorage.getItem(`tasks`));
    console.log(...JSON.parse(localStorage.getItem(`tasks`)));
    // {a: 1, b: 2} {a: 2, b: 4}


    Кстати вот несколько идей, как можно улучшить ваш todo-list: добавить возможность удалять задачи(крестик справа от задачи например), добавить инпут слева от кнопки New Task и генерировать текст новой задачи из данных в нем (сбрасывать ввод после добавления задачи), по чекбоксу слева от задачи зачеркивать текст задачи (статус выполнено), добавить в шапке количество задач (общее, выполнено, не выполнено), добавить возможность указывать срок выполнения задачи (опционально, например дату и время к которой задача должна быть выполнена) и подсвечивать не выполненные в срок задачи красным цветом.

    Удачи в изучении Javascript!


    1. JustDont
      03.02.2019 01:05
      -2

      Используйте лучше строгое сравнение

      Того, кто пытается использовать строгое сравнение для null — нужно долго бить канделябрами по голове до полного просветления. Пока не поймёт, что ничего принципиально страшного в сравнении с приведением типов нет, а писать (x === null || x === undefined) — удел не очень умных, но очень принципиальных людей.


      1. baxxter
        03.02.2019 01:39
        +1

        Согласен, при условии что Вы понимаете что делаете. Лично я считаю что явное, лучше чем неявное. Кому то ваш код возможно потом еще читать придется :)


        1. vintage
          03.02.2019 09:32
          -3

          А строки вы тоже явно сравниваете через цикл посимвольно?


      1. Zibx
        03.02.2019 07:51

        Ещё лучше — следить за данными. Раньше использовал undefined, причём в виде void 0 чтоб кто-нибудь его случайно не переопределил, а теперь полностью исключил ситуации внезапно появляющихся ключей и никак не нарадуюсь.


      1. dom1n1k
        03.02.2019 13:34

        И всё-таки лучше писать ===. Это если исходить из реалий, а не абстрактной теории.
        Даже если человек, написавший (x == null), абсолютно четко понимал, как это работает — у человека, читающего сей код через год, не может быть в этом уверенности. У него нет 100% гарантий, что предшественник действительно сделал это сознательно, а не по недосмотру. А значит, для устранения сомнений нужно больше тратить времени на изучение кода.
        Это тот самый случай, когда явное действительно лучше, чем неявное.


        1. JustDont
          03.02.2019 13:43
          -1

          Я тут пожалуй присоединюсь к vintage, и скажу, что если продолжать линию рассуждений «как бы чего не вышло» — станет ясно, что писать, например, .map() тоже нельзя. Только перебор элементов в цикле for со счётчиком. А то вдруг читающий не поймёт™.
          Да и вообще в пределе окажется, что писать нужно только словами и на бумаге. А то напридумывают этих ваших сложностей, языки, компуктеры, ничего не понятно!

          ЗЫ: «Исходя из реалий» ситуация очень проста: JS — уникален в части создания сложностей на ровном месте со своими null и undefined, где null !== undefined. Поэтому код, пытающийся эту «фичу» использовать — не должен проходить в прод, за исключением каких-то совершенно фантастических случаев, когда без этого вообще никак невозможно. А если у нас нет кода, который обрабатывает null и undefined по-разному — то далее везде мы спокойно пишем == null и != null, и это работает.
          В случаях других сравнений — можете делать что угодно, хоть везде === насаждать, хоть нет. Это типичная bike shed problem, которую пытаются обсуждать в сотни раз больше, чем она того стоит.


        1. vintage
          03.02.2019 19:12
          +1

          Ну а там, где написано === null точно так же придётся потратить дополнительное время, чтобы убедиться, что человек не забыл сравнить с undefined. А там, где написано === null || === undefined нужно потратить дополнительное время, чтобы убедиться, что человек не написал эту конструкцию на автомате по привычке и тут действительно нужна одна ветка логики для обоих случаев. Вообще говоря, если у вас есть сомнения, что предшественник понимал, что делает, то вам в любом случае придётся потратить время на анализ всех вариантов входного значения.


    1. DarthVictor
      03.02.2019 12:26

      Не строгое сравнение с null — распространенный кейс. В ESLint например для этого есть отдельная настройка eslint.org/docs/rules/eqeqeq#smart, которая включена в наиболее часто наследуемом конфиге eslint-config-airbnb github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/best-practices.js#L40
      Строгое сравнение с null при этом делается, когда вам нужно исключить из сравнения undefined, а это крайне редкий кейс.


      1. JustDont
        03.02.2019 13:27

        Строгое сравнение с null при этом делается, когда вам нужно исключить из сравнения undefined, а это крайне редкий кейс.

        Я бы даже сказал, что если у вас возникает ситуация, когда нужно разделить null и undefined — то стоит переписать код так, чтоб она больше не возникала. Ничего хорошего из кода, который по-разному обрабатывает null и undefined обычно не появляется. Хотя бы потому, что при контакте с любым другим окружением помимо JS у вас немедленно появятся проблемы конверсии этих чудо-данных, где через null и undefined обозначены две разные вещи.


    1. VladimirSchneider Автор
      03.02.2019 14:40

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


  1. Drag13
    03.02.2019 01:04

    Если что, можно еще посмотреть на indexedDb. Поддерживается с IE 10. Из плюсов — асинхронная, из минусов — api немного не удобен.


    1. frog
      03.02.2019 01:13

      Неудобство API решается при помощи Dexie


    1. VladimirSchneider Автор
      03.02.2019 14:43

      Спасибо, интересно, обязательно попробую