精读 Vue 官方文档系列 🎉
个人认为一个 Todo 应用的核心在于数据的结构的定义:
{
id:Number
title:String,
completed:Boolean
}
然后围绕这个数据结构进行 增、删、改、存就可。
先看 存储,官方示例封装了一个简易的 todoStorage 对象,并提供了 fetch 与 save 两个方法。
var todoStorage = {
key:'TODOS_STORAGE_KEY',
fetch(){
var todos = JSON.parse(localStorage.getItem(this.key) || '[]');
todos.forEach((todo, index)=>{
todo.id = index;
});
todoStorage.uid = todos.length;
return todos;
},
save(todos){
return localStorage.setItem(this.key, JSON.stringify(todos));
}
}
数据的存储,可以直接深度监听数据源本身的改变来自动处理,这样就不需要开发者考虑更多的交互场景了。
{
data(){
return {
todos:todoStorage.fetch(),
newTodo:'',
editTodo:null,
status:'all'
}
},
watch:{
todos:{
handler(newValue){
todoStorage.save(newValue);
},
deep:true
}
}
}
再看增、删、改中的**“增”**
在文本输入框中使用 v-model 来双向更新 this.newTodo 的值,再点击添加时,再对 this.newTodo 的值进行包装,然后将其加入到 this.todos 中,接着便是触发深度监听,进行数据的自动存储,最后再将 this.newTodo = ‘’ 置空,用于添加完成后,清空文本框中的输入记录。
{
methods:{
addTodo(){
if (this.newTodo && this.newTodo.trim()) {
var todo = {
id: todoStorage.uid++,
title: this.newTodo,
completed: false
};
this.todos.push(todo);
this.newTodo = '';
}
}
}
}
至于删就很简单了,直接在 todos 的循环列表中,将当前 todo 对象传入到删除的方法中。
this.todos.splice(this.todos.indexOf(todo), 1)
接着,便是改,如果是对 todos 循环列表中的每个 todo 选项都提供修改功能,则可以直接将当前的 todo 选项的 title 双向绑定到输入框中,这样更简洁,优雅。
<input v-if="editTodo === todo" v-todo-focus="editTodo === todo" v-model="todo.title" @keydown.enter="doneEditTodo(todo)" @keyup.esc="cancelEditTodo(todo)" />
<span v-if="!editTodo" @dbclick="editTodo(todo)">{{todo.title}}</span>
为了以防用户修改标题后,但是没有保存,而是又取消了,所以在触发修改操作的时候,还得事先提前保存当前 todo 选项的 title,因为存储值的变量不需要是响应式对象,所以这里我们直接在组件实例上动态新增一个属性。
{
methods:{
editTodo(todo){
this.cacheTodoTitle = todo.title;
this.editTodo = todo;
}
}
}
当用户触发修改,但是输入的过程中又点击取消时,则从 cacheTodoTitle 中还原之前的值。
{
methods:{
cancelEditTodo(todo){
todo.title = this.cacheTodoTitle;
this.editTodo = null;
}
}
}
若是用户修改完成,点击保存,触发 doneEditTodo(todo) 方法即可,因为数据的保存是深度监听的,都是自动完成的,不过我们可以在这个方法里面做一些完善操作。
{
methods:{
doneEditTodo(todo){
todo.title = todo.title.trim();
if(!todo.title){
this.removeTodo(todo)
}
this.editTodo = null;
}
}
}
最后,还有两点值得仔细讨论。
todos 列表循环渲染以及状态筛选
就像前面的示例那样,我们更推荐在计算属性中进行条件或状态的筛选,而且循环渲染列表的数据也不是数据源本身,而是用计算属性来作循环渲染列表。
var filters = {
all(todos){
return todos;
},
active(todos){
return todos.filter(todo=>!todo.completed);
},
completed(todos){
return todos.filter(todo=>todo.completed);
}
};
{
computed:{
filtered({todos}){
return filters[this.visibility](todos);
}
}
}
visibility是一个变量,它的值是filters对象的 key。
由于这里的筛选条件是互斥的,不能聚合或者是并列处理,所以官方实例中将它们封装到一个 filters 对象中,然后在计算属性中通过响应式变量 this.visibility 来间接触发,这样,如果筛选条件变化了,就会触发计算属性来筛选数据,而数据源变化了,也会触发计算属性,重新进行筛选。
<li v-for="todo in filtered" :key="todo.id">
<input v-if="editTodo === todo" v-model="todo.title" @keydown.enter="doneEditTodo(todo)" @keyup.esc="cancelEditTodo(todo)" />
<span v-if="!editTodo" @dbclick="editTodo(todo)">{{todo.title}}</span>
</li>
全部完成功能
全部完成功能的实现方式有很多:
- 监听事件、然后执行方法、将所有
todo的completed置为true。 v-model更新一个响应式变量的状态,然后监听这个响应式数据,执行相应的处理。
这里,官方示例给出了更简洁的解决方案,完整的只使用一个计算属性就可以完成全部完成的功能。
{
computed:{
remaining(){
return filters.active(this.todos).length;
},
allDone:{
set(value){
this.todos.forEach(todo=>{
todo.completed = value;
});
},
get(){
return this.remaining === 0;
}
}
}
}
<input type="checkbox" v-model="allDone" />
最后,还有一个自定义指令:
{
directives: {
"todo-focus": function(el, binding) {
if (binding.value) {
el.focus();
}
}
}
}