编辑todo

2019-01-076415 阅读10 评论

编辑 todo 的功能略微有点复杂,我们一点点来分解。

首先根据之前的分析,Vue 很容易知道我们想要编辑的是哪一个 todo,只要把当前的 todo 传给绑定的方法即可。我们实现的功能是双击当前的 todo 进入编辑状态,我们就可以在后边的输入框编辑这个 todo,因此需要给元素绑定一个双击监听事件。还有一点需要注意,我们有一个取消编辑的功能,假设用户将 todo 编辑到了一半又不想编辑了,想回到原来的 todo 内容,我们该怎么办?为了解决这个问题,我们可以在 data 返回的对象添加一个属性,让它来暂存编辑前的 todo 状态,如果用户取消了编辑,就让已修改的 todo 退回到之前的状态:

var app = new Vue({
        el: '#todo-app',
        data: function () {
            return {
                todos: [],
                newTodoTitle: '',
                editedTodo: null // 用于暂存编辑前的 todo 状态
            }
        },
    })

另一个问题是后面那个编辑框十分烦人,因为无论我们是否在编辑状态,这个框始终出现。应该根据当前的 todo 是否处于编辑状态来决定它的出现与否,只有当用户双击 todo 进入编辑状态时才出现,那么怎么知道这个 todo 是否是处于当前状态呢?

我们之前在 data 返回的对象里增加了一个 editedTodo 属性,并且把它的初始值设为 null,这个属性的用途是用来暂存编辑前的 todo 状态的,即当用户编辑某个 todo 时,这个 editedTodo 就会被设置为当前 todo 未被编辑前的值。换句换说,如果 editedTodo 为 null,则一定说明这个 todo 不在编辑状态。所以我们可以根据 editedTodo 的值来判断。

<ul>
    <li v-for='todo in todos' :key='todo.id'>
        ...
        <input type="text" 
               value="编辑 todo..."
               v-if="editedTodo !==null"/>
    </li>
</ul>

然后我们为双击 todo 添加一个 editTodo 方法,这个方法把编辑前的 todo 状态暂存到 editedTodo。双击事件为 dblclick:

<li v-for='todo in todos' :key='todo.id'>
      <span :class="{finished: todo.finished}" 
            @dblclick="editTodo(todo)">{{ todo.title }}</span>
      ...
</li>

<script>
    let id = 0; // 用于 id 生成
    var app = new Vue({
        ...
        methods: {
            ...
            editTodo: function (todo) {
                this.editedTodo = {id: todo.id, title: todo.title}
            }
        },
    })
</script>

特别注意这里我们使用了

this.editedTodo = {id: todo.id, title: todo.title, finished: todo.finished}

而不是简单的 this.editedTodo=todo 进行复制,因为这样做的话仅仅只是将对 todo 的引用存到 this.editedTodo,这样的话任何对 todo 的修改都会反映到 editedTodo 上,为了防止这种情况,我们要为 editedTodo 创建一个全新的对象。

打开浏览器刷新,多创建几条 todo(一定要多创建几条),然后双击 todo 的标题,你会发现...

好吧,这并不符合我们的预期,我们希望双击哪条todo,哪条 todo 对应的编辑框弹出来,而不是所有的都弹出来。仔细分析一下我们的代码,我们根据 this.editedTodo !==null 来决定是否显示编辑框,而当某条 todo 被编辑时,this.editedTodo !==null 不再成立,所以所有编辑框都出现了,怎么解决这个问题呢?

只有在被编辑的 todo 的 id 和被暂存的 editedTodo 的 id 相等时,才表示这条 todo 在编辑,而其它的 todo 的 id 和 editedTodo 的 id 都是不相等的,所以我们可以加一个判断:

<ul>
    <li v-for='todo in todos' :key='todo.id'>
        <span :class="{finished: todo.finished}">{{ todo.title }}</span>
        ...
        <input type="button" value="删除" @click="removeTodo">
        <input type="text" value="编辑 todo..."
               v-if="editedTodo!==null && editedTodo.id===todo.id"/>
    </li>
</ul>

要注意 Vue 允许我们在指令中写入任何合法的 javascript 表达式,Vue 会自动对其求值。

然后我们将编辑框的值和 todo 的 title 值双向绑定,那么 todo 的 title 就会跟着编辑框输入的值来变化了,我们也不用担心用户改变了 todo 的 title 值,因为我们已经把编辑前的 todo 的状态暂存到了 editedTodo,想反悔可以随时还原。表单绑定用 v-model:

<li v-for='todo in todos' :key='todo.id'>
    ...
    <input type="text" value="编辑 todo..."
           v-if="editedTodo!==null && editedTodo.id===todo.id"
           v-model="todo.title"/>
</li>

然后就是用户敲击回车,编辑完成,这里我们给编辑框绑定了一个 keyup 方法监听键盘事件,enter 是修饰符,表示这个键盘事件是按下回车,此时会调用 editDone 方法。用户按下回车后因为 todo.title 的值本身就是随着编辑框输入的值变化的,所以我们基本不用做什么事情,如果用户编辑已经完成,暂存的 todo 就不再需要了,我们可以简单地把 editedTodo 还原成 null,这样编辑框的 v-if 判断就会失效,编辑框自动隐藏,完美!

<li v-for='todo in todos' :key='todo.id'>
    ...
    <input type="text" value="编辑 todo..."
           v-if="editedTodo!==null && editedTodo.id===todo.id"
           v-model="todo.title"
           @keyup.enter="editDone(todo)"/>
</li>

<script>
    let id = 0; // 用于 id 生成
    var app = new Vue({
        ...
        methods: {
            ...
            editDone: function (todo) {
                this.editedTodo = null
            }
        },
    })
</script>

取消怎么办呢?取消就是把已经编辑修改的 todo 标题还原,我们的原始信息存在 editedTodo,取出来即可。用户按键盘的 ESC 键进行取消编辑,为此绑定一个键盘事件,和 enter 类似,用 esc 修饰该事件,表示按下的是 ESC 键,然后调用 cancelEdit 方法,该方法将 todo 还原成编辑前的状态:

<li v-for='todo in todos' :key='todo.id'>
    ...
    <input type="text" value="编辑 todo..."
           v-if="editedTodo!==null && editedTodo.id===todo.id"
           v-model="todo.title"
           @keyup.enter="editDone(todo)"
           @keyup.esc="cancelEdit(todo)"/>
</li>

<script>
    let id = 0; // 用于 id 生成
    var app = new Vue({
        ...
        methods: {
            ...
            cancelEdit: function (todo) {
                todo.title = this.editedTodo.title;
                this.editedTodo = null
            }
        },
    })
</script>

注意要编辑框聚焦后按 ESC 才有效。

同样因为用户编辑已经取消,todo 状态已经还原,暂存的 todo 也不再需要了,我们可以简单地把 editedTodo 还原成 null,这样 v-if 判断就会失效,编辑框自动隐藏。

练习

我们应用的体验有一点点不好的地方,如果用户把编辑的内容清空然后按回车确认修改,这时一条空的 todo 就保存了。我们认为用户清空内容就是不想要这条 todo 了,毕竟现实一条空的 todo 没有意义,所以我们应该删除掉这条 todo,实现这个需求。(hint:我们之前实现了 removeTodo 方法,就是做这个的。学会复用代码而不是重复造轮子。)

另外一个不爽的地方就是双击 todo 后弹出编辑框,焦点并没有自动转到编辑框,只有手动点击编辑哭后我们才能编辑内容。这个需求的实现设计到 Vue 的自定义指令功能,我们在下一节来实现。

-- EOF --

10 评论
登录后回复
Qin Yuan
2020-07-12 13:36:08

再次感谢CyberFork和楼主的耐心.
我在CyberFork的基础上,给每一条todo都加了一个newTitle,所以可以同时编辑几条.
否则的话我觉得应该要防止一条编辑了既没有按esc又没有按enter就跑去下一条

回复
mingxiaoxu
2020-04-21 19:16:16

这章编辑todo还有一个体验不好的地方就是如果是已完成的todo双击点击也会出现编辑框,所以还应该在editTodo方法前面判断这个todo是不是已完成:
editDone: function(todo){
if (todo.title.length === 0){
this.deleteTodo(todo)
}
this.editedTodo = null
}

回复
mingxiaoxu mingxiaoxu
2020-04-21 19:17:22

editTodo: function(todo){
if (todo.completed===true){
return
}
this.editedTodo = {id: todo.id, title: todo.title}
}

回复
CyberFork
2020-01-08 10:16:00

博主,感谢您的教程。
我现在使用了一种新方式来编辑更新todo:
1. 首先在li中使span双击调用进入该条的编辑模式@dblclick='editTodo(todo)'
在editTodo(todo)中将todo.editingTodo = true 设置
根据todo.editingTodo 条件渲染隐藏span,显示编辑的text input。
2. 之后在input中输入数据,使用v-model='editedTodo'绑定数据到editedTodo中,最后如果回车是空数据则不更新,如果不是空数据则更新数据todo.title = this.editedTodo。
3. 这样就不需要像您教程一样额外使用一个this.editedTodo = {id: todo.id, title: todo.title}啦

<span v-if='!todo.editingTodo' :class='{completed:todo.completed}'        @dblclick='editTodo(todo)'>{{ todo.title }}</span>
 <input v-if='todo.editingTodo' type="text" @keyup.enter='modifyTodo(todo)' v-model='editedTodo' />

data: function () {
                return {
                    todos: [],
                    newTodoTitle: '',
                    isEmpty: 0,
                    editedTodo: '' //
                }
            },
            methods: {
                addTodo: function () {
                    this.newTodoTitle ? this.todos.push({
                        id: id++,
                        title: this.newTodoTitle,
                        completed: false,
                        deleted: false,
                        editingTodo: false
                    }) : this.emptyEnter();
                    this.newTodoTitle = ''
                },
                editTodo: function (todo) {
                    todo.editingTodo = true
                },
                modifyTodo: function (todo) {
                    this.editedTodo ? todo.title = this.editedTodo : todo.editingTodo = false
                    todo.editingTodo = false
                }
回复
追梦人物 CyberFork
2020-02-25 11:35:31

嗯!不错的实现方式,学习了!

回复
ijackwu
2019-10-10 16:20:09

v-if='editedTodo!==null && editedTodo.id===todo.id'

上面好像跑不通...

v-if='!(editedTodo !== null && editedTodo.id === todo.id)'

回复
ijackwu
2019-10-10 16:08:55

前面标记完成是:completed

后面变成: finished

回复
zhangyupeng233
2019-07-31 16:08:55

练习那一处有一个问题  就是在editDone()中, 调用removeTodo()方法, 

this的指向发生了变化,  直接调用removeTodo. this指向我们定义的vue,

在editDone中调用removetodo的话, this指向了vue.methods

回复
zhangyupeng233 zhangyupeng233
2019-07-31 16:11:55

于是我在removeTodo这个方法中, 将this.todos修改为了 app.todos, 请教一下这两种写法有什么区别和影响吗

回复
zhangyupeng233 zhangyupeng233
2019-08-01 11:42:21

事实证明是我用错了方法     

我应该直接再editDone()中调用 this.removeTodo(),

 由于对vue不熟悉, 我调用了this.$option.methods.removeTodo(),

导致了这个错误

回复

目录