艾草:「もうだめだ。我已經沒有梗了,不行了...」
「艾草,醒醒!我們不是說好要一起征服這個世界嗎?」
艾草:「我..的魔法作業都做不完啦,嗚哇啊啊啊,救我~~」
「...上次的古書好像有提到讓人充滿動力的魔法 - todo 實作魔法!魔法咒語 ㄉㄡ ㄌ ㄟ ㄇ一ㄙ ㄡ ~~」
艾草:「等一下,先不要 todo 只是拿來壓時程的東西啊啊, No ~ 身體自己動起來了!」

(艾草壞掉中)
此次使用六角學院提供於「Vtuber x Coding 蹦出新滋味 ⚙️」影片下方的 todoList 版型,並參考影片內容實作 todolist !
功能切分如下:
input 細節優化
首先附上 HTML 程式碼:
<div class="container">
  <h1>TODO LIST</h1>
  <div class="card input">
    <input type="text" placeholder="請輸入待辦事項" id="inputVal" />
    <a href="#" class="btn_add" id="addTodoBtn">+</a>
  </div>
  <div class="card card_list">
    <ul class="tab" id="tab">
      <li class="active" data-tab="all">全部</li>
      <li data-tab="work">待完成</li>
      <li data-tab="done">已完成</li>
    </ul>
    <div class="cart_content">
      <ul class="list" id="todoList">
      </ul>
      <div class="list_footer">
         <p><span id="workNum"></span> 個待完成項目</p>
        <a href="#" id="deleteBTN">清除已完成項目</a>
      </div>
    </div>
  </div>
</div>
最先開始實作的功能為新增待辦事項,實作過程基本如下:
input 欄位輸入的值new Date().getTime() )input 欄位是否有值,有值的情況下,將內容推到全域變數的陣列內forEach 組字串,字串須留意要埋藏checked 屬性至 input 欄位(判斷完成狀態)input 文字內容innerHTML 渲染至網頁上,並執行該函式//透過 querySelector 選取 input 欄位
const inputVal = document.querySelector("#inputVal");
//透過 querySelector 選取 button 欄位(新增按鈕)
const addTodoBtn = document.querySelector("#addTodoBtn");
//宣告全域變數 todoData 來接組出的物件資料
let todoData = [];
//監聽是否點擊新增按鈕
addTodoBtn.addEventListener("click", addTodo);
//一點擊就執行 addTodo()
function addTodo() {
  // 組出未來要用到的物件
  let todo = {
    // input 的值
    txt: inputVal.value,
    // id 用 getTime() 取毫秒
    id: new Date().getTime(),
		//紀錄待辦事項完成狀態
    complete: false
  };
  //防呆 確保有填入文字
  if (todo.txt.trim() !== "") {
    //要塞在第一筆資料,所以用 unshift 把組好的 todo 物件賦予到外層的 todoData 
    todoData.unshift(todo);
    // 把 input 欄位清空
    inputVal.value = ""; //清空
  }
  //跑 render 函式,把外層的 todoData 放進去
	render(todoData);
}
//透過 querySelector 選取要放入資料的 ul
const todoList = document.querySelector("#todoList");
//渲染的函式
function render(todo) {
  let str = "";
  //透過 todoData 跑迴圈
  todo.forEach((item) => {
    //將 todo 的 id 透過 data-id 埋進去
    //將是否打勾埋在 input 標籤內
    //將字放進去
    str += `<li data-id="${item.id}">
          <label class="checkbox" for="">
            <input type="checkbox" ${item.complete ? "checked" : ""}/>
            <span>${item.txt}</span>
          </label>
          <a href="#" class="delete"></a>
        </li>`;
  });
  //最後 innerHTML 把組好的字串賦予給 todoList
  todoList.innerHTML = str;
}
補充:
Date 物件:基於世界標準時間(UTC) 1970 年 1 月 1 日開始的毫秒數值來儲存時間,可以透過 new Date().getTime() 的方式來當成 id 使用。data-"自定義名稱" :可以拿來埋各種資料進 HTML 結構內。checked 屬性:input checkbox 的屬性 checked ,可以使 checkbox 維持打勾執行監聽 ul 區域內的刪除功能與打勾時完成狀態能切換。

實作流程:
ul 區塊的點擊事件li 的 id 值input 欄位,要透過 closest 才能點擊到 lili 取出的 id 值字串型別轉型為數字型別findIndex 比對符合的 id 後使用陣列方法刪除該筆資料forEach 比對點擊 id 是否符合 Data 內的 id 值true/false
//監聽註冊 ul todoList 的點擊事件
todoList.addEventListener("click", (e) => {
  //透過 closest 的方式能找出點擊到的 li 標籤
  //透過 dataset.id 取出埋在該 li 內的 id
  //取出來的 id 會是字串型別記得幫它轉型成數字型別
  let id = parseInt(e.target.closest("li").dataset.id);
  //刪除功能
  //透過 nodeName 確認是否為 A 連結
  if (e.target.nodeName === "A") {
    e.preventDefault(); //取消 a 標籤預設行為
    //透過陣法方法 findIndex 比對 todoData 內的 id 是否等於點擊到的 id
    let index = todoData.findIndex((item) => item.id === id);
    //如果是的話刪除該筆資料
    todoData.splice(index, 1);
  } else {
    //切換打勾功能
    //透過 todoData 去跑 forEach
    todoData.forEach((item) => {
      //如果 todoData 內的 id 是否等於點擊到的 id
      if (item.id === id) {
        //更改資料是否狀態
        item.complete ? (item.complete = false) : (item.complete = true);
      }
    });
  }
  //重新渲染
	render(todoData);
});
補充:
closest :當在複雜 HTML 結構中想透過 e.target 選取某個 Element ,卻都只能選到它的子層時,可以透過 closest 去取到自己想要的父層 Element。接下來實作點擊此區需有狀態樣式切換,並能篩選出對應資料。

實作流程:
tab 內新增 class 名稱為 active 實現
tab 區塊status 記錄 tab 點擊狀態並取出該 HTML 結構內埋藏的值,預設為 all
querySelectorAll 選取所有 tabs 狀態forEach 先移除所有 tabs active 樣式tab 新增 active 樣式透過  status 狀態判斷點擊到的狀態為何,並將資料賦予給全域儲存待辦事項陣列的變數
a. 為 all 全部時顯示全域儲存變數
b. 為 work 待完成時篩選出未完成狀態
c. 都不是時,篩選出已完成狀態
透過未完成狀態長度篩選出左下角待完成項目,並渲染至網頁上
將更新後的資料透過渲染函式執行
重要:透過 updateList() 函式取代掉其它地方之 render(todo) 渲染函式呼叫
//切換 tab
//透過 querySelector 選取 id tab
const tab = document.querySelector("#tab");
//預設顯示狀態為全部
let status = "all";
//註冊監聽是否點擊到 tab
tab.addEventListener("click", changeTab);
//點擊到 tab 就執行 changeTab(e)
function changeTab(e) {
  //透過 e.target 將 dataset 埋入的 tab 取出
  status = e.target.dataset.tab;
  //透過 querySelectorAll 選取 tab 標籤底下的 li
  let tabs = document.querySelectorAll("#tab li"); //類陣列
  //點擊時 tab 先清掉全部 class 樣式
  tabs.forEach((item) => {
    //先移除全部的 class active 樣式
    item.setAttribute("class", "");
  });
  //有被點擊到的才加 class 樣式
  e.target.setAttribute("class", "active");
  //切換頁面重新渲染
  updateList();
}
//修改完成狀態
function updateList() {
  //切換不同頁面顯示資料
  let showData = [];
  //跟切換 tab 的 status 整合
  if (status === "all") {
    //狀態為全部 "all" 時就全部顯示
    showData = todoData;
    //狀態為待完成 "work" 時
  } else if (status === "work") {
    //篩選出未完成
    showData = todoData.filter((item) => !item.complete);
  } else {
    //篩選出已完成
    showData = todoData.filter((item) => item.complete);
  }
  //計算幾個待完成項目 (左下角)
  const workNum = document.querySelector("#workNum");
  //篩選出未完成的長度
  let todoLength = todoData.filter((item) => !item.complete);
  //並將長度賦予到該 DOM 節點上
  workNum.textContent = todoLength.length;
  //渲染 showData
  render(showData);
}
updateList(); //初始化頁面
此階段執行清除所有已完成項目,並優化新增功能,使用 enter 按鍵也能新增待辦事項。
實作流程:
input 欄位的鍵盤事件程式碼:
// 清除已完成項目
// 透過 querySelector 選取 id 為 deleteBTN 的 DOM
const deleteBTN = document.querySelector("#deleteBTN");
// 註冊監聽 deleteBTN 的點擊事件
deleteBTN.addEventListener("click", function (e) {
  //取消預設效果
  e.preventDefault();
  //重新將 todoData 賦予未完成的資料
  todoData = todoData.filter((item) => !item.complete);
  //重新渲染 updateList()
  updateList();
});
//點擊 Enter 也可以新增資料
//註冊監聽 inputVal 的鍵盤 "keyup" 事件
inputVal.addEventListener("keyup", function (e) {
  //如果點擊到 "Enter"
  if (e.key === "Enter") {
    //執行新增該筆資料
    addTodo();
  }
});
補充:
keyup:鍵盤事件 keyup 可以拿來偵測是否按下特定鍵盤,而 keyup 的觸發時機為當你按下特定鍵盤又放開的那刻。todoList 可以練習新增、切換狀態、刪除功能,後續還可以持續優化新增編輯功能,透過練習 todoList 更了解 JavaScript 魔法!