JS30. Задание 15 LocalStorage

Демо https://js3015.github.io/
Код: https://github.com/js3015/js3015.github.io

Задание называется LocalStorage, подсказывая, что речь пойдёт о сохранении введённых пользователем данных при перезагрузке страницы. Эту тему уже рассматривала здесь пункт "Состояния. Хранение состояний", но без конспекта вспомнить как оно реализуется не получается. Попробую вспомнить с конспектом, заодно и оценю его качество.

Получаем переменные:

var button = document.querySelector('input[type="submit"]');
var input = document.querySelector('input[type="text"]');
var list = document.querySelector(".plates");
var items = [];


Отслеживаем клики по button
Выводим в консоль значение input.value

button.addEventListener("click", function(){
    console.log(input.value);
});

Здесь первая проблема. В консоль значение input.value не выводится. Зато выводится в alert. И что бы это значило?

UPD: кажется, разобралась. Проблема в форме, у которой есть событие по умолчанию, отменить которое я забыла. Форма пытается отправить данные на сервер, поэтому код работает странно или не работает вообще.

Добавим введённый в поле текст в список:

button.addEventListener("click", function(){
    var l = input.value;
    var li = document.createElement("li");
    li.innerText = l;
    list.appendChild(li);
    input.value = "";
});


Работает. Ура.

Чтобы сохранить введённый текст при обновлении страницы, создаём переменную state, в которой будем сохранять текущее состояние:

var state = {
    items: []
}

Теперь, когда добавляем элемент в список, будем одновременно добавлять его в массив item:

state.item.push(l);

...и сохранять объект state в хранилище localStorage

localStorage.setItem("data", JSON.stringify(state));

Код полностью:

var button = document.querySelector('input[type="submit"]');
var input = document.querySelector('input[type="text"]');
var list = document.querySelector(".plates");

var state = {
    items: []
};

var data = localStorage.getItem("data");
if(data) {
    state = JSON.parse(data);
}

window.onload = function() {
    var loaded = sessionStorage.getItem('loaded');
    if(loaded) {
        update();
    } else {
        sessionStorage.setItem('loaded', true);
    }
}

function update(){
    list.innerHTML = "";
    for(var i = 0; i < state.items.length; i++) {
        var li = document.createElement("li");
        li.innerText = state.items[i];
        list.appendChild(li); 
    }
}

button.addEventListener("click", function(
event){
    event.preventDefault();
    var l = input.value;
    var li = document.createElement("li");
    li.innerText = l;
    list.appendChild(li);
    input.value = "";
   
    state.items.push(l);
    localStorage.setItem("data", JSON.stringify(state));
});


... и результат его работы


Всё работает, введённые данные сохраняются, в консоли ошибок нет.

Посмотрим, как реализован код автора
Код, правда, немаленький. Попробую разобраться.

1. Находим элементы

var addItems = document.querySelector(".add-items");
var itemsList = document.querySelector(".plates");
var items = JSON.parse(localStorage.getItem("items")) || [];

addItems - это вся форма в которой
- окно для ввода заказов - input type="text"
- кнопка для добавления заказов - input type="submit"
- список, в котором сохраняется всё заказанное - ul class="plates"

То есть, мы не создаём одновременно переменную для инпута ввода заказов и переменную для кнопки добавления заказов, и объединяем их в одну переменную. Думаю, это нужно  для того, чтобы не назначать отдельный обработчик каждому элементу. Мы ставим один обработчик на их общего предка - форму. Из него можно получить целевой элемент event.target, понять на каком именно потомке произошло событие и обработать его. Такой приём называется делегирование событий и считается одним из ключевых в веб-разработке подробнее

itemsList - список, в котором сохраняется всё заказанное - ul class="plates"

items - содержание списка с заказами - JSON.parse(localStorage.getItem("items")) || [];

Здесь достаточно интересная реализация - целая функция внутри объявления переменной
Мы проверяем сохранён ли в localStorage объект items.
Если да, считываем его с помощью функции JSON.parse
Если нет, тогда содержание списка равно пустому массиву.

2. Функция добавления заказа в список

function addItem(event) {
    event.preventDefault();
    var text = (this.querySelector("[name=item]")).value;
    var item = {
        text,
        done: false
    };

    items.push(item);
    populateList(items, itemsList);
    localStorage.setItem("items", JSON.stringify(items));
    this.reset();
}

● event.preventDefault() - отмена события по умолчанию. так как у нас форма и кнопка в этой форме, кнопка по клику будет пытаться отправить данные. Чтобы она этого не делала, нужно отменить данное событие, указанное для формы по умолчанию

var text = (this.querySelector("[name=item]")).value; - это такой странный способ написать input[type="text"].value; В общем, это значение инпута: пустая строка или тот текст, который в него ввели.
Кстати, чтобы инпут не отправляли пустым, в его html-коде используется атрибут required, который устанавливает поле формы обязательным для заполнения перед отправкой формы на сервер

<input type="text" name="item" placeholder="Item Name" required>

var item = {
        text,
        done: false
    };

Н-да. Это какой-то объект. В нём наш текст, та переменная, которую только что определили. И ключ  done с значением false - done: false. Что это я не совсем понимаю, похоже на итератор - объект, значения которого можно перебирать


Пошла смотреть видео. Может получится понять что он там говорит.

С видео не очень получается, но вот что говорит mdn

Возвращаемое значение - объект с двумя свойствами:
  • done (boolean)
    • Имеет значение true если iterator прошел конец итерируемой последовательности. В этом случае value опционально определяется выражением return value внутри итератора .
    • Имеет значение false если iterator имеет возможность вернуть следующее значение последовательности. Это равносильно когда свойство done не указано.
То есть, это свойство done: false можно смело удалить - хуже код работать не станет.
Так и есть.

Зачем было добавлять свойство done: false, которое и так уже есть по умолчанию? Вероятно, в этом есть какой-то скрытый смысл. Может дальше получится понять.

items.push(item);
Это просто. В массив items добавляем только что введённый в поле для ввода заказа текст item


populateList(items, itemsList);
Это мы вызываем функцию populateList с двумя аргументами items и itemsList.

localStorage.setItem("items", JSON.stringify(items));
В localStorage добавили - setItem - массив items и сразу преобразовали его в строку - JSON.stringify(items));

this.reset();
Функция для сброса формы.

3. Функция добавления элемента в список

function populateList(plates = [], platesList) {
    platesList.innerHTML = plates.map((plate, i) => {
        return `
<li>
<input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? "checked" : ""} />
<label for="item${i}">${plate.text}</label>
</li>
`;
    }).join("");
}

function populateList(plates = [], platesList)

Объявляем функцию populateList с двумя параметрами plates и platesList

Вот здесь пока тоже кажется не совсем удобным создавать функцию с параметрами plates и platesList, зная, что потом будем вызывать её с аргументами items и itemsList. Ну то есть сразу создать функцию populateList(items, itemsList); кажется проще и удобнее

Здесь интересный момент, в параметрах функции присваивается значение: plates = []

platesList.innerHTML = plates.map((plate, i) => {
        return `
<li>
<input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? "checked" : ""} />
<label for="item${i}">${plate.text}</label>
</li>
`;
    }).join("");

Достаточно сложный фрагмент кода.
Попробую понять что этот код делает.

Если помнить, что platesList это на самом деле itemsList, потому что именно с таким аргументом будет вызываться данная функция, то есть список заказов, тогда вот этот фрагмент кода, это то, что мы хотим добавить в html

<li>
<input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? "checked" : ""} />
<label for="item${i}">${plate.text}</label>
</li>
`
Здесь кроме пункта списка <li> видим чекбокс с условием plate.done.
Вот и стало понятно зачем понадобилось свойство  done: false над которым я ломала голову. Оказывается, это условие, показывающее отмечен чекбокс галочкой или нет

Есть небольшая проблема - в демо автора чекбоксы у меня не отображаются. Хоть здесь, например (чей-то проект по курсу js30) чекбоксы присутствуют.

4. Функция для переключения свойства done и сохранения списка с заказами в localStorage

function toggleDone(e) {
    if (!e.target.matches("input")) return; // skip this unless it's an input
    var el = e.target;
    var index = el.dataset.index;
    items[index].done = !items[index].done;
    localStorage.setItem("items", JSON.stringify(items));
    populateList(items, itemsList);
}


if (!e.target.matches("input")) return; 

Мы проверили соответствует ли аргумент функции css-селектору input и, если нет: !e.target.matches("input") прекращаем выполнение функции.

●   var el = e.target;
    var index = el.dataset.index;
    items[index].done = !items[index].done;


Переключаем свойство done

localStorage.setItem("items", JSON.stringify(items));

Теперь, когда у нас есть массив элементов, мы можем сохранить его в LocalStorage, который сохранит этот список при обновлении страницы.

populateList(items, itemsList);

Вызываем функцию  populateList с двумя аргументами items и itemsList.

5. Добавляем слушателей

addItems.addEventListener("submit", addItem);

Событие submit возникает при отправке формы. Мы вызываем функцию addItem для добавления заказа в список


itemsList.addEventListener("click", toggleDone);

При клике по чекбоксу в списке срабатывает функция  toggleDone изменяющая состояние чекбокса и сохраняющая его в LocalStorage

populateList(items, itemsList);
Функция, обновляющая html

Всё. Задание было насыщенным и сложным.
для самостоятельной работы автор предлагает добавить ещё три кнопки, позволяющие
  • Удалить все элементы.
  • Отметить все чекбоксы.
  • Очистить все чекбоксы.

Кроме того, на мой взгляд, было бы хорошо выводить список блюд и при клике отправлять их в заказ. При этом можно менять изображение, там где картинка с рыбкой, на то, которое было заказано, благо сайт с картинками автор указал в коде: https://thenounproject.com/search/?q=fish&i=589236

Код:

var add = document.getElementById("add");
var del = document.getElementById("del");
var select = document.querySelector("select");
var icon = document.querySelector(".icon");
var list = document.querySelector(".plates");
var l = "apple";
var state = {
    items: []
};

var data = localStorage.getItem("data");
if(data) {
    state = JSON.parse(data);
}

window.onload = function() {
    var loaded = sessionStorage.getItem("loaded");
    if(loaded) {
        update();
    } else {
        sessionStorage.setItem("loaded", true);
    }
}

function update(){
    list.innerHTML = "";
    for(var i = 0; i < state.items.length; i++) {
        var li = document.createElement("li");
        li.innerText = state.items[i];
        list.appendChild(li);
    }
}

select.addEventListener("change", function(e) {
    l = this.value;
    changeLogo();
});

function changeLogo() {
    icon.style.backgroundImage = 'url("img/' + l + '.svg")'
}

function addItem(event) {
    event.preventDefault();
    var li = document.createElement("li");
    li.textContent = l;
    list.appendChild(li);
   
    state.items.push(l);
    localStorage.setItem("data", JSON.stringify(state));
}

function delItem(event){
    event.preventDefault();
    localStorage.clear();
    list.innerHTML = "";
    state.items = [];
}

add.addEventListener("click", addItem);
del.addEventListener("click", delItem);