iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 15
0
Modern Web

JS30 錄系列 第 15

Day 15 - Local Storage

任務目標

製作一個待辦清單,該清單所儲存的待辦事項即使網頁關掉再重開,記錄仍是保存的。

作法

一個簡單的待辦清單架構如下

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

若暫時不考慮永久儲存的功能,我們先讓待辦清單能夠順利顯示、以及能夠手動增加新項目就好。邏輯如下:

  1. 清單的資料被儲存在陣列內
  2. 當網頁載入的當下,清單資料必須能夠被顯示在畫面上
  3. 每當「Add item」按鈕被按下時,清單資料會增加一筆新的,並且將新的資料重新顯示到畫面上

先來看看如何實現第一點與第二點:

// 儲存清單項目的陣列
let items = [];
// 網頁上裝著清單項目的元素 <ul>
const itemsList = document.querySelector('.plates');

// 顯示清單項目
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('');
}

// 網頁載入後先執行
populateList(items, itemsList)

populateList 函式中有兩個參數,第一個參數為待辦清單的資料,預設參數為空陣列。預設參數允許我們在缺乏輸入參數或是輸入參數值為 undefined 的時後使用預設的值充當參數,適當使用能夠避免一些意外狀況。

第二個參數為轉換格式後的清單資料輸出的對象,這裡為 <ul> 標籤。

在函式內部,我們只是單純將資料轉換為HTML格式的標籤組合而成的字串並塞回輸出對象 <ul> 內部而已。

整個流程為,讀取 items 陣列內記錄的待辦事項資料,轉換成HTML格式後貼到元素內,如此而已。

完成函數後,在最下方執行 populateList(items, itemsList)

到此,網頁一載入就會先將所有待辦事項顯示到畫面中。 接下來要實現第三點,增加資料:

function addItem(e) {
  // 得先把 submit 預設的功能取消
  e.preventDefault();
  // 從使用者輸入欄位得到新資料, 處理後加到資料陣列內
  const text = this.querySelector('[name=item]').value;
  const item = {
    text,
    done: false
  };
  items.push(item);
  // 顯示更新後的資料到畫面上
  populateList(items, itemsList);
  // 將輸入欄位清空
  this.reset();
}

// 監聽到 `submit` 就執行函式
addItems.addEventListener('submit', addItem);

我們用 type 屬性為 submit 的按鈕作為用來增加資料的按鈕,好處是輸入完資料後,不管按 Enter 或是按按鈕都會觸發表單的 submit 事件,壞處是預設的 submit 事件觸發後,視同提交該表單,會重新整理一次頁面。

先設立監聽器監聽 submit 事件。在自訂函數內,第一步必須先將預設回應的動作取消,監聽器回傳的事件物件中有個方法為 preventDefault() 就是用來做這件事。

待辦清單的資料陣列 items 中,每個陣列元素代表一項待辦事項,可用物件表示。需要有兩個屬性,一個為事項內容,以字串表示;另一個為事項完成與否,以布林值表示。

為此我們可以將取得的每筆新資料整理為儲存上述資訊的物件,再用 push 新增到資料陣列中。

事項內容屬性 text 使用了 ES6 新增的語法糖,原本我們要將儲存著新輸入資料的變數 text 指定給該屬性,由於命名相同,我們可以直接簡寫,輸入該變數名稱即可。

預設事項剛建立時都是未完成的,因此 done 預設為 false

推進去資料陣列後,將新內容更新到畫面上,再將輸入欄位清空即可。

接下來是重頭戲,雖然現在能夠順利增加清單,但是清單卻不具備儲存功能,一但網頁重新整理、或是關閉瀏覽器頁面,資料就要重來,因此需要一個能夠永久儲存資訊的方法。而瀏覽器本身提供的 Local Storage 就是這次的救星!

Local Storage

Web Storage API 是瀏覽器本身提供一套標準化的API,透過該API,我們能夠很輕易的在客戶端的瀏覽器中儲存資料,資料格式基本上為 Key - Value pair ,類似物件的屬性一樣,有個資料名稱(key)和與之配對的資料值(value)。被儲存的資料們會自動被轉換成字串格式。

儲存資料的方式分為兩種,一種是 sessionStorage ,該種方式儲存的資料為具有生命週期的資料,其生命週期僅維持到瀏覽器關閉,並非我們首選。另一種為 localStorage,其資料的生命週期為永久,就是我們這次要使用的。

使用方法很簡單,利用 localStorage 提供的 setItem() 方法,第一個參數是 key , 第二個參數是 value 。讀取則是用 getItem() 方法,刪除資料則是用 removeItem() 方法,參數皆同上。

要引入 localStorage ,我們先讓每次新增待辦事項時會將資料寫入 localStorage 內。在 addItemthis.reset() 前加入下列程式碼:

function addItem(e) {
  // 前略, 加入 local... 這行
  localStorage.setItem('items', JSON.stringify(items));
  this.reset();
}

每次新增資料後,將整包 items 陣列用 JSON.stringify 轉換為字串後存入 localStorage 內,名稱為 items。為什麼要先轉換成字串呢?前面有提到 localStorage 只能分辨及儲存字串,它不懂陣列解析。若未轉換成字串, localStorage 只會存下代表該陣列的名稱而已,而非內容。

接下來實作讀取資料。

原本資料會在重開瀏覽器後消失,是因為 <script> 會在每次重新載入網頁時執行,因此 items 陣列就會被初始化為空陣列,只要改變 items 陣列,讓其初始化時就從 localStorage 中讀取資料就好。
程式如下:

const items = JSON.parse(localStorage.getItem('items')) || [];

意思為如果 localStorage 有資料,就用 JSON.parse() 將該資料由字串轉換回 Javascript 看得懂的格式,在此為陣列。然後再指定給 items , 否則初始化為空陣列。

到此,資料就會被永久儲存了。

若有勾選或取消特定待辦事項是否完成,該如何讓 localStorage 存取其狀態?一樣的道理,看以下程式碼:

// <ul> 元素本身
const itemsList = document.querySelector('.plates');

function toggleDone(e) {
  // 我們只需要針對 <input> checkbox 做回應
  if (!e.target.matches('input')) return;
  const el = e.target;
  const index = el.dataset.index;
  items[index].done = !items[index].done;
  // 更新完 items 資料後再存到 localStorage 內
  localStorage.setItem('items', JSON.stringify(items));
  populateList(items, itemsList);
}

// 監聽按鍵事件
itemsList.addEventListener('click', toggleDone);

Event.target 指向觸發該事件的元素,在這裡每次點擊會有多項元素被觸發,但我們只需要回應一項觸發,利用 Element.matches ,能夠篩出選擇器名稱的元素。

在先前的 populateList 函式中,資料整理為HTML格式後,單項為以下結構:

<li>
  <input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? 'checked' : ''} />
  <label for="item${i}">${plate.text}</label>
</li>

也就是說,利用 data-index 這個屬性,我們讓每項待辦事項的HTML標籤都能夠與 items 陣列內相同鍵值的資料相互對應。

因此我們只需要讀取該元素的 data-index 值,找出具有相同值的陣列索引,再修改其資料就好。

完成後將結果儲存到 localStorage 內,便能永久紀錄待辦事項的完成狀況。

事實上,只要依循下列模式,便能依序做出刪除資料、全選資料、清除所有已選取資料等功能:

  1. 更動 items 內資料
  2. 將更動過的 items 陣列儲存到 localStorage 內
  3. 呼叫 populateList 更新畫面

實作功能在下方參考資料中,有興趣可以研究,也歡迎一起討論!

以上就是 JS30 第十五篇!

Reference

預設參數
Local Storage
Event.preventDefault
JSON.Stringify
Element.matches
練習的完整程式碼


上一篇
Day 14 - Reference Copy or Value Copy ?
下一篇
Day 16 - Mouse Move Shadow
系列文
JS30 錄30

尚未有邦友留言

立即登入留言