此项目是基于vue官网上的todoMVC示例仿写而成。
基于MVC设计思想,用到了vue2中的双向绑定、侦听器深度监听、计算属性、过滤器、自定义指令,以及基于localStorage的永久化存储和路由监听触发事件。此项目覆盖了vue的大部分基础知识。
功能点:
- 刷新页面,列表中的内容不会丢失,保持刷新前的状态。
- input输入框输入内容,回车后内容渲染到input下方列表,原input框内容清空。
- 双击input输入框旁边向下的方向箭头,选中所有列表,箭头高亮。再次点击,取消全部选中,箭头变暗。
- 点击列表单行文字部分,进入input编辑框,修改内容,回车或者鼠标点击别处保存,按Esc键退出input框,内容回到编辑前。
- 点击ALL(全部事项)、Active(未完成事项)、Completed(已完成事项),列表渲染状态切换。item left的数值与Active条数一致。点击Clear completed,删除 Completed相关列表。
那么,接下来我们按照功能点一个个实现它们吧!~
刷新页面,列表中的内容不会丢失,保持刷新前的状态
用到的知识点:localStorage 永久储存
定义一个key名,然后通过localStorage将数据储存在本地。如果取出,则通过key名将储存在本地中的数据取出。
let STORAGE_KEY = 'todos-vuejs'
let todoStorage = {
fetch: function () {
let todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]")
todos.forEach((value, index) => {
value.id = index
})
todoStorage.uid = todos.length
return todos
},
save: function (todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
}
input输入框输入内容,回车后内容渲染到input下方列表,原input框内容清空
用到的知识点:双向绑定,事件修饰符
通过v-model="newTodo“ 双向绑定,再用@keyup.enter回车修饰符触发addTodo事件。
addTodo事件中将双向绑定的newTodo的值trim()去掉空格,然后push进todos列表数组中。最后再将newTodo的值清空。
<header class="header">
<h1>备忘录</h1>
<input class="new-todo"
autofocus autocomplete="off"
placeholder="准备做什么?"
v-model="newTodo"
@keyup.enter="addTodo">
</header>
data: {
todos: todoStorage.fetch(),
newTodo: '',
},
methods: {
addTodo: function () {
let value = this.newTodo && this.newTodo.trim()
if (!value) {
return
}
this.todos.push({
id: todoStorage.uid++,
title: value,
completed: false
})
this.newTodo = ''
},
}
点击input输入框旁边向下的方向箭头,选中所有列表,箭头高亮。再次点击,取消全部选中,箭头变暗
用到的知识点:计算属性(计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter )
点击input输入框旁边向下的方向箭头,实际上就是点击了checkbox,值为true,随即便触发了allDone中的set与get,通过forEach遍历将todos数组列表中的每一项的todo.completed变为true,即列表全部选中。再次点击取消,即又触发allDone中的set与get,列表全部取消选中。
单独点击列表单行checkbox即单行选中状态,v-model绑定值为todo.completed,与上面的allDone计算属性中的数据相呼应。
<input type="checkbox" id="toggle-all" class="toggle-all" v-model="allDone">
<label for="toggle-all"></label>
<ul class="todo-list">
<li v-for="todo in filteredTodos"
:key="todo.id"
class="todo"
:class="{completed:todo.completed, editing: todo == editedTodo}">
<div class="view">
<input type="checkbox" class="toggle" v-model="todo.completed">
</div>
</li>
</ul>
computed: {
allDone: {
get: function () {
return this.remaining === 0
},
set: function (value) {
this.todos.forEach((todo) => {
todo.completed = value
})
}
}
},
双击列表单行文字部分,进入input编辑框,修改内容,回车或者鼠标点击别处保存,按Esc键退出input框,内容回到编辑前
用到的知识点:双击事件,事件修饰符,自定义指令,深度监听
@dblclick="editTodo(todo)" 双击触发editTodo(todo)事件,将当前todo.title值存入this.beforeEditCache中。
未双击前input设为隐藏,双击后根据条件判断 将label隐藏,input显示,失焦点或者退出 则值反过来。
事件修饰符用到了:@blur失去焦点修饰符与@keyup.enter回车修饰符和@keyup.esc退出修饰符。
v-todo-focus="todo == editedTodo" 自定义指令当指令值为true时,input聚焦(详情可见vue自定义指令)
当数据结构层级超过一层,常规的watch监听不一定能触发,此时需要深度监听。例如这里的todos数据改变。
<ul class="todo-list">
<li v-for="todo in filteredTodos"
:key="todo.id"
class="todo"
:class="{completed:todo.completed, editing: todo == editedTodo}">
<div class="view">
<input type="checkbox" class="toggle" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input type="text" class="edit"
v-model="todo.title"
v-todo-focus="todo == editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)">
</li>
</ul>
watch: {//深度监听
todos: {
handler: function (todos) {
todoStorage.save(todos)
},
deep: true
}
},
methods: {//方法
removeTodo: function (todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
},
editTodo: function (todo) {
this.beforeEditCache = todo.title
this.editedTodo = todo
},
doneEdit: function (todo) {
if (!this.editedTodo) {
return
}
this.editedTodo = null
todo.title = todo.title.trim()
if (!todo.title) {
this.removeTodo(todo)
}
},
cancelEdit: function (todo) {
this.editedTodo = null
todo.title = this.beforeEditCache
},
},
directives: {//自定义指令
'todo-focus': function (el, binding) {
if (binding.value) {
el.focus()
}
}
}
点击ALL(全部事项)、Active(未完成事项)、Completed(已完成事项),列表渲染状态切换。item left的数值与Active条数一致。点击Clear completed,删除 Completed相关列表
用到的知识点:过滤器,路由监听
这里用到了2个过滤器,第一个filters变量是自己定义的,内部有三个方法,分别过滤:全部,未完成的,已完成的。第二个过滤器为vue内部过滤器,通过双花括号{{ | }} 语法实现。
路由监听则通过window.location.hash取参数值,通过正则取参数后,再结合自定义过滤器来实现todos数据列表过滤。
另外需要注意在window下监听hashchange,否则当url改变时,列表会无法更新。
<footer class="footer" v-show="todos.length" v-cloak>
<span class="todo-count">
<strong>{{remaining}}</strong>{{remaining | pluralize}} left
</span>
<ul class="filters">
<li><a href="#/all" :class="{selected: visibility == 'all'}">ALL</a></li>
<li><a href="#/active" :class="{selected: visibility == 'active'}">Active</a></li>
<li><a href="#/completed" :class="{selected: visibility == 'completed'}">Completed</a></li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
Clear completed
</button>
</footer>
let filters = {//自定义的过滤器
all: function (todos) {
return todos
},
active: function (todos) {
return todos.filter((value) => {
return !value.completed
})
},
completed: function (todos) {
return todos.filter((value) => {
return value.completed
})
}
}
let app = new Vue({
data: {
todos: todoStorage.fetch(),
visibility: 'all'
},
computed: {
filteredTodos: function () {
return filters[this.visibility](this.todos)
},
remaining: function () {
return filters.active(this.todos).length
},
},
filters: {//vue内部过滤器
pluralize: function (n) {
return n === 1 ? 'item' : 'items'
}
},
methods: {
removeCompleted: function () {
this.todos = filters.active(this.todos)
}
},
})
function onHashChange() {//监听路由
let visibility = window.location.hash.replace(/#\/?/, '')
if (filters[visibility]) {
app.visibility = visibility
} else {
window.location.hash = ''
app.visibility = 'all'
}
}
window.addEventListener('hashchange', onHashChange)
onHashChange()
最后挂载即可
app.$mount('.todoapp')
项目展示链接:https://a-tione.github.io/todoMVC/index.html 用vue实现一个便利贴
项目代码:https://github.com/A-Tione/todoMVC