[JavaScript 30]Day15-LocalStorage and Event Delegation

Ivy Ho
IvyCodeFive
Published in
11 min readJun 19, 2022

今天要練習製作一個可新增、勾選的 to-do list,並將新增的項目以及勾選狀態存入 localStorage 內。練習中也能了解到 Event Delegation (事件指派)的觀念。

功能需求

  1. 可新增、勾選項目
  2. 重整後項目仍然存在
  3. 記錄勾選狀態

學習重點

  • 監聽 submit 事件
  • localStorage

1. 存入 : localStorage.setItem()

2. 取出: localStorage.getItem()

3. JSON 轉字串 : JSON.stringify()

4. JSON 轉回原資料型態 : JSON.parse()

  • Event Delegation
  • .map()
  • .join('')
  • data-set
  • .matches('')

觀察範例檔案初始狀態

HTML

  • 等等將會對 .plates 以及 .add-items 進行操作
  • .plates 內已有一個示意的 <li> 項目
<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>

CSS

已有一些基本的樣式設定,以及以下的 checkbox 圖形切換設定 :

.plates input + label:before {  content: "⬜️";  margin-right: 10px;}.plates input:checked + label:before {  content: "🌮";}

JS

  • 預先抓好了 .add-items.plates 的 DOM
  • 愈先定義資料 items 為一空陣列
const addItems = document.querySelector('.add-items');const itemsList = document.querySelector('.plates');const items = [];

(最後完成的完整 JS 程式碼,以及檔案下載、課程連結,將放在最下方)

解決思路

一、取得 input 輸入的值,並將資料新增至一個陣列內

二、將新增的項目顯示於畫面上

三、將資料存入 localStorage 中,並在刷新頁面、新增項目後,從中取出

四、儲存 checkbox 的勾選狀態

一、取得 input 輸入的值,並將資料新增至一個陣列內

  • 對 addItems 表單監聽 submit 事件,觸發 addItem。
  • 取得 input 內輸入的內容。
  • 組成 item 物件,加上 done 狀態布林值 。(用來切換勾選狀態)
  • 將新增的物件 push 進 items 陣列中。
  • 目前 addItem 事件內的 this 指向 addItems 這個<form>,而 <form>reset() 方法,所以我們可以透過 this.reset(); 來做到表單 submit 之後清空 input 內容。

小補充

當物件的keyvalue 的變數名稱相同時,可以使用 ES6 的寫法,只寫變數名稱即可。

二、將新增的項目顯示於畫面上

1. 新增一個 populateList function

  • 在要傳入的參數位置,可以預設 plates = []以避免忘記傳入參數導致程式碼出錯。
  • 利用 .map() 可以將 plates 陣列資料處理過後,回傳出一個新的陣列
  • .map() 處理過後的新陣列裡,一個項目是一段 html 字串,我們可以透過 .join('') 將他們全部合併成一個大字串。
  • 再透過 .innerHTML 將剛剛組好的 html 字串放進 .plateList 裡,顯示於畫面上。

小補充

  • HTML 中只要有 checked 屬性,就會是 checked 的狀態,即便給它一個值 checked="false",也會顯示為已勾選。
  • 所以可以透過 plate.done 狀態,來判斷是否加上 checked : ${plate.done ? 'checked' : ''}
  • null 在 html 上會顯示出來,所以使用空字串 '' 來切換沒有 checked 的狀態
  • 目前的作法是,新增一個項目就更新一整個列表至畫面上,如果資料更新並不頻繁,效能不會有太大問題。
    但如果資料量龐大且更新頻繁,則可能需要考慮借助於 JS 框架的優點(虛擬 DOM + Diff 算法,盡量複用 DOM 節點),讓列表在新增時,先與原本資料做比對,再單獨更新新增的項目。

2. 在上一個步驟新增的 addItems function 內,呼叫 populateList function

  • 傳入 items 和 itemsList 作為參數
  • 將 populateList function 設計為需傳入第二個參數,而不是在 function 內直接使用全域變數中的 itemsList, 是為了讓這個 function 複用性更高,如果日後有兩份菜單列表,也可以用同一個 function 做處理。

三、將資料存入 localStorage 中,並在刷新頁面、新增項目後,從中取出

  1. 資料存入 localStorage
  • 在每一次新增時,我們可以使用 localStorage.setItem() 將資料存入 localStorage 裡面
  • 而在 localStorage 中,資料型態雖然看起來也是由 key 和 value 組成,但只能使用 "字串" 的形式存入。
    所以在存入之前,需使用 JSON.stryigify() 將資料型態轉為 JSON 字串 :
localStorage.setItem('items', JSON.stringify(items));
  • 在 input 內輸入菜單項目後,我們可以打開 F12 >Application >Local Storage ,看到資料已存入 localStorage 內

2. 資料從 localStorage 中取出

  • 剛剛提到,在存入資料時,我們會先將資料用 JSON.stryigify() 轉為 JSON 字串再存入。取出時,則可以使用 JSON.parse() ,將資料轉回原來的資料型態。
  • 在最一開始,我們有預設 items 為一個空陣列,現在可以改為,先從 localStorage 中嘗試取得,如果沒有資料,再預設為空陣列:
  • 此時當我們重整頁面,會看到畫面上,並沒有顯示我們已存入 localStorage 內的項目,需要再新增一個項目,才會顯示。
  • 因此,我們可以在程式碼最底下 (外部全域環境) 再呼叫一次 populateList(),就能讓頁面重整後的一開始,就將 localStorage 內的資料顯示於畫面上。

四、儲存 checkbox 的勾選狀態

此時新增的項目雖然可勾選,但勾選狀態並沒有與我們的資料連動,需再新增以下這個 toggleDone function ,來同步存入、更新畫面 :

小補充 : Event Delegation(事件指派)

如果是跑迴圈對所有子層 <li> 元素監聽 click 事件,當我們再用 JS 新增新的 <li>項目,會發現新增的節點並不會有 click 事件的註冊
更好的作法是,只監聽父層 <ul> 一個元素,也就是 itemsList,再藉由 Event Bubbling 傳遞給子層,即為 Event Delegation(事件指派)的做法。

但當我們用事件指派的做法,通常需再判斷 e.target 是我們要的目標節點,再安排接續後續的事件內容。

如範例中,監聽了一整個 <ul>,再點擊 <ul> 範圍內的任意一處,會發現 e.target 所偵測點擊到的是 <label><input><li>等不同元素 :

function toggleDone(e) {     
console.log(e.target);
}
temsList.addEventListener('click', toggleDone);
  • 我們更希望的是,當使用者點擊到的是 <input> 的位置,再觸發後續事件,也希望能獲取 input 上的 data-index 值,所以在前面會再加上一段判斷式 :
if(!e.target.matches('input')) return;

到這邊就完成了今天的練習,頁面重整後,項目和勾選狀態,都會保存在 localStorage ,並顯示於畫面上。

完整 JS 程式碼

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

--

--

Ivy Ho
IvyCodeFive

"You don't have to be great to start, but you have to start to be great."