Demo
todoMVC-vue 的應用- Demo
todoMVC 分析
學習大約 10 天的 vue 後,嘗試製作入門的 todoMVC 功能。這次實作著重在於功能的撰寫,把 todoMVC 官網上的版本 clone 下來,我是用 jQuery 的版本來修改,刪除 js 所有檔案,保留 html 靜態網頁的部分、css 版面等等,再引入 vue 的 cdn。
功能分析
先分析有哪些功能,怎麼觸發功能,再思考怎麼去執行。
主要有七個功能:
- 新增待辦項目
- 刪除項目功能
- 修改項目狀態
- 選取(取消選取)全部
- 清除完成項目
- 編輯已存在的項目
- 篩選狀態後顯示項目
新增待辦項目
輸入後按下 enter 即可新增,綁定keyup.enter
事件。
刪除待辦項目
移動到已存在的項目後,右方會有 x 按鈕,點擊會刪除該項目。
預計用splice
來刪除選取到的陣列中該 index 的項目。
修改項目狀態
點擊該按鈕,狀態會在執行中跟已完成切換,下方的未完成的數量會根據狀態來計算。
數量的部分,自己分析是在 vue 實例中給一個長度 0,新增項目就+1,切換狀態到已完成或是刪除就-1。
選取(取消選取)全部
點擊選取全部或是取消選取全部。
清除已完成項目
刪除是狀態已完成的項目,分析用 for 迴圈來執行全部待辦事項的陣列,判斷狀態是已完成就刪除。
編輯已存在的項目
click 後會進入編輯狀態,這部分一開始只有分析出,點擊後會帶入 class 來產生顯示編輯狀態,按 enter 會送出,esc 會離開不修改。
篩選狀態後顯示項目
task 有兩種狀態,active 執行中與 completed 完成,選擇 all 則是全部狀態都顯示,一開始預設是 all 顯示。
這部分認為是用 v-if, v-else 來顯示,具體做法還不確定,先執行其他功能。
選擇 active,判斷狀態是進行中的 task
completed,判斷是已選取的 task
功能
新增待辦項目
保留原 html 內容,v-model
綁定inputValue
,v-on:keyup.enter="add"
綁定 keyup 事件執行新增項目的 function。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <input class="new-todo" type="text" v-model="inputVaule" @keyup.enter="add" placeholder="Add to do item..." autofocus />
<script> data:{ inputValue: '', items: [], todoLen: 0, }, methods:{ add() { let todoContent = { text: this.inputVaule, completed: false, }; this.items.push(todoContent); this.todoLen++; this.inputVaule = ''; } </script>
|
刪除待辦項目
顯示的待辦項目中,button 綁定 click 事件來執行刪除項目的函式。
數量的部分,自己分析是在 vue 實例中給一個長度 0,新增項目就+1,切換狀態到已完成或是刪除就-1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <ul class="todo-list"> <template v-for="(item, index) in items"> <li :class="{completed: item.completed}"> <div class="view"> <input class="toggle" type="checkbox" /> <label>{{item.text}}</label> <button class="destroy" @click="removeTodo(index)"></button> </div> </li> </template> </ul>
<script> methods:{ removeTodo(index) { if (this.items[index].completed == false) { this.todoLen--; } this.items.splice(index, 1); }, } </script>
|
修改項目狀態
點擊該按鈕,狀態會在執行中跟已完成切換。用 v-model 綁定項目狀態,再綁定 change 事件來執行函式。
這邊要注意的是,如果項目原本是未完成的,點擊時會因為 v-model 綁定狀態,會把項目的狀態改回 true 也就是已完成。這時候 change 事件執行的函式,就需要判斷如果是改變後的狀態是 true,計算未完成的數量要減一。
反之原本是已完成的項目,點擊後,狀態改為未完成、數量加一。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <ul class="todo-list"> <template v-for="(item, index) in items"> <li :class="{completed: item.completed}" v-else> <div class="view"> <input class="toggle" type="checkbox" v-model="item.completed" @change="changeTodo(index)" /> <label>{{item.text}}</label> <button class="destroy" @click="removeTodo(index)"></button> </div> </li> </template> </ul>
<script> methods:{ changeTodo(index) { if (this.items[index].completed == true) { this.todoLen--; } else { this.todoLen++; } }, } </script>
|
選取(取消選取)全部
點擊選取全部或是取消選取全部。綁定點擊執行 selectAll 函式。
判斷如果現在未完成數量等於 0,用 for 迴圈把陣列 items 中每一個項目的狀態都改為 false,然後把未完成數量改為陣列的長度。
如果未完成數量不等於 0,也用 for 迴圈把每一個項目的狀態改為 true 以達成全選的效果,未完成數量則改為 0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <section class="main"> <input id="toggle-all" class="toggle-all" type="checkbox" @click="selectAll" /> <label for="toggle-all">Mark all as complete</label> <ul class="todo-list"> <li>...</li> .... </ul> </section>
<script> selectAll() { if (this.todoLen == 0) { for (let i = 0; i < this.items.length; i++) { this.items[i].completed = false; } this.todoLen = this.items.length; } else { for (let i = 0; i < this.items.length; i++) { this.items[i].completed = true; } this.todoLen = 0; } }, </script>
|
清除已完成項目
刪除是狀態已完成的項目,用 for 迴圈來執行全部待辦事項的陣列,判斷狀態是已完成就刪除。
在實作的時候發現,執行時會有異常,狀態有些項目突然變成已完成等等。
原因是 splice 執行時會改變陣列長度,所以只有在不刪除元素狀況下才會去執行行 i++。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <footer class="footer" v-if="items != ''"> <span class="todo-count"><strong>{{todoLen}}</strong> item left</span> <ul class="filters"> <li>...</li> ... </ul> <button class="clear-completed" @click="clearCompleted"> Clear completed </button> </footer>
<script> methods:{ clearCompleted() { for (let i = 0; i < this.items.length;) { if (this.items[i].completed == true) { this.items.splice(i, 1); } else { i++; } } }, } </script>
|
編輯已存在的項目
在實作編輯功能的時候,卡關很久,網路上搜尋資源,很多方式來達成,但因為前面功能自己寫,跟網路上做法有些差異,後來網路上找到別的相對簡單方式來實現。
分析的時候,這個 css 配置是 li + input 搭配li class="editing"
就會轉換成編輯模式。
用 click 來觸發編輯模式,double click在mobile上不支援。
v-if 判斷顯示編輯模式
另外還需要 v-if 來判斷顯示,在 vue instance 的 data 新增一個editedItem: null
,這是用來暫存編輯項目的原本內容,還用來判斷顯示編輯模式會取用的值。
如果 editedItem 不是 null,就會顯示編輯模式,所以在 click 執行的函式中,會把原本項目的內容存到這裏來,這樣編輯模式也會顯示。原本的待辦項目顯示用 v-else 則不會渲染出來。
註:這邊 v-if 也需要判斷暫存資料(editedItem)中的 index 跟目前選取的項目 index 是否相同,這樣編輯模式才會一次顯示一個。
html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <ul class="todo-list"> <template v-for="(item, index) in items"> <li class="editing" v-if="editedItem !== null && editedItem.index === index" > <input class="edit" v-model="item.text" @keyup.enter="editFinished(item)" @keyup.esc="cancelEdit(item)" v-focus="true" /> </li> <li :class="{completed: item.completed}" v-else> <div class="view"> <input .... /> <label @click="editItem(item, index)">{{item.text}}</label> <button ...></button> </div> </li> </template> </ul>
|
編輯的部分需要三個功能
- 在原本項目上 click 後執行
- 編輯完成後 keyup.enter 執行函式
- 取消編輯,keyup.esc 執行函式
Click 後執行函式
觸發事件後執行editItem(item, index)
。
把 data 中要當作暫存的 editedItem 指定為目前項目的內容、狀態,index 也加起來是為了判斷編輯模式只會顯示在正確的項目。
這邊不用this.editedItem = item
,是因為這樣會變成直接引用,只要 item 有修改,暫存區的內容也會跟著修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| data: { editedItem: null, }, methods:{ editItem(item, index) { this.editedItem = { index: index, text: item.text, completed: item.completed }; }, }
|
編輯完成按下 enter 後執行函式
在編輯模式的 input 中用了 v-model 綁定item.text
,輸入會直接綁定 item 的內容。
按下 enter 則是會把用來暫存的 editedItem 清空改為 null,也會讓編輯模式的 v-if 條件變為 false 而不渲染出來。
1 2 3 4 5 6
| methods:{ editFinished(item) { this.editedItem = null }, }
|
取消編輯,按下 esc 後執行函式
Click 後進入編輯模式後,尚未編輯或者是編輯到一半,因為 v-model 綁定輸入值,所以按下 esc 後要從暫存的地方取回資料後再去清空改為 null。
1 2 3 4 5 6 7 8
| methods:{ cancelEdit(item) { item.text = this.editedItem.text; this.editedItem = null; }, }
|
補充:進入編輯模式後,自動 focus 在 input 上
在 html 的 input 中加上v-focus="true"
。
這樣在 click 顯示編輯模式的時候也會直接 focus 在 input 上。
1 2 3 4 5 6 7 8
| directives: { focus: { inserted(el) { el.focus() } } },
|
篩選狀態後顯示項目
篩選的這個功能也是卡關很久,也是參考跟編輯功能相同資源來學習。
在 vue instance 的 data 中新增 filterTask,預設用'all'
,意思就是顯示全部。再用 computed 來計算屬性(fitlerItems),在 html 透過綁定 click 事件來修改 filterTask 的內容,進而觸發 computed 的屬性計算。
computed 的 filterItems,在判斷 data 中 filterTask 的值後再用 filter 來 return 值。
如果是'active'
,return 的則會是只有 item.completed = false 的 item。
如果是'completed'
,return 的則會是只有 item.completed = true 的 item。
其餘的(也就是'all'
),就會 return 在 vue instance 中的待辦項目陣列。
用 v-for 來顯示待辦項目中的陣列也改為 computed 中的 filterItems 來渲染。
另外每一個篩選的按鈕要綁定 class,用三元判斷式來判斷 filterTask 的內容,如果符合=true,就回傳selected
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| <section class="main"> <ul class="todo-list"> <template v-for="(item, index) in filterItems"> <li>...</li> </template> </ul> </section> <footer class="footer" v-if="items != ''"> <span class="todo-count"><strong>{{todoLen}}</strong> item left</span> <ul class="filters"> <li> <a href="#/all" :class="(filterTask == 'all')? 'selected' : ''" @click="filterTask ='all'" >All</a > </li> <li> <a href="#/active" :class="(filterTask == 'active')? 'selected' : ''" @click="filterTask ='active'" >Active</a > </li> <li> <a href="#/completed" :class="(filterTask == 'completed')? 'selected' : ''" @click="filterTask ='completed'" >Completed</a > </li> </ul> <button class="clear-completed" @click="clearCompleted"> Clear completed </button> </footer>
<script> data:{ filterTask: 'all', }, computed: { filterItems() { if (this.filterTask === 'active') { return this.items.filter(item => !item.completed) } else if (this.filterTask === 'completed') { return this.items.filter(item => item.completed) } else { return this.items } }, }, </script>
|
資料存進 localsotrage
做完上面這些功能,可以說是完成了,但只要網頁一重新整理,全部的待辦事項都會清空了,所以這時候就需要使用瀏覽器中的 localstorage 來存取。
使用 js 語法,把儲存與獲取的兩個 function 指定在一個物件裡面,方便取用。
要注意,存進 localstorage 時要轉為字串,取用時要用 parse 把字串解析回來。
1 2 3 4 5 6 7 8 9 10 11
| const storageLists = "toDoMVC-vue"; let todoStorage = { getToDos() { let todos = JSON.parse(localStorage.getItem(storageLists) || "[]"); return todos; }, saveToDos(item) { localStorage.setItem(storageLists, JSON.stringify(item)); } };
|
在設定好 localstorage 存取的函式後,就可以來設定什麼時候存取了。
vue instance data 中的 itmes 待辦項目陣列,直接指定執行獲取 localstorage 的函式。
watch 偵聽 items 陣列,只要 items 中有資料變更,就會存到 localstorage 中。
1 2 3 4 5 6 7 8 9 10 11 12
| data: { items: todoStorage.getToDos(), },
watch: { items: { handler(items) { todoStorage.saveToDos(items); }, deep: true }, },
|
mounted 執行未完成項目數量的初始化
在左下角會顯示剩餘未完成項目的數量,在更新網頁時會歸零,因為在 vue data 中預設值是 0。
為了處理這個狀況,在 methods 中寫了一個初始化數量的函式,用 for 迴圈來判斷整個待辦項目陣列的長度後,再來判斷狀態為 false 的話,數量參數 todoLen 就要+1。
並在 vue 中的 mounted 中去執行這個函式,這樣才每次網頁重新整理建立 vue 的時候就會執行。
1 2 3 4 5 6 7 8 9 10 11 12 13
| methods:{ initTodoLen() { let itemsLen = this.items.length; for (let i = 0; i < itemsLen; i++) { if (this.items[i].completed === false) { this.todoLen++; } } }, }, mounted() { this.initTodoLen(); },
|