[JavaScript 30]Day15-LocalStorage and Event Delegation
今天要練習製作一個可新增、勾選的 to-do list,並將新增的項目以及勾選狀態存入 localStorage 內。練習中也能了解到 Event Delegation (事件指派)的觀念。
功能需求
- 可新增、勾選項目
- 重整後項目仍然存在
- 記錄勾選狀態
學習重點
- 監聽 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 內容。
小補充
當物件的key
和 value
的變數名稱相同時,可以使用 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 中,並在刷新頁面、新增項目後,從中取出
- 資料存入 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);