大家好~經過長達快一個月的鐵人賽,在最後的這四天,我要分三天來做todo list的程式實作,最後一天會為這次的鐵人賽做個總結。
三天的規劃如下:
todo-day1/
├── index.html
├── style.css
├── main.js # 瀏覽器實際執行的JavaScript
└── src/
└── main.ts # TypeScript原始碼
type Category = "work" | "personal";
type Filter = "all" | Category;
interface Todo {
id: number;
title: string;
category: Category;
completed: boolean;
}
Category:用聯合型別限制只能是 "work" 或 "personal"區塊。Filter:直接復用Category,再加一個"all",這樣篩選條件跟分類的型別自然就是相容的,不用另外維護兩份字串。Todo interface:定義一筆待辦事項的資料結構。id:因為目前是簡單的前端練習,我這裡使用 Date.now()以目前時間為基礎的數字ID。completed :布林值管理打勾狀態。let todos: Todo[] = [];
let currentFilter: Filter = "all";
todos:存取所有待辦事項的陣列。currentFilter:記錄目前選到哪個篩選按鈕。因為還沒接資料庫,所以先用記憶體內的變數替代。function getElement<T extends HTMLElement>(selector: string): T {
const el = document.querySelector<T>(selector);
if (!el) {
throw new Error(`找不到元素:${selector}`);
}
return el;
}
const todoForm = getElement<HTMLFormElement>("#todo-form");
const todoInput = getElement<HTMLInputElement>("#todo-input");
const categorySelect = getElement<HTMLSelectElement>("#category-select");
const todoList = getElement<HTMLUListElement>("#todo-list");
const emptyMessage = getElement<HTMLParagraphElement>("#empty-message");
const filterButtons = document.querySelectorAll<HTMLButtonElement>(".filter-button");
這裡要特別說明一下,querySelector找不到元素時可能會回傳null,而且如果沒有指定泛型,TypeScript通常只能知道它是某種Element。如果沒有特別處理,之後變數後打 .value、.addEventListener會有紅波浪。為了避免這種情況發生,這裡寫了條件句處理,如果抓不到就直接丟錯誤,讓問題在一開始就丟出來。
//送出表單
todoForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
addTodo();
});
function isFilter(value: string | undefined): value is Filter {
return value === "all" || value === "work" || value === "personal";
}
//篩選按鈕
filterButtons.forEach((button) => {
button.addEventListener("click", () => {
const filter = button.dataset.filter;
if (isFilter(filter)) {
currentFilter = filter;
updateActiveFilterButton(button);
renderTodos();
}
});
});
function addTodo(): void {
const title = todoInput.value.trim();
if (title === "") return;
const category: Category = categorySelect.value === "work" ? "work" : "personal";
todos.push({
id: Date.now(),
title,
category,
completed: false,
});
todoInput.value = "";
renderTodos();
}
function toggleTodo(id: number): void {
todos = todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
renderTodos();
}
function deleteTodo(id: number): void {
todos = todos.filter((todo) => todo.id !== id);
renderTodos();
}
function filterTodos(): Todo[] {
return currentFilter === "all"
? todos
: todos.filter((todo) => todo.category === currentFilter);
}
addTodo:先使用trim()移除輸入內容前後的空白,再判斷是否為空字串。如果沒有輸入實際內容,就停止新增。分類的部分用三元運算子簡單判斷是work還是personal。新增完就清空輸入框,然後呼叫 renderTodos() 重新畫面。toggleTodo跟deleteTodo:不直接改原本陣列裡的物件,而是用map和filter建立新的陣列,再重新指定給todos。filterTodos:如果目前篩選是"all"就回傳全部,不然就用filter挑出分類相符的項目。回傳型別明確標成Todo[]。function renderTodos(): void {
const visibleTodos = filterTodos();
todoList.replaceChildren();
emptyMessage.hidden = visibleTodos.length > 0;
visibleTodos.forEach((todo) => {
const item = document.createElement("li");
item.className = "todo-item";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.completed;
checkbox.setAttribute("aria-label", `完成任務:${todo.title}`);
checkbox.addEventListener("change", () => toggleTodo(todo.id));
const title = document.createElement("span");
title.className = todo.completed ? "todo-title completed" : "todo-title";
title.textContent = todo.title;
const category = document.createElement("span");
category.className = "todo-category";
const categoryLabels: Record<Category, string> = {
work: "工作",
personal: "個人",
};
category.textContent = categoryLabels[todo.category];
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "delete-button";
deleteButton.textContent = "刪除";
deleteButton.addEventListener("click", () => deleteTodo(todo.id));
item.append(checkbox, title, category, deleteButton);
todoList.append(item);
});
}
filterTodos():拿到目前該顯示的待辦事項。todoList.replaceChildren():先把原本的待辦清單內容清空。這裡使用DOM API重新建立元素。另外,標題使用textContent設定,也能避免輸入內容被當成HTML解析。forEach把每一筆待辦事項組成一個 <li>,裡面包含勾選框、標題文字、分類標籤、刪除按鈕,逐一綁上對應的事件。function updateActiveFilterButton(activeButton: HTMLButtonElement): void {
filterButtons.forEach((button) => button.classList.remove("active"));
activeButton.classList.add("active");
}
renderTodos();
updateActiveFilterButton:負責處理動態按鈕的樣式先把所有按鈕的active class拿掉,再幫目前點的那顆加上去。今天先將todo list的基礎功能完成,明天來新增一些常用的功能,像是日歷、時間等等,今天先到這邊啦,我們明天見!