0%

[Vue] 用 Vue 寫一個 todo-list

運用已學到的基礎與進階指令,試著寫出一個 todo-list 待辦事項清單吧!

先附上成品 Demo,以下會一步步說明該如何實作一個 todo-list。

基本架構:輸入欄及待辦事項列表

HTML 部分

  • 任務輸入欄:v-model="newTodo"
  • 輸入按鈕:@click="addTodo"
  • 渲染每項任務的 <li>v-for="items in todos"todos 是一個陣列,用來儲存所有待辦事項
  • <li> 裡面的 checkbox:checkbox 要用 v-bind 綁定 todos 中每個物件的 id 特性,:id="item.id"
  • <li> 裡面的 label:要指向跟 checkbox 同樣的 id,所以要寫成 :for="item.id"<label> 中間用雙花括號包住 item.title
  • 任務的完成狀態:在 checkbox 使用 v-model="item.completed"
  • 加上按鍵功能:為了讓按下 Enter 也能產生跟點擊一樣的效果,在 input 上綁定按鍵事件,@keyup.enter="addTodo"

Vue 的 data 部分

1
2
3
4
5
6
7
8
9
10
newTodo:'', // 一個字串,對應到任務輸入欄
todos: [] // 一個陣列,用來儲存所有待辦事項,對應到 <li>

// todos 陣列
// 裡面放物件,一個物件就代表一個任務,該物件會有以下的特性
{
id: '',
title: '',
completed: false
}

Vue 的 methods 部分 (把新事項加到列表)

1
2
3
4
5
6
7
8
9
10
11
methods: {
addTodo: function() {
// 抓取輸入的文字
var value = this.newTodo;
// 賦予 id - 將當下的時間轉為數字
var timestamp = Math.floor(Date.now());
// 把資料以物件格式推進 todos 陣列
this.todos.push({ id: timestamp, title: value, completed: false });
this.newTodo = '';
}
}

輸入欄防呆

依照上面的設定可以順利新增任務了,但是如果沒在 <input> 打入文字也會送得出去,所以需要增加一些防止空白的效果。

addTodo() 裡面的程式碼稍作修改:

1
2
3
4
5
6
7
8
9
addTodo: function() {
// 加上 trim() 去除頭尾空白並防止沒鍵入文字
var value = this.newTodo.trim;
// 如果輸入欄空白就不會往下執行
if(!value){
return;
}
... // 後面一樣
}

刪除待辦事項

刪除功能必須仰賴待辦事項在陣列中的索引值,才能辨認使用者要刪除的是哪一筆資料,所以要將索引值設參數傳入刪除按鈕綁定的事件中去處理。

  • 將原本 <li> 設定的 v-for 值改為 (item, key) in todoskey 代表待辦事項的索引值
  • <li> 中的刪除按鈕綁定事件:@click="removeTodo(key)"
  • methods 增加 removeTodo()
    1
    2
    3
    4
    removeTodo: function(key) {
    this.todos.splice(key,1);
    // 括號中的 1 代表從該索引值起,刪除 1 筆資料
    }

為已完成事項加上刪除線

寫一個 CSS 樣式,內容是在文字上加上刪除線。
由於 chceckbox 上已經綁定了 v-model="tem.completed" 可以雙向修改 data 資料中 completed 的值,所以接下來只要在 <label> 上切換 class 即可。
<label> 上設定::class="{ 'completed': item.completed }"

製作「全部」、「進行中」、「已完成」頁籤切換,及過濾相對應的待辦事項

頁籤切換

data 新增一個特性,這個特性會去感應使用者點擊的是哪一個頁籤,這邊將該特性取名作 visibility,且它的值可以先預設成第一頁頁籤的名稱。
在 HTML 頁籤結構的 <a> 上使用切換 class 的指令::class="{'active': visibility == '頁籤名稱'}",各個頁籤 <a> 都要加這一行,名稱記得替換。頁籤名稱可以依據各頁代表的意義來取。
接著在頁籤 <a> 上綁定點擊事件:@click="visibility = '頁籤名稱'"

頁籤過濾

切換頁籤的功能做好後,繼續做頁面內容的替換(過濾)。
剛剛我們呈現在畫面上的都是從 todos 陣列撈出來的資料 (原始資料),但其實畫面上應該呈現的是過濾後的資料。
所以要在 computed 裡面新增方法,運用 todos 進行資料的過濾。

methods 同層新增一個 compute 物件,並宣告一個新方法(這邊取名為 filteredTodos),用來執行過濾功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
compute:{
filteredTodos: function() {
if(this.visibility=='全部任務頁籤名'){
return this.todos;
} else if (this.visibility=='進行中任務頁籤名') {
anotherTodos = [];
this.todos.forEach(function(item){
if(!item.completed) {
anotherTodos.push(item);
}
})
return anotherTodos;
} else if (this.visibility=='已完成任務頁籤名') {
anotherTodos = [];
this.todos.forEach(function(item){
if(item.completed) {
anotherTodos.push(item);
}
})
return anotherTodos;
}
}
}

<li> 原本寫的 v-for = "(item, key) in todos"todos 改為 filteredTodos,可以這樣寫是因為 filteredTodos 回傳的是一個陣列。(in 後面可以接陣列或物件)

刪除功能的修正

加入了頁籤換頁功能後,原本的刪除功能會失靈,這是因為不同頁籤就是內容不同的陣列,因此其他頁籤的待辦事項索引值會跟在原始陣列 todos 不同,透過修正 removeTodo() 的程式碼,去比對在不同頁籤中的待辦事項 id 是否相同,如果 id 相同,就回傳它在原始陣列 todos 的索引值,統一從 todos 裡刪除,這樣才不會刪錯。

1
2
3
4
5
6
7
8
removeTodo: function(todo) {
var vm = this;
var newIndex = vm.todos.findIndex(function(item, key){
// 這裡的 item 指的是 todos 裡的元素
return todo.id === item.id;
})
this.todos.splice(newIndex,1);
}

原本 removeTodo() 是傳入 key 當參數,要改成 todotodo 代表所點選的項目;刪除按鈕綁定的事件 @click="removeTodo(key)",要把參數 key 改成 item

用變數存取 this 的必要性

若在 forEach 中的 callback 函式內使用 this 來存取 data 中的屬性,就會發生讀取不到的問題,為了保險起見還是會宣告個 vm 變數,以確保存取的屬性是 Vue 實例中的 data 內的屬性。

雙擊修改待辦事項內容

<li> 的雙擊事件

在設定 v-for<li> 上,再綁一個雙擊事件的監聽:@dblclick="editTodo(item)"

data 預存要編輯的任務

data 新增兩個特性,用來預存要編輯的物件及該物件的文字:

1
2
cacheTodo: {},
cacheTitle: ''

methods 增加 editTodo() 方法

1
2
3
4
5
editTodo: function(item){
// 將雙擊的項目傳入 data
this.cacheTodo = item;
this.cacheTitle = item.title;
}

添加編輯任務的輸入欄

把一個 <input> 寫在待辦列表 <li> 裡面的最下方(刪除按鈕的下面)。
這個 <input> 就是拿來編輯待辦事項用的,<input> 跟待辦事項兩者不會同時顯示,所以要在 <li> 的下一層(包住所有表單元素的 <div>)設定判斷是否要渲染一個待辦事項出來。

  • <li> 下判斷:
    1
    2
    v-if="item.id !== cacheTodo.id"
    <!-- 這一行的意思是:只顯示沒有被雙擊的項目 -->
  • <input> 上設定的判斷則相反:
    1
    2
    v-if="item.id == cacheTodo.id"
    <!-- 項目被雙擊時被 input 取代 -->

關於渲染的邏輯
v-if 條件為 true 時會把內容的 DOM 渲染出來,條件為 false 時會把 DOM 移掉。

  1. 雙擊 <li> 時,產生 cacheTodo.id,讓 item.id == cacheTodo.idtrue,所以 <input> 就會渲染出來 。
  2. 沒有雙點擊 <li> 時, 不會產生 cacheTodo.id,讓 item.id !== cacheTodo.idtrue,所以待辦事項就會渲染出來。

<input> 欄位中顯示被雙擊的任務文字

<input> 上設定:v-model="cacheTitle"

按下 Esc 鍵時取消編輯

1
2
<!-- HTML input -->
@keyup.esc="cancelEdit()"
1
2
3
4
5
// JS methods
cancelEdit: function(){
// 切換到未雙擊時的狀態
this.cacheTodo = {};
}

項目修改完成

按下 Enter 鍵就用新文字替換掉舊的,在 <input> 上設定:@keyup.enter="doneEdit(item)"
methods 去新增方法:

1
2
3
4
5
6
7
doneEdit: function(item){
// 按下 Enter 的瞬間把編輯中文字變為編輯完成的項目
item.title = this.cacheTitle;
// 回復到未點擊狀態
this.cacheTitle = '';
this.cacheTodo = {};
}

關於將暫存資料清空
doneEdit 方法中的 this.cacheTitle = ''; 刪除也不會影響功能,是因為在 editTodo 方法中已經用 this.cacheTitle = item.title; 先把 this.cacheTitle 的內容用當下這筆事項的內容取代了,當編輯不同筆事項時,this.cacheTitle 中的值都會被使用者正在編輯的事項內容給取代。

如果沒有 this.cacheTitle = '' ; 編輯完成後 cacheTitle 的值就會固定成改變後的值,而不是原來預設在 data 裡的空值。雖然 editTodo 方法中已經寫好了替換編輯內容的效果,但假設有要再用 cacheTitle 做其他資料的處理,可能就會受影響,所以保險起見把它的值歸零。

未完成任務數目與清除所有任務

未完成任務數目

  • computed 新增方法,目的是計算 todos.completedfalse 的數量
  • filter() 過濾 todos
  • 取得一個未完成任務組成的陣列
  • compute 方法 return 這個陣列的長度
  • 在 HTML 中用雙花括號插入 compute 方法名

清除所有任務

  • 在「清除所有任務」<a> 上綁定事件監聽
  • methods 新增方法,把 todos 宣告為空陣列即可