前言
代辦事項是非常常見的習題,其中需求含括了增、刪、讀、改資料,充分的模擬到未來在操縱資料時會碰上的各種情境與問題,可以說各式各樣的軟體都是一種客製化的代辦事項。隨著助教寫一次,未來寫很多應用會輕鬆得多~
解題
第一步:製作介面
本步驟要完成的動作如下:
- 提供用戶輸入介面
- 使用 JavaScript 選取並監聽介面動靜
<!-- 提供用戶輸入介面 --><div class="todo"> <form data-todo-form class="todo__form"> <input name="userInput" type="text" placeholder="請輸入事項" required /> <input type="submit" value="提交" /> </form> <ul data-todo-list></ul></div>
常常同學會直接 <input>
上綁監聽,雖然並不會造成什麼問題但助教會更傾向使用現有的 <form>
來讓用戶提交資料,原因是 HTML 原生就支持 客戶端的資料驗證 ,可以更方便的去檢核用戶輸入的資料對錯;也可以更語意化的描述「這是一個表單,等待用戶填寫內容」,而非「這是畫面上隨便一個可以輸入的欄位」。
<!-- 使用驗證強制要求用戶必須輸入內容 --><input type="text" required />
接著就是使用 JavaScript 選取並監聽介面動靜 (表單提交與事項清單點擊)
// 使用 JavaScript 選取並監聽介面動靜const todoForm = document.querySelector('[data-todo-form]');const todoList = document.querySelector('[data-todo-list]');
// 表單當提交時……todoForm.addEventListener('submit', (e) => { // 取消瀏覽器預設行為(跳轉頁面) e.preventDefault();});
// 當事項清單被點擊時……todoList.addEventListener('click', (e) => {});
第二步:顯示代辦事項
在介面架構好之後就需要於第一次網頁加載時將 JavaScript 中的資料顯示到畫面上,因此先定義一筆資料 (在此填充了假資料,方便同學理解):
// id = 事項獨一無二的辨識碼// name = 事項的名稱// isCompleted = 事項完成狀態
let tasks = [ { id: '1', name: 'Learn HTML', isCompleted: false, }, { id: '2', name: 'Learn CSS', isCompleted: true, },];
之後就是考慮如何顯示這筆資料,由於這是一個會頻繁執行的動作,製作一個函式將功能包裝起來,方便之後引用:
function renderTask(tasks) { // 抓取 task 資料,並用 map 跑過一輪,在每次迭代中把資料取出並放入 HTML 中回傳 const tasksHTML = tasks.map( (task) => ` <li task-id=${task.id}> <input id=${task.id} data-task-toggle type="checkbox" ${task.isCompleted ? 'checked' : ''}> <label for=${task.id}>${task.name}</label> <button data-task-delete>刪除</button> </li> `, ); todoList.innerHTML = tasksHTML.join('');}
renderTask(tasks);
可以特別注意這裡使用了現有的資料作判斷,當「isCompleted 為 true」就為 input 添加打勾否則不打勾。
<input ${task.isCompleted ? "checked" : ""}>
第三步:新增代辦事項
後續步驟就簡單很多了,新增事項流程上就是:
- 偵測表單被提交
- 將新增的事項名稱丟給 addTask 函式處理
- 加入一筆新的資料給 tasks
- 呼叫 renderTask 重新渲染一次
如果不想要因為一個事項改動就觸發整個代辦事項重新渲染一次,可以作細微的操控去編輯 DOM,不過在本次解題中先採取最簡單直觀的方式 :更新完資料就整體刷新再顯示。
// 01.偵測表單被提交todoForm.addEventListener('submit', (e) => { e.preventDefault(); // 02.將提交的事項名稱傳入 addTask 函式 addTask(e.target.userInput.value); // 重置表單內容 e.target.reset();});
function addTask(taskName) { // 03.加入新的資料給 tasks,tasks 等於(舊的 tasks + 新事項) tasks = [ ...tasks, { id: Date.now().toString(), name: taskName, isCompleted: false, }, ]; // 04.重新渲染一次 renderTask(tasks);}
這裡使用 Date.now() 來得出自 1970/01/01 00:00:00 UTC 起經過的毫秒數作為 ID 使用。
Date.now().toString();
第四步:刪除代辦事項
要刪除事項,就勢必要得知「點擊下去的刪除按鈕是哪個事項」,再根據事項 ID 與目前 tasks
資料去交互比對,如果相同就移除這筆資料並重新渲染一次所有事項。
刪除事項流程就是:
- 偵測代辦清單是否被點擊
- 確認被點擊的是刪除按鈕
- 取出 task-id 屬性
- 傳入 removeTask 函式中
- 在 removeTask 函式中比對 tasks 中相同 task-id 的事項
- 刪除 tasks 中該筆事項
- 呼叫 renderTask 重新渲染一次
// 監聽代辦清單是否被點擊todoList.addEventListener('click', (e) => { // 當被點擊時,確認被點擊的事項有 "data-task-delete" 屬性 if (e.target.hasAttribute('data-task-delete')) { // 如果有,將其父元素上的 task-id 屬性取出,傳入 removeTasks 函式內 const taskId = e.target.parentElement.getAttribute('task-id'); removeTask(taskId); }});
這裡助教使用了一個陣列方法 reduce 來快速的剃除不需要的資料(curr.id === targetId)。
function removeTask(targetId) { // 比對 tasks 中相同 task-id 的事項, // 最終 tasks 等於(過濾掉刪除事項的事項) tasks = tasks.reduce((prev, curr) => { if (curr.id === targetId) { return prev; } return [...prev, curr]; }, []); renderTask(tasks);}
第五步:切換代辦事項
切換事項狀態的思路和刪除事項非常相似,切換事項流程就是:
- 偵測代辦清單是否被點擊
- 確認被點擊的是核取方塊
- 取出
task-id
屬性 - 傳入
toggleTask
函式中 - 在
toggleTask
函式中比對tasks
中相同task-id
的事項 - 切換 tasks 中該筆事項的狀態
// 監聽代辦清單是否被點擊todoList.addEventListener('click', (e) => { // 當被點擊時,確認被點擊的事項有 "data-task-toggle" 屬性 if (e.target.hasAttribute('data-task-toggle')) { // 如果有,將其父元素上的 task-id 屬性取出,傳入 toggleTask 函式內 const taskId = e.target.parentElement.getAttribute('task-id'); toggleTask(taskId); }});
function toggleTask(targetId) { // 跑過 tasks 中每件事項,如果 id 相同,翻轉該事項的完成狀態 tasks.forEach((task) => { if (task.id === targetId) { task.isCompleted = !task.isCompleted; } });}
結語
See the Pen Todo vanilla JS by Riceball (
@riecball) on CodePen.
以上是助教製作整個代辦事項題目的思考歷程,同學可以參考但不用奉為宗旨死背或直接複製貼上,實際操作一次效果最佳~