witch@&*weaves

30daysJS-15 - LocalStorage

这个练习的要求是:

  1. 输入文本, 点击按钮添加入指定区域;
  2. 点击todo单项,可以改变图片,表示完成或者未完成;
  3. 能够存入本地数据,刷新后依旧能显示之前写的todo。

我的写法

我之前练习的时候写过一个有本地保存功能的todo,所以这个不难。

todo.gif

所以我按照以前思路先写了一遍,这次试着多分解成几个函数,写完瞟了一眼老师答案感觉要遭,怎么这么短,不过我也明白原因,我之前写得非常命令式,添加就写“点击→创建→修改DOM→修改本地数据”,DOM和数据都混在一起了,于是就进行了修改,最终思路:

用户事件只会进行数据修改,所有修改数据先统计入本地储存,再用本地存储统一生成todo。

也就是看起来是用户操作了DOM,实际上在中间进行了一层转手,用户操作只调整了本地数据,本地数据生成DOM这点是不会受到用户操作影响的。

结果:

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

document.addEventListener("DOMContentLoaded", render);

addItems.addEventListener("submit", add);
itemsList.addEventListener("click", iconChange);

function render() {
  itemsList.innerHTML = "";

  items.forEach((item: any) => {
    const li = document.createElement("li");

    li.dataset.id = item.id;
    li.innerHTML = `
      <input type="checkbox" ${item.isChecked}>
         <label></label>${item.content}
      `;
    itemsList.appendChild(li);
  });
  console.log();
}

function add(e: Event) {
  e.preventDefault();
  if (!e.target) return;

  const text = document.querySelector('input[type="text"]') as HTMLInputElement;

  const id = String(Date.now());
  let liItem = {
    content: text.value,
    id: id,
    isChecked: "",
  };

  items.push(liItem);
  save();
  render();
}

function iconChange(e: Event) {
  if (!e.target) return;

  const element = e.target as HTMLElement;
  let check;
  if (element.tagName === "LABEL") {
    const li = element.closest("li") as HTMLElement;
    const input = li.querySelector("input") as HTMLInputElement;

    if (input.checked === true) {
      check = "";
    } else if (input.checked === false) {
      check = "checked";
    }

    items.find((item: any) => {
      return item.id === li.dataset.id;
    }).isChecked = check;
    save();
    render();
  }
}

function save() {
  localStorage.setItem("items", JSON.stringify(items));
}

老师答案

写完我就去看答案了,老师的this用得真是炉火纯青啊,我就总是想不起来用它。

老师的模板字符串和数组结合的用法很好,对数组本质很有理解:

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('');
    

直接遍历所有元素,去掉间隔符后添加入ul里当innerHTML。

另外就是点击切换图标的部分了,关键代码有以下几处:

${plate.done ? 'checked' : ''}

不是像我那样使用字符串当值,而是使用布尔值当值,并使用三元运算符在生成时判定此处的值。

items[index].done = !items[index].done

不通过点击确认点击内容的方式判定显示形式,而是进行了抽象,每次点击都是为了获得与上次点击相反的结果,所以就列出了这个状态机。

index是map里回调函数的参数,可以直接通过items[index]访问数据修改,我是通过为每一个数据添加值为时间戳的data属性来定位的,老师的方法不太适合乱序或者可以删除某一项的场景。

之后再直接把修改后的items存入本地,生成。

知识点

数组

map方法和foreach都可以遍历数组,通过回调函数对每个元素进行操作,返回新值,区别是前者返回新数组,后者是直接修改原数组。

map有参数callbackfn,以及thisArg,callbackfn有参数element, index, array。

在上面的应用中,数组作为参数传入,老师选择使用map可以一石二鸟,一是为了避免直接修改原数组,二是每次遍历参数数组,便会通过回调函数生成一个新的li元素,传入新数组中。

join()方法将所有元素连接成一个字符串,并返回这个字符串,用指定的分隔符字符串分隔,这里分隔符字符串为空'',所以直接返回了一长串字符串,innerHTML确实是一串字符串哦!

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('');
  }

localStorage & JSON

JSON是一种数据表现格式,可以作为一个对象或者字符串存在,作为对象时它的键都是字符串,必须使用双引号。

它也可以进行对象和字符串两种形式的转换:

JSON.parse() //转换成对象
JSON.stringify() //转换成字符串

localStorage则是在浏览器本地长期存储数据,浏览器关闭并不会使数据消失,也是以键值对的方式存储,它只能存储字符串。

localStorage.setItem(key,value) //存储
localStorage.removeItem(key) //删除
localStorage.getItem(key) //获取
localStorage.clear()

所以利用JSON可以转换字符串和对象的特性,将数据存储为JSON对象格式,再转换成JSON字符串,把其作为Item键的值存储入localStorage中。

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

读取就是反过来,先从localStorage中读取,再转换成对象。

JSON.parse(localStorage.getItem("items"))

获取节点上下左右

获取上级元素:

element.closet()

获取下级元素:

elemet.querySeletor()

获取同级相邻元素:

element.nextElementSibling() //下一个
element.previousElementSibling() //上一个

这些都是通过某一元素获取其它元素,还有一种是直接检定点击节点是不是想要的元素,通过检定元素的字符串来确定。

element.matches(selectors)

测试element是否会被指定的CSS选择器选择,返回 true或false。

答案

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>LocalStorage</title>
  <link rel="stylesheet" href="style.css">
  <link rel="icon" href="https://fav.farm/✅" />
</head>
<body>
  <!--
      Fish SVG Cred:
      https://thenounproject.com/search/?q=fish&i=589236
   -->

   <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"><g><path d="M495.9,425.3H16.1c-5.2,0-10.1,2.9-12.5,7.6c-2.4,4.7-2.1,10.3,0.9,14.6l39,56.4c2.6,3.8,7,6.1,11.6,6.1h401.7   c4.6,0,9-2.3,11.6-6.1l39-56.4c3-4.3,3.3-9.9,0.9-14.6C506,428.2,501.1,425.3,495.9,425.3z M449.4,481.8H62.6L43,453.6H469   L449.4,481.8z"/><path d="M158.3,122c7.8,0,14.1-6.3,14.1-14.1V43.4c0-7.8-6.3-14.1-14.1-14.1c-7.8,0-14.1,6.3-14.1,14.1v64.5   C144.2,115.7,150.5,122,158.3,122z"/><path d="M245.1,94.7c7.8,0,14.1-6.3,14.1-14.1V16.1c0-7.8-6.3-14.1-14.1-14.1C237.3,2,231,8.3,231,16.1v64.5   C231,88.4,237.3,94.7,245.1,94.7z"/><path d="M331.9,122c7.8,0,14.1-6.3,14.1-14.1V43.4c0-7.8-6.3-14.1-14.1-14.1s-14.1,6.3-14.1,14.1v64.5   C317.8,115.7,324.1,122,331.9,122z"/><path d="M9.6,385.2c5.3,2.8,11.8,1.9,16.2-2.2l50.6-47.7c56.7,46.5,126.6,71.9,198.3,71.9c0,0,0,0,0,0   c87.5,0,169.7-36.6,231.4-103.2c5-5.4,5-13.8,0-19.2c-61.8-66.5-144-103.2-231.4-103.2c-72,0-142.2,25.6-199,72.5l-50-47.1   c-4.4-4.1-10.9-5-16.2-2.2c-5.3,2.8-8.3,8.7-7.4,14.6l11.6,75L2.2,370.6C1.3,376.5,4.2,382.4,9.6,385.2z M380.9,230.8   c34.9,14.3,67.2,35.7,95.3,63.6c-10.1,10-20.8,19.2-31.9,27.5c-22.4-3.3-29.6-8.8-30.7-9.7c-4-5.7-11.8-7.7-18.1-4.4   c-6.9,3.6-9.5,12.2-5.9,19.1c1.9,3.5,7.3,10.3,22.4,16c-10.1,5.7-20.5,10.7-31.1,15.1C352.4,320.2,352.4,268.6,380.9,230.8z    M36.3,255.6l29.4,27.7c5.3,5,13.6,5.1,19.1,0.3c53.2-47.6,120.7-73.7,190-73.7c26.9,0,53.2,3.9,78.5,11.3   c-29.3,44.6-29.3,102,0,146.6c-25.3,7.4-51.6,11.3-78.5,11.3c-69,0-136.3-26-189.4-73.2c-2.7-2.4-13.4-6.3-19.1,0.3l-30.1,28.3   l5.7-40C42.2,293,36.3,255.6,36.3,255.6z"/><circle cx="398.8" cy="273.8" r="14.1"/></g></svg>

  <div class="wrapper">
    <h2>LOCAL TAPAS</h2>
    <p></p>
    <ul class="plates">
      <li>Loading Tapas...</li>
    </ul>
    <form class="add-items">
      <input type="text" name="item" placeholder="Item Name" required>
      <input type="submit" value="+ Add Item">
    </form>
  </div>

<script>
  const addItems = document.querySelector('.add-items');
  const itemsList = document.querySelector('.plates');
  const items = JSON.parse(localStorage.getItem('items')) || [];

  function addItem(e) {
    e.preventDefault();
    const text = (this.querySelector('[name=item]')).value;
    const item = {
      text,
      done: false
    };

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

  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 toggleDone(e) {
    if (!e.target.matches('input')) return; // skip this unless it's an input
    const el = e.target;
    const index = el.dataset.index;
    items[index].done = !items[index].done;
    localStorage.setItem('items', JSON.stringify(items));
    populateList(items, itemsList);
  }

  addItems.addEventListener('submit', addItem);
  itemsList.addEventListener('click', toggleDone);

  populateList(items, itemsList);

</script>


</body>
</html>

#code