Vue To-do List 拆解練習
需載入 CDN
- Vue
- Bootstrap
版型範例程式碼
<div id="app" class="container mt-4">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">待辦事項</span>
</div>
<input type="text" class="form-control" placeholder="準備要做的任務">
<div class="input-group-append">
<button class="btn btn-primary" type="button">新增</button>
</div>
</div>
<div class="card text-center">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link active" href="#">全部</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">進行中</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">已完成</a>
</li>
</ul>
</div>
<ul class="list-group list-group-flush text-left">
<li class="list-group-item">
<div class="d-flex">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="a1">
<label class="form-check-label" for="a1">
待辦事項1
</label>
</div>
<button type="button" class="close ml-auto" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
</li>
<li class="list-group-item">
<input type="text" class="form-control">
</li>
</ul>
<div class="card-footer d-flex justify-content-between">
<span>還有 3 筆任務未完成</span>
<a href="#">清除所有任務</a>
</div>
</div>
</div><style>
.completed {
text-decoration: line-through
}
</style>
拆解需要的功能
- 🔴新增
- 🟢刪除
- 🟣按下checkbox 加上刪除線
- 🟡頁籤切換功能與篩選顯示資料
- 🔵修改(雙擊修改已新增內容)
- 🔷計算未完成待辦事項數量
- 🟠清除所有任務
1. 🔴新增
流程
1️⃣ 在 vue data 中定義 1 筆測試資料
2️⃣ 顯示 vue data 中 todos 內的清單測試資料
3️⃣ 在「新增」按鈕綁定事件觸發器
4️⃣ 雙向綁定,讓 input 輸入的內容可以寫進 vue data 內的 newTodo
5️⃣ 讓 enter 可觸發事件新增內容
6️⃣ 在方法集 methods 物件中新增一個 addTodo 事件
7️⃣避免空白內容可新增
1️⃣ 在 vue data 中定義 1 筆測試資料
newTodo: ''
用來放之後 input 新增的內容todos: []
裡面新增 1 筆物件資料來測試
var app = new Vue({
el: '#app',
data:{
newTodo: '',
todos:[
{
id: '123',
title: '待辦事項1',
completed: true
}
]
},
})
2️⃣ 顯示 vue data 中 todos 內的清單測試資料
v-for
搭配{{ ... }}
<li class="list-group-item" v-for="item in todos">
<div class="d-flex">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="a1">
<label class="form-check-label" for="a1">
{{ item.title}}
</label>
</div>
<button type="button" class="close ml-auto" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
</li>
3️⃣ 在「新增」按鈕綁定事件觸發器
v-on:click
或@click
<button class=”btn btn-primary” @click="addTodo" type="button">新增</button>
4️⃣ 雙向綁定,讓 input 輸入的內容可以寫進 vue data 內的 newTodo
v-model
<input type="text" class="form-control" v-model="newTodo" placeholder="準備要做的任務">
5️⃣ 讓 enter 可觸發事件新增內容
@keyup.enter
<input type="text" class="form-control" v-model="newTodo" @keyup.enter="addTodo" placeholder="準備要做的任務">
6️⃣ 在方法集 methods 物件中新增一個 addTodo 事件
methods: {
addTodo: function(){
addTodo : function(){
var value = this.newTodo;
var timeStamp = Math.floor(Date.now());
this.todos.push({
id: timeStamp,
title: value,
computed: false
});
this.newTodo = '';
}
}
},
7️⃣避免空白內容可新增
.trim()
- 判斷式
methods: {
addTodo : function(){
var value = this.newTodo.trim(); //消除前後空白
var timeStamp = Math.floor(Date.now());
if (!value) {
return; //若內容為空白就終止 function
}
this.todos.push({
id: timeStamp,
title: value,
computed: false
});
this.newTodo = '';
}
}
2. 🟢刪除
方法一 : 索引值對應刪除 流程
1️⃣ 在 X 按鈕綁定事件觸發器
2️⃣ 需將原本設定的 v-for 中也加上 key 索引值參數
3️⃣ 新增一個 removeTodo function
1️⃣在 X 按鈕綁定事件觸發器
v-on:click
或@click
- 為了辨識要刪除哪一筆資料,需要傳入 key 索引值參數
<button type="button" class="close ml-auto" @click="removeTodo(key)" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
2️⃣ 需將原本設定的 v-for 中也加上 key 索引值參數
<li class="list-group-item" v-for="(item, key) in todos">
<div class="d-flex">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="a1">
<label class="form-check-label" for="a1">
{{ item.title}}
</label>
</div>
<button type="button" class="close ml-auto" @click="removeTodo(key)" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
</li>
3️⃣ 新增一個 removeTodo function
.splice(索引值, 要刪除的筆數)
removeTodo: function(key){
this.todos.splice(key, 1);
}
此時完成了刪除功能,但索引值之後會因為頁籤切換而改變,因此需要換成使用帶入 id 的方式找到 vue data中對應的資料,並刪除。
方法二 : id 對應刪除 流程
1️⃣將 X 按鈕綁定觸發的 removeTodo function 參數從 key 改成 item(帶入整筆點擊到的 todo 物件資料)
2️⃣ 修改 removeTodo function
1️⃣將 X 按鈕綁定觸發的 removeTodo function 參數從 key 改成 item(帶入整筆點擊到的 todo 物件資料)
<button type="button" class="close ml-auto" @click="removeTodo(item)" aria-label="Close">
2️⃣ 修改 removeTodo function
- 參數由 key 改成點擊到的整筆 todo 資料
- 使用 forEach 逐筆抓出 vue data 中 todos 的資料
- 比對點擊的該筆 todo 和 todos 中的哪筆資料 id 相同
- 刪除該筆資料
removeTodo: function(todo){
var newIndex = '';
var vm = this;
vm.todos.forEach(function(item, key){
if(todo.id === item.id){
newIndex = key;
}
})
this.todos.splice(newIndex, 1);
},
(方法三 : 更精簡寫法)
removeTodo: function(todo){
var vm = this;
var newIndex = vm.todos.findIndex(function(item, key){
return todo.id === item.id;
})
this.todos.splice(newIndex, 1);
},
3. 🟣按下checkbox 加上刪除線
流程
1️⃣ 讓 checkbox 值與 vue data 資料 completed 屬性值雙向綁定
2️⃣ 讓 <input> 與 <label> 的 id 互相對應
3️⃣ 讓 <label> 在 vue data 中 completed 屬性值為 true 的情況下套用 completed 這個 class 名稱
1️⃣ 讓 checkbox 值與 vue data 資料 completed 屬性值雙向綁定
v-model
<input type="checkbox" class="form-check-input" v-model="item.completed">
2️⃣ 讓 <input> 與 <label> 的 id 互相對應
<input>
v-bind:id
或:id
<input type="checkbox" class="form-check-input" v-model="item.completed" :id="item.id">
<label>
v-bind:for
或:for
<label class="form-check-label" :for="item.id">
3️⃣ 讓 <label> 在 vue data 中 completed 屬性值為 true 的情況下套用 completed 這個 class 名稱
:class=”{'class名稱' : 條件}
<label class="form-check-label"
:class="{'completed': item.completed}"
:for="item.id">
4. 🟡頁籤切換功能與篩選顯示資料
流程
1️⃣ 在 vue data 中新增一個變數 visibility
2️⃣ 頁籤切換判斷
3️⃣ 在 vue 中新增 conputed 物件
4️⃣ 在computed中新增一個 filteredTodos function 來篩選顯示資料
5️⃣ 將待辦清單<li>裡面的 v-for 中 in todos 修改為 in filteredTodos
1️⃣ 在 vue data 中新增一個變數 visibility
data: {
newTodo: '',
todos:[
{
id: '123',
title: '待辦事項1',
completed: true
}
],
visibility: 'all'
},
2️⃣ 頁籤切換判斷
- 將頁籤清單原本第一個<a>中 active 這個 class 名稱刪掉 (有加 active,畫面上頁籤就會是啟動狀態)
- 使用
:class="{'class名稱' : 條件}"
- 使用
@click
讓點擊<a>後切換 visibility的值為 all、active 或 completed
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link" :class="{'active': visibility == 'all'}" @click="visibility='all'" href="#">全部</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{'active': visibility == 'active'}" @click="visibility='active'" href="#">進行中</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{'active': visibility == 'completed'}" @click="visibility='completed'" href="#">已完成</a>
</li>
</ul>
3️⃣ 在 vue 中新增 conputed 物件
var app = new Vue({
el: "#app",
data: {
newTodo: '',
todos:[
{
id: '123',
title: '待辦事項1',
completed: true
}
],
visibility: 'all'
},
methods: {
...
},
computed: { }
})
4️⃣ 在computed中新增一個 filteredTodos function 來篩選顯示資料
computed: {
filteredTodos: function() {
if(this.visibility == 'all'){
return this.todos;
}else if(this.visibility == 'active'){
var newTodos = [];
this.todos.forEach(function(item){
if(!item.completed){
newTodos.push(item);
}
})
return newTodos;
}else if (this.visibility == 'completed') {
var newTodos = [];
this.todos.forEach(function(item){
if (item.completed) {
newTodos.push(item);
}
})
return newTodos;
}
}
}
5️⃣ 將待辦清單<li>裡面的 v-for 中 in todos
修改為 in filteredTodos
<li class="list-group-item" v-for="(item, key) in filteredTodos">
<div class="d-flex">
<div class="form-check">
<input type="checkbox" class="form-check-input" v-model="item.completed" :id="item.id">
<label class="form-check-label" :class="{'completed' : item.completed}" :for="item.id">
{{item.title}}
</label>
</div>
<button type="button" class="close ml-auto" @click="removeTodo(key)" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
</li>
5. 🔵修改(雙擊修改已新增內容)
流程
1️⃣ 在清單列表<li> 中綁定雙擊觸發器
2️⃣ 在 vue data物件中新增暫存、預存變數
3️⃣ 在方法集 methods 物件中新增一個 editTodo function
4️⃣ 將最下方 list-group-item 先註解起來,只複製中間那行<input>
5️⃣ 把這段用來編輯的 <input> 貼到 todo 清單中<div class=”d-flex”>下方
6️⃣ 判斷雙擊後 id 相同即顯示<input>進入編輯狀態,否則顯示原清單內容
7️⃣ 雙向綁定 「編輯input」 與 vue data 物件中的 cacheTitle
8️⃣ 按一下 esc 可取消編輯
9️⃣ 在方法集 methods 物件中新增一個 cancelEdit function
🔟 按一下 enter 儲存編輯
1️⃣1️⃣ 在方法集 methods 物件中新增一個 doneEdit function
1️⃣在清單列表<li> 中綁定雙擊觸發器
@dblclick
- 將整個 item 物件作為事件參數
<li class="list-group-item" v-for="(item, key) in filteredTodos" @dblclick="editTodo(item)">
<div class="d-flex">
<div class="form-check">
<input type="checkbox" class="form-check-input" v-model="item.completed" :id="item.id">
<label class="form-check-label" :class="{'completed' : item.completed}" :for="item.id">
{{item.title}}
</label>
</div>
<button type="button" class="close ml-auto" @click="removeTodo(key)" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
</li>
2️⃣ 在 vue data物件中新增暫存、預存變數
- cacheTodo : 暫存我們要修改的內容物件
- cacheTitle : 預存修改後內容標題
data: {
newTodo: '',
todos:[
{
id: '123',
title: '待辦事項1',
completed: true
}
],
visibility: 'all'
cacheTodo: {},
cacheTitle: '',
},
3️⃣ 在方法集 methods 物件中新增一個 editTodo function
editTodo: function(item){
console.log(item); //測試雙擊是否觸發事件
this.cacheTodo = item;
this.cacheTitle = item.title;
}
4️⃣ 將最下方 list-group-item 先註解起來,只複製中間那行<input>
<!-- <li class="list-group-item">
<input type="text" class="form-control">
</li> -->
5️⃣ 把這段用來編輯的 <input> 貼到 todo 清單中<div class=”d-flex”>下方
<li class="list-group-item" v-for="(item, key) in filteredTodos" @dblclick="editTodo(item)">
<div class="d-flex">
<div class="form-check">
<input type="checkbox" class="form-check-input" v-model="item.completed" :id="item.id">
<label class="form-check-label" :class="{'completed' : item.completed}" :for="item.id">
{{item.title}}
</label>
</div>
<button type="button" class="close ml-auto" @click="removeTodo(key)" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<input type="text" class="form-control">
</li>
6️⃣ 判斷雙擊後 id 相同即顯示<input>進入編輯狀態,否則顯示原清單內容
v-if
<div class="d-flex" v-if="item.id !== cacheTodo.id">
<div class="form-check">
<input type="checkbox" class="form-check-input" v-model="item.completed" :id="item.id">
<label class="form-check-label" :class="{'completed' : item.completed}" :for="item.id">
{{item.title}}
</label>
</div>
<button type="button" class="close ml-auto" @click="removeTodo(key)" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<input type="text" class="form-control" v-if="item.id == cacheTodo.id">
7️⃣ 雙向綁定 「編輯input」 與 vue data 物件中的 cacheTitle
v-model
<input type="text" class="form-control"
v-model="cacheTitle"
v-if="item.id == cacheTodo.id">
8️⃣ 按一下 esc 可取消編輯
@keyup.esc
<input type="text" class="form-control"
v-model="cacheTitle"
@keyup.esc="cancelEdit"
v-if="item.id == cacheTodo.id">
9️⃣ 在方法集 methods 物件中新增一個 cancelEdit function
cancelEdit: function() {
this.cacheTodo = {}
}
🔟 按一下 enter 儲存編輯
@keyup.enter
- 帶入 item 為參數
<input type="text" class="form-control"
v-model="cacheTitle"
@keyup.esc="cancelEdit()"
@keyup.enter ="doneEdit(item)"
v-if="item.id == cacheTodo.id">
1️⃣1️⃣在方法集 methods 物件中新增一個 doneEdit function
doneEdit: function(item) {
item.title = this.cacheTitle;
this.cacheTitle = '';
this.cacheTodo = {}
}
6.🔷計算未完成待辦事項數量
在 vue computed 中新增 undoneTodos function
.filter()
computed: {
filteredTodos : function(){
...
},
undoneTodos: function(){
return this.todos.filter(todo =>todo.completed != true);
}
}
將數字 3 替換成 vue 指令
{{ ... }}
<span>還有 {{ undoneTodos.length }} 筆任務未完成</span>
7. 🟠清除所有任務
在 <a> 標籤綁定事件觸發器
@click
<a href="#" @click="removeAll">清除所有任務</a>
在方法集 methods 物件中新增一個 removeAll function
removeAll: function(){
this.todos = [];
}
完成後程式碼
html :
<div id="app" class="container mt-4">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">待辦事項</span>
</div>
<input type="text" class="form-control" @keyup.enter="addTodo" v-model="newTodo" placeholder="準備要做的任務">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="addTodo">新增</button>
</div>
</div>
<div class="card text-center">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link" :class="{'active': visibility == 'all'}" @click="visibility='all'" href="#">全部</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{'active': visibility == 'active'}" @click="visibility='active'" href="#">進行中</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{'active': visibility == 'completed'}" @click="visibility='completed'" href="#">已完成</a>
</li>
</ul>
</div>
<ul class="list-group list-group-flush text-left">
<li class="list-group-item" v-for="(item, key) in filteredTodos" @dblclick="editTodo(item)">
<div class="d-flex" v-if="item.id !== cacheTodo.id">
<div class="form-check">
<input type="checkbox" class="form-check-input" v-model="item.completed" :id="item.id">
<label class="form-check-label" :class="{'completed' : item.completed}" :for="item.id">
{{item.title}}
</label>
</div>
<button type="button" class="close ml-auto" @click="removeTodo(item)" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<input type="text" class="form-control"
v-model="cacheTitle"
@keyup.esc="cancelEdit()"
@keyup.enter ="doneEdit(item)"
v-if="item.id == cacheTodo.id">
</li>
</ul>
<div class="card-footer d-flex justify-content-between">
<span>還有 {{ undoneTodos.length }} 筆任務未完成</span>
<a href="#" @click="removeAll">清除所有任務</a>
</div>
</div>
</div>
css :
.completed {
text-decoration: line-through
}
js:
var app = new Vue({
el: "#app",
data: {
newTodo: '',
todos:[
{
id: '123',
title: '待辦事項1',
completed: true
}
],
visibility: 'all',
cacheTodo: {},
cacheTitle: '',
},
methods: {
addTodo: function(){
var value = this.newTodo.trim();
var timeStamp = Math.floor(Date.now());
if(!value){
return;
}
this.todos.push({
id: timeStamp,
title: value,
completed: false
})
this.newTodo = '';
},
removeTodo: function(todo){
var vm = this;
var newIndex = vm.todos.findIndex(function(item, key){
return todo.id === item.id;
})
this.todos.splice(newIndex, 1);
},
editTodo: function(item){
console.log(item);
this.cacheTodo = item;
this.cacheTitle = item.title;
},
cancelEdit: function() {
this.cacheTodo = {}
},
doneEdit: function(item) {
item.title = this.cacheTitle;
this.cacheTitle = '';
this.cacheTodo = {}
},
removeAll: function(){
this.todos = [];
}
},
computed: {
filteredTodos : function(){
if(this.visibility == 'all'){
return this.todos;
}else if(this.visibility == 'active'){
var newTodos =[];
this.todos.forEach(function(item){
if(!item.completed){
newTodos.push(item);
}
})
return newTodos;
}else if(this.visibility == 'completed'){
var newTodos =[];
this.todos.forEach(function(item){
if(item.completed){
newTodos.push(item);
}
})
return newTodos;
}
},
undoneTodos: function(){
return this.todos.filter(todo =>todo.completed != true);
}
}
})