iT邦幫忙

2021 iThome 鐵人賽

DAY 29
3
Modern Web

JavaScript 魔法入門 - 從入門到中階觀念系列 第 29

魔法實作 - todolist

前情提要

艾草:「もうだめだ。我已經沒有梗了,不行了...」

「艾草,醒醒!我們不是說好要一起征服這個世界嗎?」

艾草:「我..的魔法作業都做不完啦,嗚哇啊啊啊,救我~~」

「...上次的古書好像有提到讓人充滿動力的魔法 - todo 實作魔法!魔法咒語 ㄉㄡ ㄌ ㄟ ㄇ一ㄙ ㄡ ~~」

艾草:「等一下,先不要 todo 只是拿來壓時程的東西啊啊, No ~ 身體自己動起來了!」

(艾草壞掉中)


todolist

此次使用六角學院提供於「Vtuber x Coding 蹦出新滋味 ⚙️」影片下方的 todoList 版型,並參考影片內容實作 todolist !

功能切分如下:

  • 新增待辦事項
  • 刪除單筆/切換打勾狀態
  • 待辦事項狀態切換
  • 刪除全部/ input 細節優化

https://ithelp.ithome.com.tw/upload/images/20211013/201390661PSHE6p1E5.png

首先附上 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>

新增待辦事項

最先開始實作的功能為新增待辦事項,實作過程基本如下:

  1. 監聽點擊新增按鈕事件
    a. 監聽按鈕時要組出一包物件,該物件內包含:
    i. input 欄位輸入的值
    ii. 需埋藏的 id 值(埋藏 id 透過 new Date().getTime()
    iii. 待辦事項完成狀態之紀錄
  2. 判斷步驟 1 的物件 input 欄位是否有值,有值的情況下,將內容推到全域變數的陣列內
  3. 新增渲染資料函式,將步驟 1 組出的物件當成參數傳給該函式
  4. 於函式內透過 forEach 組字串,字串須留意要埋藏
    a. id 值
    b. 新增 checked 屬性至 input 欄位(判斷完成狀態)
    c. input 文字內容
  5. 將組好的字串透過 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 區域內的刪除功能與打勾時完成狀態能切換。

https://ithelp.ithome.com.tw/upload/images/20211013/20139066k8NvYHDjbr.png

實作流程:

  1. 監聽 ul 區塊的點擊事件
  2. 取出埋藏於 li 的 id 值
    a. 此 HTML 結構都會點擊到 input 欄位,要透過 closest 才能點擊到 li
    b. 將 li 取出的 id 值字串型別轉型為數字型別
  3. 判斷點擊到的是刪除按鈕
    a. 透過 findIndex 比對符合的 id 後使用陣列方法刪除該筆資料
  4. 點擊到的不是刪除按鈕(執行打勾更改狀態)
    a. 透過 forEach 比對點擊 id 是否符合 Data 內的 id 值
    b. 符合時執行更改完成狀態為 true/false
  5. 重新渲染資料
//監聽註冊 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

待辦事項狀態切換

接下來實作點擊此區需有狀態樣式切換,並能篩選出對應資料。

https://ithelp.ithome.com.tw/upload/images/20211013/201390661tJ4x3MVyg.png

實作流程:

  • 狀態樣式切換:透過在對應 tab 內新增 class 名稱為 active 實現
    1. 註冊監聽點擊到的為 tab 區塊
    2. 宣告全域變數 status 記錄 tab 點擊狀態並取出該 HTML 結構內埋藏的值,預設為 all
    3. 透過 querySelectorAll 選取所有 tabs 狀態
    4. 使用 forEach 先移除所有 tabs active 樣式
    5. 將點擊到的 tab 新增 active 樣式
  • 包裝函式執行篩選對應功能
    1. 透過 status 狀態判斷點擊到的狀態為何,並將資料賦予給全域儲存待辦事項陣列的變數
      a. 為 all 全部時顯示全域儲存變數
      b. 為 work 待完成時篩選出未完成狀態
      c. 都不是時,篩選出已完成狀態

    2. 透過未完成狀態長度篩選出左下角待完成項目,並渲染至網頁上

    3. 將更新後的資料透過渲染函式執行

    4. 重要:透過 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 按鍵也能新增待辦事項。

實作流程:

  • 刪除全部
    • 賦予未完成待辦事項給全域儲存待辦事項陣列的變數,並重新渲染
  • 細節優化
    1. 監聽 input 欄位的鍵盤事件
    2. 一但點擊為 enter 就執行新增待辦功能函式

程式碼:

// 清除已完成項目
// 透過 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 魔法!


上一篇
中階魔法 - this 指向(二)
下一篇
魔法終曲 - 魔法學習紀錄暨結賽感言
系列文
JavaScript 魔法入門 - 從入門到中階觀念30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
juck30808
iT邦研究生 1 級 ‧ 2021-10-14 12:32:20

恭喜即將邁入完賽階段~

我要留言

立即登入留言