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>