大家好!今天是實作的第二天,昨天完成了基礎功能後,今天我們要來新增關於todo list的新功能:日曆、時間和添加分類區塊功能。
const categories: string[] = ["工作", "個人"];
type Category = string;
type Filter = "all" | Category;
這邊是今天改動最大的一個地方。昨天Category是用聯合型別"work" | "personal"寫死的,優點是TypeScript檢查很嚴謹,但缺點就是使用者沒辦法自己加新分類。
今天要讓使用者可以動態新增分類,Category沒辦法再用聯合型別表示,需要放寬成type Category = string。取而代之的是用一個categories: string[]陣列,在執行期去維護目前有哪些分類,預設先放「工作」跟「個人」兩個預設值。
另外Todo interface也多加了兩個欄位:
interface Todo {
id: number;
title: string;
category: Category;
completed: boolean;
date: string;
time: string;
}
date跟time分別對應日期跟時間輸入框的值,先都用string表示,因為<input type="date">和<input type="time">的 .value 本身就是字串格式。
const dateInput = getElement<HTMLInputElement>("#date-input");
const timeInput = getElement<HTMLInputElement>("#time-input");
const formMessage = getElement<HTMLParagraphElement>("#form-message");
const categoryInput = getElement<HTMLInputElement>("#category-input");
const addCategoryButton = getElement<HTMLButtonElement>("#add-category-button");
const filterButtonsContainer = getElement<HTMLDivElement>("#filter-buttons");
沿用昨天寫好的getElement<T>工具函式,把今天新增的欄位寫好:
dateInput、timeInput:日期和時間的輸入框。formMessage:一個顯示表單錯誤訊息的區塊(例如「請輸入任務名稱」),今天把錯誤提示獨立出來,不再用瀏覽器內建的alert。categoryInput、addCategoryButton:新增分類用的輸入框跟按鈕。filterButtonsContainer:篩選按鈕現在改成動態產生了,所以要的是裝篩選按鈕的容器,而不是HTML裡的按鈕。addCategoryButton.addEventListener("click", addCategory);
categoryInput.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
addCategory();
}
});
按按鈕或是在輸入框按Enter都可以觸發新增分類。
function addCategory(): void {
const name = categoryInput.value.trim();
if (name === "") {
formMessage.textContent = "請輸入分類名稱。";
return;
}
if (categories.some((category) => category.toLowerCase() === name.toLowerCase())) {
formMessage.textContent = "這個分類已經存在。";
return;
}
categories.push(name);
categoryInput.value = "";
formMessage.textContent = "";
renderCategories();
categorySelect.value = name;
}
trim()將空字串擋掉,並在formMessage顯示提示。.some()加上.toLowerCase()檢查有沒有重複的分類名稱(這裡忽略大小寫,避免使用者打「Work」跟「work」變成兩個不同分類,造成資料混亂)。push進categories陣列。renderCategories()重整畫面。function renderCategories(): void {
categorySelect.replaceChildren();
categories.forEach((category) => {
const option = document.createElement("option");
option.value = category;
option.textContent = category;
categorySelect.append(option);
});
filterButtonsContainer.replaceChildren();
["all", ...categories].forEach((filter) => {
const button = document.createElement("button");
button.type = "button";
button.className = filter === currentFilter ? "filter-button active" : "filter-button";
button.textContent = filter === "all" ? "所有" : filter;
button.addEventListener("click", () => {
currentFilter = filter;
renderCategories();
renderTodos();
});
filterButtonsContainer.append(button);
});
}
因為分類現在是動態的,代表兩個地方都要跟著動態產生:
新增待辦事項時的分類下拉選單:把categorySelect裡面舊的<option>全部清空,再根據目前的categories陣列重新生一份<option>出來。
篩選按鈕:昨天的篩選按鈕是寫死在 HTML 內部,今天改成用["all", ...categories]去跑迴圈動態產生按鈕,所以不管使用者新增幾個分類,篩選按鈕都會自動跟著生出來,不用去動HTML。
還有一個地方需要注意,因為篩選按鈕是動態生成的,所以這裡的也需要做更改:
button.className = filter === currentFilter ? "filter-button active" : "filter-button";
直接在生成按鈕的當下比對filter === currentFilter,是的話就加上active樣式。也因為這樣,昨天寫的updateActiveFilterButton這個函式今天就整個用不到了,改成每次切換篩選都直接重跑一次renderCategories(),讓按鈕自己重新畫一次。
function addTodo(): void {
const title = todoInput.value.trim();
const date = dateInput.value;
const time = timeInput.value;
if (title === "") {
formMessage.textContent = "請輸入任務名稱。";
return;
}
if (date === "") {
formMessage.textContent = "請選擇日期。";
return;
}
if (time === "") {
formMessage.textContent = "請選擇時間。";
return;
}
todos.push({
id: Date.now(),
title,
category: categorySelect.value,
completed: false,
date,
time,
});
todoInput.value = "";
dateInput.value = "";
timeInput.value = "";
formMessage.textContent = "";
renderTodos();
}
跟昨天比起來,今天多了日期跟時間:標題、日期、時間三個欄位分開檢查,哪個沒填就顯示對應的錯誤訊息。全部驗證通過才會把新的待辦事項push進todos,並在新增成功後把三個欄位跟錯誤訊息都清空。
const details = document.createElement("span");
details.className = "todo-details";
details.textContent = `📅 ${todo.date} ⏰ ${todo.time} 🏷 ${todo.category}`;
renderTodos()裡面,原本昨天是分開兩個<span>顯示標題跟分類,今天把分類跟新加的日期時間合併成一個details區塊,用emoji當作圖示,把日期、時間、分類這三個資訊一次呈現,畫面上看起來資訊也比較完整。
renderCategories();
renderTodos();
因為現在下拉選單跟篩選按鈕都是動態產生的,所以頁面一開始載入的時候,除了昨天就有的renderTodos(),還要多呼叫一次renderCategories(),確保分類選單跟篩選按鈕在畫面剛出現的時候就先渲染好,不會等到使用者操作才生成。
今天在原先的基礎上新增了兩項功能,明天做好後端資料庫後就能收尾了~大家明天見!