魔法实作 - 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 魔法!


<<:  我买开发板了

>>:  突破自我设限

[C 语言笔记--Day18] 用 linked list 实作 merge sort

题目来源 #include <stdio.h> #include <stdlib....

Azure证照

小弟近日工作刚好会用到Azure,公司希望我们可以以考取AZ204为目标,所以接下来会照着这个目标发...

Day 16 : 模型衡量指标

由於我们需要有指标来衡量一个模型的好坏,而问题可以粗略分成「分类」和「回归」问题。而根据不同的问题,...

Day50. 范例:十二生肖

本文同步更新於blog 情境:玉皇大帝要举办渡河比赛,动物选手各显神通。 <?php na...

Day 15 知识地图

画出一张属於自己的学习地图,只有知道自己身在何处,才不会迷路。你必须知道这项学习的终点,该拥有什麽样...