番外篇(2)一起来做 To Do List!- 实作篇(1)

上一篇先介绍运用的知识点,这篇会着重在实作时的心路历程...不是啦,是怎麽把这个网页写出来的。先上成品与程序码,若程序有写得太过繁琐的部分,也希望大家多包涵并不吝指教:
github // github page

要写文章时回头看整个程序码,觉得好像也没用到什麽技术,但刚开始一片空白要写出东西还真的毫无头绪。有 bug 卡住的时候,明明觉得答案近在咫尺但就是想不到也很崩溃(不愧是 JS 小菜鸡)。因此才想要补充这篇文章,如果你也正因为卡住在找答案的话,希望能帮到忙(不是倒忙)。

厘清需求

在开始前要先厘清需求,我想要做出这些功能:

  • 可以插入日期、时间、代办事项种类、代办事项内容
  • 没有输入日期、时间、内容的话,要弹视窗警告,且无法新增一条代办事项
  • 代办事项的种类用 icon 表示
  • 每个代办事项都可以按完成或删除,也可以一次删除全部的代办事项
  • 完成的代办事项会被划线,跑到最下方
  • 能计算完成多少代办事项,也能删除所有已完成的代办事项的内容及数量
  • 代办事项预设以时间排列
  • 代办事项的顺序可以被使用者拖拉
  • 超过时间还没完成的代办事项以红色显示
  • 有进度、月份、种类的筛选器
  • 有分析按钮,按下去会跳出视窗,显示种类的圆饼图(单纯是自己想练习 c3.js d3.js)
  • RWD 所以要有手机汉堡选单
  • 即使重新整理或关掉页面,打过的内容也不会消失

看起来很多,所以先求有再求好,至少先让内容能在输入後被加到下方吧!

第一步

当使用者填好资料後,按下加号,要让 JS 操控 HTML 新增一条 todo 到画面上,架构应该如下一样的呈现:

    <ul class="todo-list">
        <li class="todo"> <!--变数名称 todoLi-->
            <ul class="todo-item"> <!--变数名称 newTodo-->
                <li class="todo-date">Date</li> dateInput.value 输入什麽就显示什麽 <!--变数名称 newTodoDate-->
                <li class="todo-time">Time</li> timeInput.value 输入什麽就显示什麽 <!--变数名称 newTodoTime-->
                <li class="todo-sort">Sort</li> sortSelect.value 输入什麽就显示什麽 <!--变数名称 newTodoSort-->
                <li class="todo-detail">item detail</li> todoInput.value 输入什麽就显示什麽 <!--变数名称 newTodoDetail-->
            </ul> 
            <div class="todo-btn"> <!--变数名称 newTodoButton-->
                <button class="complete-btn"></button> <!--变数名称 completedButton-->
                <button class="complete-btn"></button> <!--变数名称 trashButton-->
            </div>        
        </li>
    </ul>

之所以要先想好架构,是因为要新增的东西很多,如果边做边想很容易搞乱。由此会想到昨天介绍的 createElement() 、 appendChild()。因此便可以照着刚刚想好的架构开始组装并加上 class 。

除了上述之外,还有几个小点要注意:

  • 为避免 form 照着原本属性规定的,直接 submit ,要加 event.preventDefault();
  • 为了达到”没有输入日期、时间、内容的话,要弹视窗警告,且无法新增一条代办事项“的需求,因此要加上 if 判断式
if (todoInput.value == 0 || todoInput.value == undefined || todoInput.value == null){
        alert("内容栏为必填");
    }else if(dateInput.value ==0 || dateInput.value == undefined || dateInput.value == null){
        alert("日期栏为必填");
    }else if(timeInput.value == 0 || timeInput.value == undefined || timeInput.value == null){
        alert("时间栏为必填");
    }else{
  • 因为要使代办事项的种类用 icon 表示,可在 HTML 中的 option 设 value , JS 加入 if 判断式搭配 inner.HTML 转换 value 成相对应的图示。图示部分我都是取用 font awesome。
//选什麽种类就秀对应图案
        if(taskSort.value == "job"){
            newTodoSort.innerHTML += `<i class="fas fa-briefcase"></i>`;
        }else if(taskSort.value == "housework"){
            newTodoSort.innerHTML += `<i class="fas fa-home"></i>`;
        }else if(taskSort.value == "sport"){
            newTodoSort.innerHTML += `<i class="far fa-futbol"></i>`;
        }else if(taskSort.value == "routine"){
            newTodoSort.innerHTML += `<i class="fas fa-hourglass"></i>`;
        }else if(taskSort.value == "others"){
            newTodoSort.innerHTML += `<i class="fas fa-palette"></i>`;
        }else{
            newTodoSort.innerHTML += `<i class="fas fa-times"></i>`;
        };
  • 为晚点要存到本地端铺路,这部分我们第三步再细谈。
//add todo to localstorage
        let saveLocal = [dateInput.value,timeInput.value,taskSort.value,todoInput.value];
        saveLocalTodos(saveLocal);
  • 用 location.reload(); 让加完之後画面可以重整一下
  • 最後要让输入栏被清空, 因此要让 value = ""

之後你就会得到:

//selectors
const dateInput = document.querySelector('#date-input');
const timeInput = document.querySelector('#time-input');
const todoInput = document.querySelector('.todo-input');
const addButton = document.querySelector('.addlist-button');
const todoList = document.querySelector('#todoList');
const taskSort = document.querySelector('#task-sort');

//event listeners
addButton.addEventListener('click',addTodo);
todoList.addEventListener('click',deleteCheck);

//functions
function addTodo(event){
    event.preventDefault(); 
    const todoLi = document.createElement('li');
    todoLi.classList.add("todo");
    const newTodo = document.createElement('ul');
    newTodo.classList.add("todo-item");
    todoLi.appendChild(newTodo);

    const newTodoDate = document.createElement('li');
    const newTodoTime = document.createElement('li');
    const newTodoSort = document.createElement('li');
    const newTodoDetail = document.createElement('li');
    newTodoDate.classList.add("todo-date");
    newTodoTime.classList.add("todo-time");
    newTodoSort.classList.add("todo-sort");
    newTodoDetail.classList.add("todo-detail");
    
    if (todoInput.value == 0 || todoInput.value == undefined || todoInput.value == null){
        alert("内容栏为必填");
    }else if(dateInput.value ==0 || dateInput.value == undefined || dateInput.value == null){
        alert("日期栏为必填");
    }else if(timeInput.value == 0 || timeInput.value == undefined || timeInput.value == null){
        alert("时间栏为必填");
    }else{
        newTodoDate.innerText = dateInput.value; 
        newTodoTime.innerText = timeInput.value; 
        newTodoDetail.innerText = todoInput.value;
        if(taskSort.value == "job"){
            newTodoSort.innerHTML += `<i class="fas fa-briefcase"></i>`;
        }else if(taskSort.value == "housework"){
            newTodoSort.innerHTML += `<i class="fas fa-home"></i>`;
        }else if(taskSort.value == "sport"){
            newTodoSort.innerHTML += `<i class="far fa-futbol"></i>`;
        }else if(taskSort.value == "routine"){
            newTodoSort.innerHTML += `<i class="fas fa-hourglass"></i>`;
        }else if(taskSort.value == "others"){
            newTodoSort.innerHTML += `<i class="fas fa-palette"></i>`;
        }else{
            newTodoSort.innerHTML += `<i class="fas fa-times"></i>`;
        };

        newTodo.appendChild(newTodoDate); 
        newTodo.appendChild(newTodoTime); 
        newTodo.appendChild(newTodoSort); 
        newTodo.appendChild(newTodoDetail); 

        let saveLocal = [dateInput.value,timeInput.value,taskSort.value,todoInput.value];
        saveLocalTodos(saveLocal);

        const newTodoButton = document.createElement('div');
        newTodoButton.classList.add("todo-btn");
        todoLi.appendChild(newTodoButton); 
        const completedButton = document.createElement('button');
        completedButton.innerHTML = '<i class="fas fa-check"></i>';
        completedButton.classList.add('complete-btn');
        newTodoButton.appendChild(completedButton); 
        const trashButton = document.createElement('button');
        trashButton.innerHTML = '<i class="fas fa-trash"></i>';
        trashButton.classList.add('danger-btn');
        newTodoButton.appendChild(trashButton); 
        todoList.appendChild(todoLi);
        
        location.reload();
        dateInput.value = "";
        timeInput.value = "";
        todoInput.value = "";
    };
}

第二步

接着是完成和删除键的功能撰写。

在 To Do List 练习中,将会一直牵涉到 HTML DOM 中的查找,可以善用 console.log 来确定我们要取得的是不是跟我们打的一样。

按删除键时,想达成的目的是要删除一整条的 todoLi 。用console.log(e.target);可以发现, e.target 等於我们点的位置的 html 标签,意即若直接 remove 掉 e.target,移除的会是删除钮本身,因此须回到父层再删除,同理按完成键也是一样概念,因此可将它们放在同一函式中。但要怎麽分别删除和完成呢?

没错,同样是要运用 DOM 观念查找,发现可以从 item.classList[0] ,也就是 item 的第一层 class 为 danger-btn 或 complete-btn 来区分,进而执行不同的事。

删除的部分,我们要帮他加动画效果 fall ,并用 css 设定 fall 这个动画的细节。在动画跑完後,才执行函式将 todo 本身移除。并且要让本地端储存的资料一并删除。但是本地端的部分,让我们稍後再细谈。

完成键的部分,要在 todo 这个 div 加上 completed 的 class,藉此设定画线样式。让本地端储存的资料一并删除,但纪录完成了哪些事。

function deleteCheck(e){
    
    const item = e.target;
    const todo = item.parentElement.parentElement; 
    
    //delete btn
    if(item.classList[0] === 'danger-btn'){
        todo.classList.add("fall");
        removeLocalTodos(todo); 
        todo.addEventListener('transitionend',function(){ 
            todo.remove();
        });
    }
    
    //check btn
    if(item.classList[0] === 'complete-btn'){
        todo.classList.add('completed');
        let date = todo.querySelector(".todo-date").innerText;
        let time = todo.querySelector(".todo-time").innerText;
        let detail = todo.querySelector(".todo-detail").innerText;
        let sort = todo.querySelector(".todo-sort").innerHTML;
        if (sort == `<i class="fas fa-briefcase"></i>`){
            sort = "job";
        }else if(sort == `<i class="fas fa-home"></i>`){
            sort = "housework";
        }else if(sort == `<i class="far fa-futbol"></i>`){
            sort = "sport";
        }else if(sort == `<i class="fas fa-hourglass"></i>`){
            sort = "routine";
        }else{
            sort = "others";
        };
        let saveLocalComplete = [date,time,sort,detail];
        saveLocalCompleteTodos(saveLocalComplete);
        removeLocalTodos(todo); 
    }
}

第三步

先停一下,来处理储存与删除本地端资料的问题。要储存的值会有三项:还没完成的项目、已完成的项目、完成的数目。可以分别将三个 key 命名为 todo 、 complete 和 completeTask ,并分别储存。网页重整时,再从本地端提出来。

  • 储存
    首先要确认本地端有没有已存在的 todo 和 complete ,如果没有,则建空阵列。如果有,用 json 把已存在的资料拿来,并建立内容相同的阵列。但不论刚刚有没有拿到东西,都要把参数 push 进 todos 阵列,并将 todos 资料转回字串,更新到资料库中。这个参数会是刚刚在第一和第二步骤设定的格式:[日期,时间,种类,内容],之所以会这样设定是因为等等提取时也是要这样依序放回 HTML 中。

为了让 completeTask 的数量等同目前存在於 complete 阵列中的数, completeTodos.length 派上用场。最後,按完完成键时,设定它在一定时间後自动重整页面。

function saveLocalTodos(todo) {
    let todos;
    if (localStorage.getItem("todos") === null) {
      todos = [];
    } else {
      todos = JSON.parse(localStorage.getItem("todos"));
    }
    todos.push(todo);
    localStorage.setItem("todos", JSON.stringify(todos));
}
function saveLocalCompleteTodos(todo){
    let completeTodos;
    if (localStorage.getItem("complete") === null) {
      completeTodos = [];
    } else {
      completeTodos = JSON.parse(localStorage.getItem("complete"));
    }
    completeTodos.push(todo);
    localStorage.setItem("complete", JSON.stringify(completeTodos));
    if (completeTodos.length != 0){
        completedNum.innerHTML = `已完成 ${completeTodos.length} 项工作!`;
    }else{
        completedNum.innerHTML = `尚未有完成的工作!`;
    }
    localStorage.setItem("completeTask",JSON.stringify(completeTodos.length));
    window.setTimeout(function () { 
        window.location.reload();
    }, 500);
}
  • 提取
    希望在每次画面重整时,自动提取。因此加上监听与宣告:
const completedNum = document.querySelector('#completedNum');
const clearCompleteNum = document.querySelector('#clearCompleteNum');
document.addEventListener('DOMContentLoaded',getTodos); 

分别确认本地端有没有 todos 和 completes ,没有的就要建立空阵列。排列组合下会写出四种出来。这时可用 console.log(todos); 检查,发现 todo 已被分成一条 task 一个阵列的状态,这时再复制上面 function addTodo ,只是记得将 input 改成 todo[n] / complete[n]。

也在这同步处理完成数量的程序:

if(completes = []){
        completedNum.innerHTML = `尚未有完成的工作!`;
    }else{
        completedTotalNum = JSON.parse(localStorage.getItem('completeTask'));
        completedNum.innerHTML = `已完成 ${completedTotalNum} 项工作!`;
    }

时间排序及过期的设定先不管的话,现在的你应该会得到以下程序:

function getTodos(){
    let todos;
    let completes;
    if(localStorage.getItem('todos') === null && localStorage.getItem('complete') === null){
        todos = [];
        completes = [];
    }else if(localStorage.getItem('todos') === null && localStorage.getItem('complete') !== null){
        todos = [];
        completes = JSON.parse(localStorage.getItem("complete"));
    }else if(localStorage.getItem('complete') === null && localStorage.getItem('todos') !== null){
        todos = JSON.parse(localStorage.getItem('todos'));
        completes = [];
    }else if(localStorage.getItem('complete') !== null && localStorage.getItem('todos') !== null){
        todos = JSON.parse(localStorage.getItem('todos'));
        completes = JSON.parse(localStorage.getItem("complete"));
    }

    todos.forEach(function(todo) {
        const todoLi = document.createElement('li');
        todoLi.classList.add("todo");
        const newTodo = document.createElement('ul');
        newTodo.classList.add("todo-item");
        todoLi.appendChild(newTodo); 
    
        const newTodoDate = document.createElement('li');
        const newTodoTime = document.createElement('li');
        const newTodoSort = document.createElement('li');
        const newTodoDetail = document.createElement('li');
        newTodoDate.classList.add("todo-date");
        newTodoTime.classList.add("todo-time");
        newTodoSort.classList.add("todo-sort");
        newTodoDetail.classList.add("todo-detail");
        newTodoDate.innerText = todo[0]; 
        newTodoTime.innerText = todo[1]; 
        newTodoDetail.innerText = todo[3]; 

        if(todo[2] == "job"){
            newTodoSort.innerHTML += `<i class="fas fa-briefcase"></i>`;
        }else if(todo[2] == "housework"){
            newTodoSort.innerHTML += `<i class="fas fa-home"></i>`;
        }else if(todo[2] == "sport"){
            newTodoSort.innerHTML += `<i class="far fa-futbol"></i>`;
        }else if(todo[2] == "routine"){
            newTodoSort.innerHTML += `<i class="fas fa-hourglass"></i>`;
        }else if(todo[2] == "others"){
            newTodoSort.innerHTML += `<i class="fas fa-palette"></i>`;
        }else{
            newTodoSort.innerHTML += `<i class="fas fa-times"></i>`;
        };

        newTodo.appendChild(newTodoDate); 
        newTodo.appendChild(newTodoTime); 
        newTodo.appendChild(newTodoSort); 
        newTodo.appendChild(newTodoDetail); 
        const newTodoButton = document.createElement('div');
        newTodoButton.classList.add("todo-btn");
        todoLi.appendChild(newTodoButton); 

        const completedButton = document.createElement('button');
        completedButton.innerHTML = '<i class="fas fa-check"></i>';
        completedButton.classList.add('complete-btn');
        newTodoButton.appendChild(completedButton); 
        const trashButton = document.createElement('button');
        trashButton.innerHTML = '<i class="fas fa-trash"></i>';
        trashButton.classList.add('danger-btn');
        newTodoButton.appendChild(trashButton); 
        todoList.appendChild(todoLi);
    });
    completes.forEach(function(complete){
        const todoLi = document.createElement('li');
        todoLi.classList.add("todo");
        todoLi.classList.add("completed");

        const completeTodo = document.createElement('ul');
        completeTodo.classList.add("todo-item");
        todoLi.appendChild(completeTodo); 

        const doneTodoDate = document.createElement('li');
        const doneTodoTime = document.createElement('li');
        const doneTodoSort = document.createElement('li');
        const doneTodoDetail = document.createElement('li');
        doneTodoDate.classList.add("todo-date");
        doneTodoTime.classList.add("todo-time");
        doneTodoSort.classList.add("todo-sort");
        doneTodoDetail.classList.add("todo-detail");
        completeTodo.appendChild(doneTodoDate); 
        completeTodo.appendChild(doneTodoTime); 
        completeTodo.appendChild(doneTodoSort); 
        completeTodo.appendChild(doneTodoDetail);

        doneTodoDate.innerText = complete[0]; 
        doneTodoTime.innerText = complete[1]; 
        doneTodoDetail.innerText = complete[3];
        if(complete[2] == "job"){
            doneTodoSort.innerHTML += `<i class="fas fa-briefcase"></i>`;
        }else if(complete[2] == "housework"){
            doneTodoSort.innerHTML += `<i class="fas fa-home"></i>`;
        }else if(complete[2] == "sport"){
            doneTodoSort.innerHTML += `<i class="far fa-futbol"></i>`;
        }else if(complete[2] == "routine"){
            doneTodoSort.innerHTML += `<i class="fas fa-hourglass"></i>`;
        }else if(complete[2] == "others"){
            doneTodoSort.innerHTML += `<i class="fas fa-palette"></i>`;
        }else{
            doneTodoSort.innerHTML += `<i class="fas fa-times"></i>`;
        };
        todoList.appendChild(todoLi);
    })
    completedTotalNum = JSON.parse(localStorage.getItem('completeTask'));
    if(completedTotalNum == null){
        completedNum.innerHTML = `尚未有完成的工作!`;
    }else{
        completedNum.innerHTML = `已完成 ${completedTotalNum} 项工作!`;
    }
};

顺带一提,因为上面先写 todos.forEach 再写 completes.forEach ,还没完成的会放在上面,已经完成的会被摆到下面。若不太懂我的意思,你可以把两个顺序倒过来试试,就会知道我在说什麽。

  • 删除
    这个函式会在按完删除键或完成键後执行。一样要先确认本地端有没有已存在的 todo ,没有就建立空阵列,有就用 json 把已存在的 todos 拿来,并建立内容相同的阵列 todos。

因为 todos 是阵列包着阵列, 而 todo 是点下去的那段的程序码,所以把 todo 转成跟 todos 一样的阵列方式(有点类似刚刚储存时转过来,现在再转回去)。

接着要用 indexOf 找到他在阵列上是第几个位置,然後用 slice 把它切掉。刚刚提过了, todos 是阵列中又包着阵列。当要在阵列中包着阵列的形式中寻找特定阵列,使用 indexOf 会找不到,因为 indexOf 是用严格模式判断,例如即使 todos=[[3,0],[1,2]] ,找 todos.indexOf([3,0]) 也找不到。为此需要客制化 indexOf :

既然阵列是从 0 开始数,预设代表阵列位置的变数 i=0 。当 i 小於查找项目的长度,跑下面的回圈,跑完加一再继续跑,直到等於长度时停止。从查找阵列的第 0 项开始,当第 0 项阵列中的第 0 个位置的值,跟要找的阵列的第 0 个值相同,就让 i 显示 0 返回,藉此得知要找的就在第 0 的位置,依此类推,若都找不到则返回 -1。

终於写好。依上面写好的程序代入 todo (被找的父阵列) 和 todoIndex (要找的内容)。1 的位置要填的数字,代表要删几个,只删一个所以填一。最後,把结果传回本地端。

function removeLocalTodos(todo){
    let todos;
    if(localStorage.getItem('todos') === null){
        todos = [];
    }else{
        todos = JSON.parse(localStorage.getItem('todos'));
    }
    let date = todo.querySelector(".todo-date").innerText;
    let time = todo.querySelector(".todo-time").innerText;
    let detail = todo.querySelector(".todo-detail").innerText;
    let sort = todo.querySelector(".todo-sort").innerHTML;
    if (sort == `<i class="fas fa-briefcase"></i>`){
        sort = "job";
    }else if(sort == `<i class="fas fa-home"></i>`){
        sort = "housework";
    }else if(sort == `<i class="far fa-futbol"></i>`){
        sort = "sport";
    }else if(sort == `<i class="fas fa-hourglass"></i>`){
        sort = "routine";
    }else{
        sort = "others";
    };
    let todoIndex = [date,time,sort,detail];
    
    function indexOfCustom (parentArray, searchElement) {
        for (let i = 0; i < parentArray.length; i++ ) { 
            if ( parentArray[i][0] == searchElement[0] && parentArray[i][1] == searchElement[1] && parentArray[i][2] == searchElement[2] && parentArray[i][3] == searchElement[3]) { 
                return i;
            }
        }
        return -1;
    }
    todos.splice(indexOfCustom(todos,todoIndex),1); 
    localStorage.setItem("todos",JSON.stringify(todos)); 
}

虽然文章和程序很长,但其实可以发现都是一些简单的观念重复运用,只有少数几个小卡关的点而已。而我们没达成的需求还剩:

  • 可以一次删除全部的代办事项
  • 能删除所有已完成的代办事项的内容及数量
  • 代办事项预设以时间排列
  • 代办事项的顺序可以被使用者拖拉
  • 超过时间还没完成的代办事项以红色显示
  • 有进度、月份、种类的筛选器
  • 有分析按钮,按下去会跳出视窗,显示种类的圆饼图
  • RWD 所以要有手机汉堡选单

<<:  ABAP OO-ALV 客制报表呈现

>>:  网拍的创业回亿:管理与经营(一)

Day12.进入 ARM 世界: ARM Cortex-M Exception Behavior

Nested Interrupts Cortex-M3 和 NVIC 在硬体架构上支援(Nested...

第29天-CSS-影像-(3-3)

背景位置 background-position 可以使用这个属性将背景图片指定到想要的位置 有以下...

D30. 学习基础C、C++语言

D30. 心得 虽然之前已经有学过一些C语言,但经过这30天的自学还是学到很多东西,像是C语言之前我...

Day20# Leetcode - Roman to Integer

第 20 天的 Leetcode 要开始拉,那我们就开始吧 ─=≡Σ(((っ›´ω`‹ )っ! 今天...

2021-Day26. Serverless(十 四):AWS - SSL / TLS 凭证

影片一录好,就把AWS帐号给关掉了,就不用像昨天一样後制到怀疑人生... ...