iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0

大家好!今天是實作的第二天,昨天完成了基礎功能後,今天我們要來新增關於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 本身就是字串格式。

二、抓取新增的 DOM 元素:

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裡的按鈕。

三、addCategory():

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;
}
  1. 先把輸入的分類名稱使用trim()將空字串擋掉,並在formMessage顯示提示。
  2. 用.some()加上.toLowerCase()檢查有沒有重複的分類名稱(這裡忽略大小寫,避免使用者打「Work」跟「work」變成兩個不同分類,造成資料混亂)。
  3. 通過檢查後才把新分類push進categories陣列。
  4. 清空輸入框、清掉錯誤訊息,然後呼叫renderCategories()重整畫面。
  5. 最後把下拉選單直接切到剛新增的分類,這樣使用者新增完分類後,不用再手動選一次,可以直接接著新增待辦事項。

四、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);
  });
}

因為分類現在是動態的,代表兩個地方都要跟著動態產生:

  1. 新增待辦事項時的分類下拉選單:把categorySelect裡面舊的<option>全部清空,再根據目前的categories陣列重新生一份<option>出來。

  2. 篩選按鈕:昨天的篩選按鈕是寫死在 HTML 內部,今天改成用["all", ...categories]去跑迴圈動態產生按鈕,所以不管使用者新增幾個分類,篩選按鈕都會自動跟著生出來,不用去動HTML。

還有一個地方需要注意,因為篩選按鈕是動態生成的,所以這裡的也需要做更改:

button.className = filter === currentFilter ? "filter-button active" : "filter-button";

直接在生成按鈕的當下比對filter === currentFilter,是的話就加上active樣式。也因為這樣,昨天寫的updateActiveFilterButton這個函式今天就整個用不到了,改成每次切換篩選都直接重跑一次renderCategories(),讓按鈕自己重新畫一次。

五、addTodo() :

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(),確保分類選單跟篩選按鈕在畫面剛出現的時候就先渲染好,不會等到使用者操作才生成。


小結

今天在原先的基礎上新增了兩項功能,明天做好後端資料庫後就能收尾了~大家明天見!


上一篇
[Day-27]todo list實作(1)
下一篇
[Day-29]todo list實作(3)
系列文
從零開始學習TypeScript:菜鳥的入門介紹與概覽 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言