上一篇先介绍运用的知识点,这篇会着重在实作时的心路历程...不是啦,是怎麽把这个网页写出来的。先上成品与程序码,若程序有写得太过繁琐的部分,也希望大家多包涵并不吝指教:
github // github page
要写文章时回头看整个程序码,觉得好像也没用到什麽技术,但刚开始一片空白要写出东西还真的毫无头绪。有 bug 卡住的时候,明明觉得答案近在咫尺但就是想不到也很崩溃(不愧是 JS 小菜鸡)。因此才想要补充这篇文章,如果你也正因为卡住在找答案的话,希望能帮到忙(不是倒忙)。
在开始前要先厘清需求,我想要做出这些功能:
看起来很多,所以先求有再求好,至少先让内容能在输入後被加到下方吧!
当使用者填好资料後,按下加号,要让 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 。
除了上述之外,还有几个小点要注意:
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{
//选什麽种类就秀对应图案
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);
之後你就会得到:
//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 ,并分别储存。网页重整时,再从本地端提出来。
为了让 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 ,还没完成的会放在上面,已经完成的会被摆到下面。若不太懂我的意思,你可以把两个顺序倒过来试试,就会知道我在说什麽。
因为 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));
}
虽然文章和程序很长,但其实可以发现都是一些简单的观念重复运用,只有少数几个小卡关的点而已。而我们没达成的需求还剩:
Nested Interrupts Cortex-M3 和 NVIC 在硬体架构上支援(Nested...
背景位置 background-position 可以使用这个属性将背景图片指定到想要的位置 有以下...
D30. 心得 虽然之前已经有学过一些C语言,但经过这30天的自学还是学到很多东西,像是C语言之前我...
第 20 天的 Leetcode 要开始拉,那我们就开始吧 ─=≡Σ(((っ›´ω`‹ )っ! 今天...
影片一录好,就把AWS帐号给关掉了,就不用像昨天一样後制到怀疑人生... ...