在 week7 的 hw3 就有做到 Todo list ,不過只有 css 和 js ,那重新整理頁面資料就會都沒有了,所以這次再做一次的最大差別是把 todos 存到資料庫,將資料用 API 讓前端串。
前端功能
- 新增
- 刪除
- 打勾
- filter
- 移除已完成代辦事項
- 更新未完成
版面部分
使用 Bootstrap
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Week12 Todo List</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="index.js"></script>
<style>
.btn-delete {
opacity: 0;
}
.todo:hover .btn-delete {
opacity: 1;
}
input[type=checkbox]:checked~label {
text-decoration: line-through;
color: rgba(0, 0, 0, 0.3);
}
.todo__content-wrapper {
flex: 1;
}
.todo__content {
display: block;
}
.options div,
.clear-all {
cursor: pointer;
border-radius: 6px;
padding: 4px;
border: 2px solid transparent;
}
.options div.active {
border-color: rgba(255, 0, 0, 0.3);
}
.options div:hover {
border-color: rgba(255, 0, 0, 0.5);
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6">
<h1 class="text-center">Todo List</h1>
<div class="input-group mb-3">
<input type="text" class="input-todo form-control" placeholder="todo">
<div class="input-group-append">
<button class="btn btn-add btn-outline-secondary" type="button">新增</button>
</div>
</div>
<div class="todos list-group ">
<div
class="todo list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div class="todo__content-wrapper custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="todo-1">
<label class="todo__content custom-control-label" for="todo-1">todo1</label>
</div>
<button type="button" class="btn-delete btn btn-danger">刪除</button>
</div>
</div>
<div class="info mt-1 d-flex justify-content-between align-items-center">
<div>2 個未完成</div>
<div class="options d-flex">
<div class="active">全部</div>
<div class="ml-2">未完成</div>
<div class="ml-2">已完成</div>
</div>
<div class="clear-all">
移除已完成待辦事項
</div>
</div>
<button type="button" class="btn btn-save btn-primary">儲存</button>
</div>
</div>
</div>
</body>
</html>
新增 todo 功能
JavaScript 部分
抓到 input-todo
的 value,如果沒有值(使用者沒輸入任何東西),那就 return
$('.btn-add').click(() => {
const value = $('.input-todo').val();
if (!value) return;
})
把抓到 value 給他一個版面,把上面版面 HTML 的這段程式碼剪下放在 js 裡
const template = `
<div class="todo list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div class="todo__content-wrapper custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="todo-{id}">
<label class="todo__content custom-control-label" for="todo-{id}">{content}</label>
</div>
<button type="button" class="btn-delete btn btn-danger">刪除</button>
</div>
`
按下新增按鈕後
$('.btn-add').click(() => {
const value = $('.input-todo').val();
if (!value) return;
$('.todos').append(
template
.replace('{content}', escape(value))
.replace(/{id}/g, id) //replace('{id}', id) =>這樣只會 replace 第一個,可以用 replaceAll() 或正規表達式
)
id++
$('.input-todo').val('');
})
function escape(toOutput) {
return toOutput.replace(/\&/g, '&')
.replace(/\</g, '<')
.replace(/\>/g, '>')
.replace(/\"/g, '"')
.replace(/\'/g, ''')
.replace(/\//g, '/');
}
新增 todo 功能時不只按新增按鈕可以新增,按 enter
也可以
因為要做的事情和按新增按鈕一樣,所以直接包成一個 function
function addTodo() {
const value = $('.input-todo').val();
if (!value) return;
$('.todos').append(
template
.replace('{content}', escape(value))
.replace(/{id}/g, id)
)
id++
$('.input-todo').val('');
}
$('.input-todo').keydown(e => {
if (e.key === 'Enter') {
addTodo();
}
})
刪除 todo 功能
因為 todos 是動態產生的,所以要用事件代理人
所以用外層的 $('.todos')當代理人,去監聽,把 e.target 的 parent 移除掉
$('.todos').on('click', '.btn-delete', (e) => {
$(e.target).parent().remove(); //後面用 jQuery ,前面就不能用原生的 e.target.parent().remove();
})
確認 todo 是否有被勾選
這樣就可以計算幾個 todo 完成,幾個 todo 未完成
$('.todos').on('change', '.check-todo', (e) => {
const target = $(e.target)
const isChecked = target.is(":checked")
console.log(isChecked);
})
filter
先把 count (要計算完成、未完成) 做成一個 function ,因為勾選時會變,新增和刪除也會變,所以乾脆做成一個 function
先宣告
let todoCount = 0
let uncompleteTodoCount = 0
在各個地方 todoCount 和 uncompleteTodoCount 做 ++ 或 --,要特別注意的是在按刪除按鈕時,要先判斷前面的 checkbox 是否已被勾選,如果已經勾選代表已完成,就算刪除,uncompleteTodoCount 的總數也不用動,但如果沒完成又按刪除那 uncompleteTodoCount 就要 --
// 刪除 todo
$('.todos').on('click', '.btn-delete', (e) => {
$(e.target).parent().remove(); //後面用 jQuery ,前面就不能用原生的 e.target.parent().remove();
todoCount--
const isChecked = $(e.target).parent().find('.check-todo').is(":checked");
if (!isChecked) {
uncompleteTodoCount--
}
updateCounter();
})
function updateCounter() {
$('.uncomplete-count').text(uncompleteTodoCount)
}
移除已完成代辦事項
找出所有已完成的東西,有幾種方法,
第一個是找到 checkbox (也就是 <input type="checkbox" class="check-todo custom-control-input" id="todo-{id}">
)看他是不是已完成,如果是的話就找他的 parent 刪掉。
第二個是 checkbox 打勾的時候,幫他的 class name 加上東西,這樣當按下 移除已完成代辦事項
時,就可以用這個 class name 一起刪掉
用第二種比較方便,所以在確認 todo 是否有被勾選那邊的程式,有勾加上 target.parents('.todo').addClass('checked');
,沒勾移除 class name target.parents('.todo').removeClass('checked');
$('.clear-all').click(() => {
$('.todo.checked').each(function (i, el) {
todoCount--;
el.remove();
})
})
加上 data-* attribute
資料屬性,這樣就可以利用 JavaScript 中的 dataset 物件,取得物件的屬性值, jQuery 是用內建的 .data( )
HTML 裡面加 data-* attribute
<div class="active" data-filter="all">全部</div>
<div class="ml-2" data-filter="uncomplete">未完成</div>
<div class="ml-2" data-filter="done">已完成</div>
設事件代理 $('.options')
,如果是顯示全部,那就先把所有 $('.todo')
show 出來,如果是顯示未完成,一樣先把所有 $('.todo')
show 出來,在隱藏掉已經打勾的 todo,那如果只顯示完成,則剛好相反,先把所有 $('.todo')
hide 起來,再把已經打勾的顯示出來
$('.options').on('click', 'div', e => {
const target = $(e.target)
const filter = target.attr('data-filter')
$('.options div.active').removeClass('active')
target.addClass('active')
if (filter === 'all') {
$('.todo').show()
} else if (filter === 'uncomplete') {
$('.todo').show()
$('.todo.checked').hide()
} else { // done
$('.todo').hide()
$('.todo.checked').show()
}
})