1. 列表數據渲染
經過在ul 中的li 中v-for 綁定數組lists 進行列表數據渲染css
2. 增長數據
在el-input 上經過 @keyup.enter.native="addItem"
經過按下回車鍵來增長數據。方法綁定對象來獲取輸入框數據,使用push 方法在lists 中插入新的對象。最後將輸入框內容清空。html
3. 刪除數據
在span.close 上經過 @click="remove(index)"
來刪除當前選項。方法放入index 參數,直接經過splice 方法根據index 刪除lists 中對應的對象。vue
4. 編輯數據
在data 中新增屬性 currentItem: null,後在存放內容的label 上綁定方法 @dblclick="currentItem = item"
當雙擊時currentItem 等於當前item,而後在經過v-show 默認隱藏的input 修改框中讓 v-show="currentItem === item"
,只有currentItem 爲當前對象時input 修改框纔出現。input 修改框中默認的value 爲當前對象的內容。node
最後在input 框上綁定方法用來完成修改 @keyup.enter="editTitle(item, $event)" @blur="editTitle(item, $event)"
當按下回車鍵或者失去焦點時修改完成。修改完成後將currentItem 的內容重置爲null。web
5. 標記已完成的事項
在綁定了label 的input上設置 @click="item.completed = !item.completed"
經過點擊事件來改變當前對象的completed 屬性。而且 v-model="item.completed"
根據事項的完成情況來決定是否選中。而後將當前事項的li 父元素也設置 :class="{checked:item.completed}"
根據事項的完成狀況來改變樣式。這樣當點擊label 圖片時將事項標記爲已完成的狀態。element-ui
6. 未完成事項個數
經過computed 來對未完成事項的數量進行監聽,經過this.lists.filter(item => { return !item.completed }).length;
根據條件將原數組中未完成事項返回造成一個新數組,而後返回該數組的長度。數組
7. 清除已完成事項
使用方法removeCompleted 來清除已完成的事項。
先根據remaining 的值來判斷此時是否有已完成的事項,無則警告框彈出,有則詢問是否刪除。刪除方法爲更新lists 的值,經過 this.lists = this.lists.filter(element => { return !element.completed;}
過濾completed 爲true 的事項,返回未完成事項組成的數組。app
8. 事項全選狀態的改變
設置一個向下箭頭符號用來改變全部事項的全選狀態,並根據事項全選和未全選的狀況來反饋箭頭符號的狀態。
在全選框checkbox 中綁定computed 計算屬性 v-model="toggleAll"
。在get 方法中根據未完成數remaining 是否爲0來判斷此時全部事項是否全選。
在set 方法中監聽toggleAll 數值,對全部事項狀態進行改變,當toggle 爲true 時遍歷lists 的全部對象,將computed 屬性修改成true,反之亦然。svg
9. 根據哈希值hash 來顯示事項
在data 新增屬性filterStatus 來獲取當前哈希值,在Vue 實例外使用方法函數
//當路由hash 值發生變化以後,會自動調用該函數 window.onhashchange = function(){ const hash = window.location.hash.substr(2) || "all"; app.filterStatus = hash; }
而後在computed 中新增計算屬性filterItems 用來監聽filterStatus,並根據filterStatus 的數值來調用array.filter() 方法過濾數組並返回。這裏使用了switch 語句。
回到li 標籤中,將 v-for="(item, index) in lists"
更改成 v-for="(item, index) in filterItems"
,默認所有顯示。
對於All Active Completed 三個選項的class 屬性是否改變也與filterStatus 的值相關聯。
10.使用自定義組件實現自動聚焦
將自動聚焦指令綁定到input.editInput 上實現自動聚焦。
Vue.directive("app-focus", { inserted(el, binding){ el.focus(); }, update(el, binding){ //更新以後也能調用 el.focus(); } })
此處須要調用update 方法否則沒法做用。具體緣由暫不清楚,有懂的看官歡迎來評論告知!
本弱雞目前也還不清楚怎麼讓此處的el-input 實現自動聚焦功能,autofocus 和自定義指令都無效。。。
11. 調用localStorage 實現本地存儲
在Vue 實例外對localStorage.getItem
和 localStorage.setItem
方法進行封裝。這兩個方法都是根據特定的key 值進行數據儲存和提取的。
使用watch 監聽lists 的變化,此處要監聽對象屬性的改變須要開啓深度監聽deep:true。當lists 數組的內容改變時調用 localStorage.setItem
將newValue 存入localStorage。
關於提取localStorage 的數據。將data 中的lists 變爲lists:itemStorage.fetch()
,這樣就能夠將localStorage 內JSON 格式的數據賦值給lists 數組。
注意:代碼中的link 和script 須要本身從新引入,主輸入框和警告框用了elementUI 組件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./node_modules/element-ui/lib/theme-chalk/index.css"> <title>Document</title> </head> <style> body, html, ul, h1{ margin: 0; padding: 0; font-size: 12px; color: black; } div#app{ max-width: 600px; margin: 0 auto; } h1.title{ padding: 10px 0; font-size: 80px; font-weight: normal; color: brown; opacity: 0.5; } div.todolist{ border: 1px solid gainsboro; box-shadow: 2px 0 5px lightgray; } div.header{ position: relative; border-bottom: 1px solid gainsboro; } input.el-input__inner{ padding: 25px 0px 25px 60px; border: none; outline: none; font-size: 24px; text-indent: 5px; } input.el-input__inner::placeholder{ color: lightgrey; } div.el-input span.el-input__suffix{ font-size: 24px; margin: 2px 10px 0px 0px; } input#toggle-all{ position: absolute; border: none; opacity: 0; } label.toggle-all-label{ position: absolute; display: block; box-sizing: border-box; top: 1px; left: 1px; z-index: 1; width: 48px; height: 48px; font-size: 32px; color: gainsboro; transform: rotate(90deg); -webkit-user-select: none; } label.toggle-all-label::before{ content: "❯"; position: absolute; left: 16px; } input#toggle-all:checked + label.toggle-all-label{ color: gray; } div.body ul li{ position: relative; padding: 10px 10px 10px 65px; border-bottom: 1px solid lightsteelblue; font-size: 20px; font-weight: normal; font-family: 楷體; color: rgb(36, 78, 121); } div.body ul li:last-child{ border-bottom: none; } div.body ul li.checked{ color: lightgray; text-decoration: line-through; } input.toggle{ position: absolute; border: none; opacity: 0; } label.toggle-label{ position: absolute; top: 2px; left: 4px; z-index: 1; width: 40px; height: 40px; border-radius: 20px; background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); } input.toggle:checked + label.toggle-label{ background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E"); } span.close{ position: absolute; right: 10px; top: 12px; font-weight: bold; color: firebrick; opacity: 0; transition: opacity .2s; cursor: pointer; } div.body ul li:hover span.close{ opacity: 1; } input.editInput{ position: absolute; top: 0; left: 0; z-index: 2; width: 89.1%; padding: 10px 0 10.5px 65px; font-size: 20px; font-weight: normal; font-family: 楷體; border: none; outline: none; box-shadow: inset 0px -1px 5px 0px rgba(0, 0, 0, 0.3); } div.footer{ padding: 12px; border-top: 1px solid lightgray; overflow: hidden; } span.itemNum{ margin-right: 20%; } a.operate{ display: inline-block; padding: 5px 8px; margin-right: 2px; border: 1px solid transparent; border-radius: 5px; text-decoration: none; } a.operate:hover{ border: 1px solid lightgray; } a.checked{ border: 1px solid lightgray; } a.clearAll{ margin-left: 10%; text-decoration: none; } a.clearAll:hover{ text-decoration: underline; cursor: pointer; } span.itemNum, a.operate, a.clearAll{ font-size: 14px; font-weight: normal; color: gray; } </style> <body> <div id="app"> <div style="padding: 0 10px; text-align: center;"> <h1 class="title">todos</h1> </div> <div class="todolist"> <div class="header"> <input type="checkbox" id="toggle-all" v-model="toggleAll"> <label for="toggle-all" class="toggle-all-label"></label> <el-input v-model="input" suffix-icon="el-icon-edit" placeholder="What needs to be done..." @keyup.enter.native="addItem"></el-input> </div> <div class="body"> <ul style="list-style-type: none;"> <li v-for="(item, index) in filterItems" :key="item.id" :class="{checked:item.completed}"> <label @dblclick="currentItem = item">{{ item.title }}</label> <input type="checkbox" :id="'toggle' + item.id" class="toggle" v-model="item.completed" @click="item.completed = !item.completed"> <label :for="'toggle' + item.id" class="toggle-label"></label> <span class="close" @click="remove(index)">×</span> <input type="text" class="editInput" :value="item.title" v-show="currentItem === item" @keyup.enter="editTitle(item, $event)" @blur="editTitle(item, $event)" v-app-focus="item === currentItem"> </li> </ul> </div> <div class="footer"> <span class="itemNum">{{ remaining }} item<span v-show="false">s</span> left</span> <a href="#/" :class="{operate:true, checked:filterStatus === 'all'}">All</a> <a href="#/active" :class="{operate:true, checked:filterStatus === 'active'}">Active</a> <a href="#/completed" :class="{operate:true, checked:filterStatus === 'completed'}" >Completed</a> <a class="clearAll" @click="removeCompleted">Clear Completed</a> </div> </div> </div> <script src="./node_modules/vue/dist/vue.js"></script> <script src="./node_modules/element-ui/lib/index.js"></script> <script> //自定義個storage 的key const STORAGEKEY = "items-vuejs"; //拓展功能:將數據保存在本地 const itemStorage = { //獲取數據 fetch(){ let data = localStorage.getItem(STORAGEKEY) || "[]";//經過key 獲取數據,當數據爲空時返回空數組 //經過JSON.parse 將JSON 字符串轉換爲JSON 格式 return JSON.parse(data); }, //保存數據(傳入要保存的數據) save(items){ localStorage.setItem(STORAGEKEY, JSON.stringify(items)); //經過JSON 形式保存 } } const lists = [ //初始化數據(使用localStorage 後無關緊要) {id:1, title:"吃飯", completed:false,}, {id:2, title:"學習", completed:true,}, {id:3, title:"休息", completed:true,}, ]; Vue.directive("app-focus", { inserted(el, binding){ el.focus(); }, update(el, binding){ //更新以後也能調用 el.focus(); } }) var app = new Vue({ el:"#app", data() { return { input:"", lists:itemStorage.fetch(), currentItem:null, filterStatus:"all", } }, computed: { remaining(){ return this.lists.filter(item => { return !item.completed }).length; }, toggleAll:{ get:function(){ return this.remaining === 0? true:false; }, set:function(newValue){ this.lists.forEach(element => { element.completed = newValue; }); } }, filterItems(){ switch (this.filterStatus) { case "active": return this.lists.filter(item => !item.completed); break; case "completed": return this.lists.filter(item => item.completed) default: return this.lists; break; } } }, methods: { addItem(event){ let inputVal = event.target.value.trim(); if(inputVal === ""){ this.$message.error({ message:"Writing something here..."} ); }else{ this.lists.push({ id:this.lists.length + 1, title:inputVal, completed:false, }); event.target.value = ""; } }, remove(index){ this.lists.splice(index, 1); }, removeCompleted(){ if(this.remaining === this.lists.length){ this.$message.warning({ message:"無已完成的事項。" }); return; }else{ this.$confirm("將清空因此已完成選項,是否繼續?", "提示", { confirmButtonText:"清空", cancelButtonText:"取消", type: "warning" }).then(() => { this.lists = this.lists.filter(element => { return !element.completed;} //返回未完成的 ); this.$message.success({ message:"清空成功!" }); }).catch(() => { this.$message.info({ message:"取消清空。" }); }); } }, editTitle(item, event){ let value = event.target.value; if(value.trim() === ""){ this.$message.warning({ message:"修改後的內容不能爲空。" }); }else{ item.title = event.target.value; this.currentItem = null; } }, }, watch: { //深度監聽,當對象中的屬性發生改變後,使用deep:true 選擇則能夠實現監聽 lists:{ handler: function(newValue, oldValue){ //回調函數 //數組變化時,將數據保存到本地 itemStorage.save(newValue); } , deep:true } }, }) //當路由hash 值發生變化以後,會自動調用該函數 window.onhashchange = function(){ const hash = window.location.hash.substr(2) || "all"; app.filterStatus = hash; } //頁面刷新時清除掉上一次的哈希值,避免因地址欄的哈希值致使按鈕點擊無效果 window.onload = function(){ window.location.hash = ""; } </script> </body> </html>