JavaScript Todo List Exercise

JavaScript 五個步驟製作代辦事項

前言

代辦事項是非常常見的習題,其中需求含括了增、刪、讀、改資料,充分的模擬到未來在操縱資料時會碰上的各種情境與問題,可以說各式各樣的軟體都是一種客製化的代辦事項。隨著助教寫一次,未來寫很多應用會輕鬆得多~

解題

第一步:製作介面

本步驟要完成的動作如下:

  • 提供用戶輸入介面
  • 使用 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" : ""}>

第三步:新增代辦事項

後續步驟就簡單很多了,新增事項流程上就是:

  1. 偵測表單被提交
  2. 將新增的事項名稱丟給 addTask 函式處理
  3. 加入一筆新的資料給 tasks
  4. 呼叫 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 資料去交互比對,如果相同就移除這筆資料並重新渲染一次所有事項。

刪除事項流程就是:

  1. 偵測代辦清單是否被點擊
  2. 確認被點擊的是刪除按鈕
  3. 取出 task-id 屬性
  4. 傳入 removeTask 函式中
  5. 在 removeTask 函式中比對 tasks 中相同 task-id 的事項
  6. 刪除 tasks 中該筆事項
  7. 呼叫 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);
}

第五步:切換代辦事項

切換事項狀態的思路和刪除事項非常相似,切換事項流程就是:

  1. 偵測代辦清單是否被點擊
  2. 確認被點擊的是核取方塊
  3. 取出 task-id 屬性
  4. 傳入 toggleTask 函式中
  5. toggleTask 函式中比對 tasks 中相同 task-id 的事項
  6. 切換 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.

以上是助教製作整個代辦事項題目的思考歷程,同學可以參考但不用奉為宗旨死背或直接複製貼上,實際操作一次效果最佳~