vue
任務:todo list
Udemy:https://www.udemy.com/course/vue-hexschool/learn/lecture/10271404#overview
基本功能:
進階功能:
在最上方的輸入欄,即是<input>
使用v-model
,此v-model
是連接vue的data裏的newTodo
,讓我們能夠在newTodo
儲存使用者所輸入的代辦事項。
在「新增」button設定@click="addTodo"
,以及在<input>
欄位設定@keyup.enter="addTodo"
。作用是當使用者按下新增或enter鍵時,就執行addTodo
函式。
<!-- 把使用者輸入的代辦事項儲存到vue的newTodo -->
<!-- 當使用者按下新增或enter鍵時,就執行addTodo -->
<input type="text" class="form-control" placeholder="準備要做的任務" v-model="newTodo" @keyup.enter="addTodo">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="addTodo">新增</button>
</div>
Vue:
var app = new Vue({
el: '#app',
data: {
// 存放使用者輸入的新代辦事項
newTodo: "",
// 所有代辦事項
todos: [],
},
}
addTodo
函式的作用是用.push
方法,把新增的代辦事項加進todos
裏。每筆代辦事項都需要有:因此,addTodo
的函式應該是這樣:
methods:{
addTodo: function(){
this.todos.push({
id: Math.floor(Date.now()),
title: this.newTodo,
// 預設是false
completed: false,
})
// 清空輸入欄
this.newTodo = "";
}
id
?因為每個代辦事項左邊都有個checkbox,該代辦事項的id需要與該checkbox的id相同,才可以使我們點擊該代辦事項的title時,就可以對應點擊到該checkbox。因此,每個代辦事項都要有一個獨有的id。
Date.now()
方法產生idDate.now()
會產生一組數字,該數字是自 1970/01/01 00:00:00 UTC 起經過的毫秒數。之後用Math.floor()
(取小於這個數的最大整數)去取整數。
<!-- 使用v-for撈取所有在todos裏的代辦事項 -->
<ul class="list-group list-group-flush text-left" v-for="item in todos">
<li class="list-group-item">
<div class="d-flex"
@dblclick="editTodo(item)">
<div class="form-check">
<!-- 用:id和:for 綁定該代辦事項的id -->
<!-- 用v-model來操控該代辦事項是否已被完成 -->
<input type="checkbox" class="form-check-input"
:id="item.id"
v-model="item.completed">
<label class="form-check-label"
:for="item.id">
{{ item.title }}
</label>
</div>
...
</ul>
v-model
在checkbox使用v-model
,會得出boolean值。因此可以操控該代辦事項的complated狀態是true
還是false
。
詳情:https://vuejs.org/v2/guide/forms.html
v-bind
和v-on
縮寫v-bind
縮寫:
用:
。例如::class=""
、:id=""
v-on
縮寫:
用@
。例如:@click=""
、@dblclick=""
、@keyup.enter=""
為了防止使用者新增空白的代辦事項,我們需要在addTodo
函式裏做判斷。以下修改一下addTodo
函式:
addTodo: function(){
// 用trim方法刪掉多餘的空白
const value = this.newTodo.trim();
// 避免輸入空白的代辦事項
if(!value){
return
};
this.todos.push({
// 以下是自己的寫法,自己寫時是用new Date
// id: Math.floor(new Date),
// 但老師建議這樣寫
id: Math.floor(Date.now()),
title: value,
completed: false,
})
// 清空輸入欄
this.newTodo = "";
}
先寫以下做法:
這個做法是把該事項的index直接傳進函式裏,並用它來刪掉資料。
<li class="list-group-item" v-for="(item, key) in filterTodo">
...
<button type="button" class="close ml-auto" aria-label="Close"
@click="deleteTodo(key)">...</button>
</li>
...
methods:{
deleteTodo: function(key){
this.todos.splice(key, 1);
}
}
但以上做法會之後出現bug,請在讀完頁籤分類效果的filterTodo部分後,再跳回來以下部分!
為什麼會有bug?因為當我們轉到其他頁籤時,該頁籤的陣列資料(filterTodo)會不同於原始陣列(todos)。
例如以下情況,我新增了1,2,3,4,完成了2,3,之後我按下已完成頁籤,想刪除3,你會發現3並沒有被刪掉,反而是2被刪除:
這是因為在filterTodo陣列裏,第2筆資料是「3」,但是在todos這個原始陣列裏,第2筆資料是「2」,所以反而是「2」被刪掉了。
因此,要改用以下的寫法:
<!-- 改為傳送item.id -->
<button type="button" class="close ml-auto" aria-label="Close"
@click="deleteTodo(item.id)">...</button>
methods:{
deleteTodo: function(id){
let todoIndex = this.todos.findIndex(item => item.id === id);
this.todos.splice(todoIndex, 1);
}
}
這個方法就是把該代辦事項的id傳進來deleteTodo
,然後在deleteTodo
裏,利用傳進來的id,找尋此id在原始陣列todos裏的index(我命名為todoIndex),最後利用todoIndex在todos陣列裏刪除該筆代辦事項。
這個做法就成功解決那個bug了。
當使用者完成該代辦事項時,該項目會有劃線效果:
CSS樣式:
.completed {
text-decoration: line-through
}
<!-- 切換completed CSS樣式 -->
<label class="form-check-label"
:for="item.id"
:class="{'completed': item.completed}">
{{ item.title }}
</label>
當該代辦事項的completed狀態是true時,就會加上completed
CSS樣式。
以上這些功能,可以大概猜出這裏需要一個過濾功能,例如:
但我們不需要每個狀態都開一個新陣列,例如開一個叫activeTodos
、completedTodos
,把完成和未完成的項目從todos裏面抽取來,並分別push進這兩個陣列裏,這是很累贅的做法。
我們可以這樣想:
如何看你點擊哪個頁籤?我們可以在data
裏設定一個變數,該變數會顯示目前使用者在哪個頁籤,我們把該變數取命為visibility
:
data: {
newTodo: "",
todos: [],
visibility: 'all',
}
在HTML的部分,設定當該頁籤被點擊時,就會更改visibility
的變數值:
<li class="nav-item">
<a class="nav-link" href="#"
:class="{ 'active' : visibility === 'all'}"
@click.prevent="visibility = 'all'"
>全部</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#"
:class="{ 'active' : visibility === 'active'}"
@click.prevent="visibility = 'active'"
>進行中</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#"
:class="{ 'active' : visibility === 'completed'}"
@click.prevent="visibility = 'completed'"
>已完成</a>
</li>
別忘記click後面要加上
.prevent
,因為<a>
本身有連結功能,預設是會跳到href裏的地址,所人要用.prevent
取消跳轉到連結,這等同於之前學JS時的.preventDefault()
。
另外,在頁籤的部分,當該頁籤被點擊後,就會被加上active
這個CSS樣式,例如我點擊了「進行中」後,「進行中」被框起來,以及文字變灰色:
所以以上程式碼中,也用:class
綁定active
這個CSS樣式。
回到Vue的部分,這時候我們會用到computed
,重溫computed
特性:
methods
是沒有緩存機制,不管資料有沒有更新,它都會重新渲染。
在我們的情況,因為當todos陣列(即是放置所有代辦項目的那個陣列)裏有項目被checked,即是被標記為完成,理論上,「進行中」和「已完成」頁面就需要被更新,「進行中」會顯示少了一個項目,「已完成」會顯示多了一個項目。
反之,todos陣列裏沒有變動,就不用重新渲染畫面。
所以這裏用computed
。
computed: {
filterTodo: function(){
if(this.visibility === 'all'){
return this.todos;
}
else if(this.visibility === 'active'){
return this.todos.filter( item => item.completed === false);
}else if(this.visibility === 'completed'){
return this.todos.filter( item => item.completed === true);
}
}
}
之後我們要改成在filterTodo
裏撈資料,而非原始陣列todos
:
<li class="list-group-item" v-for="(item, key) in filterTodo">
...
</li>
接下來要做的功能就是當使用者雙擊該代辦事項時,就會切換成輸入欄位,讓使用者修改該代辦事項的title,如下圖:
這個效果有幾個功能要做:
利用@dblclick=""
來設定雙擊事件,並且執行editTodo
函式。
html的部分:
<div class="d-flex" @dblclick="editTodo(item)">
Vue的部分,我們將該正在被修改的那筆代辦事項以及它的title暫存起來:
data: {
newTodo: "",
todos: [],
visibility: 'all',
// 以下變數會暫時存放該正在被修改的代辦事項
cacheTodo: {},
cacheTitle: ''
}
methods:{
...,
editTodo: function(item){
// 把傳進來的item資料存起來
// 才可以知道目前哪筆資料正在被編輯中
this.cacheTodo = item;
//cacheTitle會用來作編輯時預設顯示的內容
this.cacheTitle = item.title;
// 以上暫存的做法可以達成一次只能編輯一個item,
// 因為每當item被雙擊時,cacheTodo 和 cacheTitle 都會被更新,
// 並指向該被雙擊的item。因此不會出現同時修改多個item的情況
}
}
以下會再解釋為什麼要暫存該筆代辦事項的資料。
雙擊後,就要隱藏原本的代辦事項,並打開input輸入欄。我們可用v-if
去寫出這個效果:
<li class="list-group-item" v-for="(item, key) in filterTodo">
<div class="d-flex"
@dblclick="editTodo(item)"
v-if="item.id !== cacheTodo.id">
<div class="form-check">
<input type="checkbox" class="form-check-input"
:id="item.id"
v-model="item.completed">
<!-- 切換completed CSS樣式 -->
<label class="form-check-label"
:for="item.id"
:class="{'completed': item.completed}">
{{ item.title }}
</label>
</div>
<button type="button" class="close ml-auto" aria-label="Close"
@click="deleteTodo(key)">
<span aria-hidden="true">×</span>
</button>
</div>
<!-- 如果這個item的id與cacheTodo的id相同,就代表這個item正在被編輯中,因此要顯示以下的input欄位 -->
<input type="text" class="form-control"
v-if="item.id === cacheTodo.id">
</li>
以上程式碼解釋了為什麼剛剛我們需要暫存該筆事項,因為在這裏的隱藏/顯示功能中,需要用該筆事項去做判斷:
如果這個代辦事項的id 與 暫存中的代辦事項id相同,就代表這個代辦事項正在被編輯,所以要隱藏原本的資料欄,並打開輸入欄位。同一道理,如果該代辦事項的id 不等於 暫存的id,那就意味該筆代辦事項並沒有正在被修改,所以就隱藏輸份欄,只顯示資料欄。
這裏用v-if
去寫出效果,v-if
的用法就是只要條件是true,就會渲染該元素。
v-if
VS v-show
題外話:
v-if
:條件是true才會渲染v-show
:不管條件是否true都會渲染,並用CSSdisplay
屬性去隱藏或顯示。所以:
v-show
v-if
詳細看:https://vuejs.org/v2/guide/conditional.html
重看上面那張gif,可見雙擊代辦事項,並切換到輸入欄後,輸入欄會預設顯示該代辦項目原本的title(這個gif例子中就是「1」),而非空白:
所以,我們需要在輸入欄位,預設會顯示該筆事項的title。這裏使用v-model
去把title顯示出來:
<input type="text" class="form-control"
v-if="item.id === cacheTodo.id"
v-model="cacheTitle">
<input type="text" class="form-control"
v-if="item.id === cacheTodo.id"
v-model="cacheTitle"
@keyup.esc="cancelEdit"
@keyup.enter="finishEdit(item)"
>
Vue部分:
methods:{
...
cancelEdit: function(){
this.cacheTodo = {};
}
}
以上做法就是清空暫存的代辦事項,作用就是退出編輯。
methods:{
finishEdit: function(item){
// 更改該item的title
item.title = this.cacheTitle;
// 清空cacheTodo,否則input欄位不會隱藏起來
this.cacheTodo = {};
// 問題:以下這段不加也行?因為都是看<input>和<div>都只是看item.id去判斷。而下次修改另一筆資料時,cacheTitle會被替換掉
this.cacheTitle = '';
}
}
<div class="card-footer d-flex justify-content-between">
<span>還有 {{ countActiveTodo }} 筆任務未完成</span>
</div>
computed:{
...
countActiveTodo: function(){
return this.todos.filter( item => item.completed === false).length;
}
}
<div class="card-footer d-flex justify-content-between">
<span>還有 {{ countActiveTodo }} 筆任務未完成</span>
<a href="#" @click.prevent="deleteAllTodo">清除所有任務</a>
</div>
methods:{
...
deleteAllTodo: function(){
this.todos = [];
}
}