Как работать с localStorage

Пытаюсь создать подобие чек-листа. Не могу разобраться с localstorage. Ответил на собственный вопрос(см.ниже). Но возникла проблема при перезагрузке страницы сохраненные данные невозможно отредактировать. Можно ли решить эту проблему? Или придется другим способом реализовать чек-лист?

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Список дел</title>
    <link rel="stylesheet" href="style.css">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>

<div class="page">
  <div class="container">
    <h2>Список дел</h2>
    <div class="main">
      <input id="add" type="text" placeholder="Введите описание вашего дела...">
      <button id="button_add">Добавить</button>
    </div>
    <hr><ul id="check_list"></ul>
  </div>
</div>

<script src="app.js"></script>

</body>
</html>
#deleteTask {
width: 18px;
height: 18px;
opacity: 0.3;
margin-left: 10px;
}

#deleteTask:hover {
  opacity: 1;
  cursor: pointer;
}

#deleteTask:before, #deleteTask:after {
position: absolute;
content: ' ';
height: 15px;
width: 2px;
background-color: #333;
}

#deleteTask:before {
transform: rotate(45deg);
}

#deleteTask:after {
transform: rotate(-45deg);
}

.done span {
  text-decoration: line-through;
  color: gray;
}
let buttonAdd = document.getElementById('button_add');
let checklist = document.getElementById('check_list');
let add = document.getElementById('add');

buttonAdd.addEventListener('click', function(){

  if (add.value === ''){
    return alert('Ошибка: Пустая строка');
  }
  let li = document.createElement('li');
  let newTask = li.innerHTML =`
    <span id = 'newTask' data-elem = 'text'>` + add.value + `</span>
    <input id = 'checkTask' data-elem = 'checkbox' type = 'checkbox'>
    <span id = 'deleteTask' data-elem = 'remove'></span>`;
  add.value = '';
  checklist.appendChild(li);

  li.addEventListener('click', function(event) {
    if (event.target.dataset.elem) {
      switch (event.target.dataset.elem) {
        case 'text':
          let span = event.target;
          let input = document.createElement('input');
          input.value = event.target.innerHTML;
          span.innerHTML = '';
          span.appendChild(input);
          input.addEventListener('keydown', function(e) {
            if (e.keyCode === 13) {
              span.innerHTML = this.value;
            };
          });
        break;
        case 'checkbox':
          li.classList.toggle('done');
        break;
        case 'remove':
          li.parentElement.removeChild(li);
          event.preventDefault();
        break;
      };
    };
  });
});

Ответы (3 шт):

Автор решения: Ивашка

Сделал чтобы вам понять примерный принцип действия:

let buttonAdd = document.getElementById('button_add');
let checklist = document.getElementById('check_list');
let add = document.getElementById('add');
const tasks = localStorage.getItem('data') ? JSON.parse(localStorage.getItem('data')) : []
console.log(tasks)

tasks.forEach((item) => {
  let li = document.createElement('li');
  li.innerHTML = item
  checklist.appendChild(li);
})

const saveTasks = () => {
  localStorage.setItem('data', JSON.stringify(tasks));
}

buttonAdd.addEventListener('click', function(){

  if (add.value === ''){
    return alert('Ошибка: Пустая строка');
  }
  let li = document.createElement('li');
  let newTask = li.innerHTML =`
    <span id = 'newTask' data-elem = 'text'>` + add.value + `</span>
    <input id = 'checkTask' data-elem = 'checkbox' type = 'checkbox'>
    <span id = 'deleteTask' data-elem = 'remove'></span>`;
  add.value = '';
  const taskId = tasks.push(newTask)
  saveTasks()
  checklist.appendChild(li);

  li.addEventListener('click', function(event) {
    if (event.target.dataset.elem) {
      switch (event.target.dataset.elem) {
        case 'text':
          let span = event.target;
          let input = document.createElement('input');
          input.value = event.target.innerHTML;
          span.innerHTML = '';
          span.appendChild(input);
          input.addEventListener('keydown', function(e) {
            if (e.keyCode === 13) {
              span.innerHTML = this.value;
            };
          });
        break;
        case 'checkbox':
          li.classList.toggle('done');
        break;
        case 'remove':
            tasks.splice(newTask, 1)
            saveTasks()
          li.parentElement.removeChild(li);
          event.preventDefault();
        break;
      };
    };
  });
});

Хотя по-хорошему тут надо весь код переписывать. В частности список должен быть сделан в виде массива объектов, а при добавлении в массив и удалении - список нужно перерисовывать.

→ Ссылка
Автор решения: Joji

Собственно получилось сохранить. Но при перезагрузке страницы. Редактирование сохраненной строки не представляется возможным. Можно ли это исправить?

let buttonAdd = document.getElementById('button_add');
let checklist = document.getElementById('check_list');
let add = document.getElementById('add');

let todo;
function toLocal(){
  todo = checklist.innerHTML;
  localStorage.setItem('todo', todo);
}
buttonAdd.addEventListener('click', function newElem(){

  if (add.value === ''){
    return alert('Ошибка: Пустая строка');
  }
  let li = document.createElement('li');
  let newTask = li.innerHTML =`
    <span id = 'newTask' data-elem = 'text'>` + add.value + `</span>
    <input id = 'checkTask' data-elem = 'checkbox' type = 'checkbox'>
    <span id = 'deleteTask' data-elem = 'remove'></span>`;
  add.value = '';
  checklist.appendChild(li);
  li.addEventListener('click', function(event) {
    if (event.target.dataset.elem) {
      switch (event.target.dataset.elem) {
        case 'text':
          let span = event.target;
          let input = document.createElement('input');
          input.value = event.target.innerHTML;
          span.innerHTML = '';
          span.appendChild(input);
          input.addEventListener('keydown', function(e) {
            if (e.keyCode === 13) {
              span.innerHTML = this.value;
            };
          });
        toLocal();
        break;
        case 'checkbox':
          li.classList.toggle('done');
        toLocal();
        break;
        case 'remove':
          li.parentElement.removeChild(li);
          event.preventDefault();
        toLocal();
        break;
      };
    };
  });
});

if(localStorage.getItem('todo')) {
  checklist.innerHTML = localStorage.getItem("todo");
};
→ Ссылка
Автор решения: OPTIMUS PRIME

Хотел добавить пару строчек, случайно получилось 150+ )

У каждой записи должен быть свой уникальный идентификатор. В таком случае все записи можно хранить в объекте:

{
  "0": { text: 'запись-1' },
  "1": { text: 'запись-2' },
  "8": { text: 'запись-8', checked: 1 },
}

Состояние checked тоже придется хранить, чтобы после обновления страницы вернуть все галочки на свои места. А т.к. в localStorage можно хранить только строки, для сохранения объектов необходимо использовать JSON.stringify(obj), а для чтения - JSON.parse(string)

+ Вы и так использовали делегирование событий, но отдельно для каждого <li>. Заодно вынес обработчики на родительский <ul>: При добавлении / удалении записей не придется вешать новые обработчики.


Приставки storage, item намекают, что здесь напрашивается группирование в объекты или классы. Но пока обойдемся разбросанными функциями.

* Код рабочий, но localStorage не работает при запуске на этом сайте. Пробуйте здесь

const storage_KEY = 'check_list';

let storage_items = (function get_saved_data() {

  let items = localStorage.getItem(storage_KEY);
  
  try {
  
    let data = JSON.parse(items);
    
    if (!data) throw new Error('no data');
    
    if (typeof data != 'object') {
      throw new Error('Expected Object, but found: ' + data);
    }
    
    return data;
    // Если благополучно получилось прочесть JSON, в переменную storage_items
    // попадет этот объект.
   
  } catch(e) {
  
    console.log(e);
    return {}; // иначе, новый пустой {}
  }

})();

console.log('%cITEMS:', 'color: orange', storage_items);

// Любое действие со storage, выполняет операции в объекте storage_items,
// затем вызывает storage_update(), который записывает объект в localStorage

function storage_update() {
  //test_start
  // Временный блок, чтобы поймать ошибки во время написания,
  // если вдруг прилетит id-не число.
  if (Object.keys(storage_items).some(isNaN)) {
    console.error(storage_items);
    throw new Error('NaN!');
  }
  //test_end

  localStorage.setItem(storage_KEY, JSON.stringify(storage_items));
}

function storage_empty() {
  localStorage.setItem(storage_KEY, null);
}

/***/
function storage_add(item_id, item_text) {
  storage_items[item_id] = { text: item_text };
  storage_update();
}

function storage_remove(item_id) {
  delete storage_items[item_id];
  storage_update();
}

/***/
function storage_edit_item(item_id, new_text) {
  storage_items[item_id].text = new_text;
  storage_update();
}

function storage_check_item(item_id) {
  storage_items[item_id].checked = 1;
  storage_update();
}

function storage_uncheck_item(item_id) {
  delete storage_items[item_id].checked;
  storage_update();
}

function storage_max_id() {
  // Выдает максимальное число из всех ключей (id) объекта.
  // Нужен, чтобы id новых задач не пересекались со старыми.
  return Math.max(0, ...Object.keys(storage_items));
}


/*****/
// <заметка>
// id элементов <li> создаются в виде id="task-число",
// но во все функции storage должны передаваться в виде числа.

let checklist = document.getElementById('check_list');

(function _init_list_from_storage() {
  // * Список всех записей уже получен и сохранен в объект storage_items
  let html = '';

  for (let id in storage_items) {
    let obj = storage_items[id];
    html += item_html(obj.text, id, obj.checked);
  }  
  
  checklist.insertAdjacentHTML('beforeend', html);
})();


checklist.addEventListener('click', function(e) {
  let dataset = e.target.dataset.elem;
  if (!dataset) return;

  switch (dataset) {
    case 'text'    : item_edit(e);        break;
    case 'checkbox': item_toggle_done(e); break;
    case 'remove'  : item_remove(e);      break;
  }
});

function item_edit(e) {
  // Здесь можно использовать contentEditable, не стал менять вашу реализацию
  
  let span = e.target;
  let input = document.createElement('input');
  input.value = span.textContent;
  
  span.textContent = '';
  span.appendChild(input);
  
  input.addEventListener('keydown', function(e) {
    if (e.keyCode === 13) {
      span.textContent = this.value;
      
      let id = item_numeric_id(span.closest('li'));
      storage_edit_item(id, this.value)
    }
  });
}

function item_toggle_done(e) {
  let li = e.target.closest('li');
  li.classList.toggle('done');
  
  let id = item_numeric_id(li);
  (e.target.checked ? storage_check_item : storage_uncheck_item)(id);
  // см. «Тернарный оператор».
  // Здесь выбирается одна из функций в зависимости от условия,
  // которая затем вызывается с аргументом (id);
}

function item_remove(e) {
  e.preventDefault();
  
  let li = e.target.closest('li');
  storage_remove(item_numeric_id(li));
  
  li.remove();
}

/***/
let add_btn = document.getElementById('button_add');
let add_inp = document.getElementById('add');

add_btn.addEventListener('click', function() {
  if (add_inp.value === '') {
    return alert('Ошибка: Пустая строка');
  }

  item_add(add_inp.value);  
  add.value = '';
});

/***/
let unique_id = storage_max_id() + 1;

function item_numeric_id(li) {
  // "task-11" → 11
  return Number(li.id.match(/\d+/)[0]); // «Регулярные выражения»
}

function item_html(text, id, checked) {  
  return (`
    <li id="task-${ id }" class="${ checked ? 'done': '' }">
      <span data-elem="text">${ text }</span>
      <input data-elem="checkbox" type="checkbox" ${ checked ? 'checked' : '' }>
      <span class="delete" data-elem="remove"></span>
    </li>
  `);
}

function item_add(text) {
  let id = unique_id++;
  
  checklist.insertAdjacentHTML('beforeend', item_html(text, id));
  storage_add(id, text);
}
.delete {
  width: 18px;
  height: 18px;
  opacity: 0.3;
  margin-left: 10px;
}

.delete:hover {
  opacity: 1;
  cursor: pointer;
}

.delete::before,
.delete::after {
  position: absolute;
  content: ' ';
  height: 15px;
  width: 2px;
  background-color: #333;
}

.delete::before {
  transform: rotate(45deg);
}

.delete::after {
  transform: rotate(-45deg);
}

.done span {
  text-decoration: line-through;
  color: gray;
}
<div class="page">
  <div class="container">
    <h2>Список дел</h2>
    <div class="main">
      <input id="add" type="text" placeholder="Введите описание вашего дела...">
      <button id="button_add">Добавить</button>
    </div>
    <hr>
    <ul id="check_list"></ul>
  </div>
</div>

→ Ссылка