Vue To-do List 拆解練習

Ivy Ho
IvyCodeFive
Published in
29 min readJul 13, 2020
https://cdn-images-1.medium.com/max/1920/1*nfvapd86apvGH-hNBYkYuw.png

需載入 CDN

  1. Vue
  2. 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">&times;</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>

拆解需要的功能

  1. 🔴新增
  2. 🟢刪除
  3. 🟣按下checkbox 加上刪除線
  4. 🟡頁籤切換功能與篩選顯示資料
  5. 🔵修改(雙擊修改已新增內容)
  6. 🔷計算未完成待辦事項數量
  7. 🟠清除所有任務

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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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);
}
}
})

--

--

Ivy Ho
IvyCodeFive

"You don't have to be great to start, but you have to start to be great."