内容创作不易,各位帅哥美女,求个小小的赞!!!
【15.给todoList案例添加编辑按钮
】
本篇内容在TodoList案例的基础上添加个编辑按钮,要求:
(1)点击编辑按钮后,编辑事项,此时编辑按钮隐藏,输入框自动获取焦点
(2)编辑完后当输入框失去焦点,保存编辑后的内容
(1)TodoItem.vue文件中先添加一个编辑按钮和input框。
<label>
<input type="checkbox"
:checked="todo.done"
@change="handleCheck()"
/>
<span>{{todo.title}}</span>
<input type="text"
:value="todo.title"
>
</label>
<button class="btn btn-danger" @click="handleDelete()">删除</button>
<button class="btn btn-edit" @click="handleEdit()">编辑</button>
页面展示:
(2)TodoItem.vue文件中,上图中的1
和2
中只能展示一个。
-
用
v-show="todo.isEdit"
和v-show="!todo.isEdit"
二选一来展示。 -
【补充一个点】:v-show=“undefined”【判断为false,页面不展示】;v-show=“!undefined”【判断为true,页面展示】
<span v-show="!undefined">答案是多所</span>
写法一:
handleEdit(todo):将props传过来的数据todo传入函数handleEdit()中,然后<script></script>
中定义函数handleEdit时需要用到todo这个对象时,可以直接用。【正常情况使用props传过来的数据要这样写this.todo
】
<label>
<input type="checkbox"
:checked="todo.done"
@change="handleCheck()"
/>
<span v-show="!todo.isEdit">{{todo.title}}</span>
<input type="text"
:value="todo.title"
v-show="todo.isEdit"
>
</label>
<button class="btn btn-danger" @click="handleDelete()">删除</button>
<button class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
//编辑
handleEdit(todo){
// 判断对象todo里面是否有isEdit属性
if(todo.hasOwnProperty('isEdit')){
todo.isEdit=true
}else{
// 把todo.isEdit设置成响应式数据
//给对象this.todo设置属性isEdit='true'
this.$set(todo,'isEdit',true)
}
},
写法二:
handleEdit():点击事件触发后,直接调用函数handleEdit()中【不带参数】,然后<script></script>
中定义函数handleEdit时需要用到props传过来的数据todo这个对象时,不可以直接用,需要this.todo
去调用props中的数据。【正常情况使用props传过来的数据要这样写this.todo
】
<label>
<input type="checkbox"
:checked="todo.done"
@change="handleCheck()"
/>
<span v-show="!todo.isEdit">{{todo.title}}</span>
<input type="text"
:value="todo.title"
v-show="todo.isEdit"
>
</label>
<button class="btn btn-danger" @click="handleDelete()">删除</button>
<button class="btn btn-edit" @click="handleEdit()">编辑</button>
//编辑
handleEdit(){
// 判断对象todo里面是否有isEdit属性
if(this.todo.hasOwnProperty('isEdit')){
this.todo.isEdit=true
}else{
// 把todo.isEdit设置成响应式数据
this.$set(this.todo,'isEdit',true)
}
},
-
A.hasOwnProperty(‘B’):是用来判断对象A里是否有你给出的名称的属性或对象【B】。有则返回true,没有返回false,不过需要注意的是,
此方法无法检查该对象的原型链中是否具有该属性,该属性必须是对象本身的一个成员
。 -
A.$set(target,propertyName/index,value)
添加的属性propertyName做响应式处理。 -
参数:
-
{Object | Array} target
:对象或数组所在的地方 ,注:但不能是Vue实例或Vue实例的根元素
-
{string | number} propertyName/index
:需要添加的属性名【比如:“性别”】 -
{any} value
:添加的属性值【比如:“男”】
-
-
返回值:设置的值
-
用法:向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如
this.todo.isEdit=true
)
为什么使用Vue.set()
或vm.$set()
或vc.$set()
-
因为受现代JS的限制,vue不能检测到对象属性的添加或删除。由于vue会在初始化实例时对属性执行
getter/setter
转化过程,所以属性必须在data对象上存在才能让vue转换它,这样它才能是响应的。vue不允许在已经创建的实例上动态添加新的根级响应式属性
,不过可以使用Vue.set()方法将响应式属性添加到嵌套的对象上。 -
//这样配置的属性,不是响应式的,Vue无法探测普通的新增property :isEdit this.todo.isEdit=true
(3)点击编辑后,input编辑框自动获取焦点。
-
获取焦点:xxx.focus()【xxxDOM元素获取焦点】【xxxDOM可借用Vue里面的ref属性来获取DOM元素】
-
ref属性:
- 被用来给元素或子组件注册引用信息(id的替代者)
- 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
- 使用方式:
- 打标识:
<h1 ref="xxx">.....</h1>
或<School ref="xxx"></School>
- 获取:
this.$refs.xxx
- 打标识:
-
vue也有自带的获取DOM的方法,那就是ref。它不仅可以获取DOM元素还可以获取组件
1、如果给普通的dom元素使用,引用指向的是dom元素。
2、如果是给子组件使用,引用指向的是子组件的实例。【ref 加在子组件上,用this.$refs.(ref值) 获取到的是组件实例,可以使用组件的所有方法和属性。】
-
$nextTick:将回调延迟到下次 DOM 更新循环之后执行。
- 语法:
this.$nextTick( 箭头函数体 )
- 作用: this.$nextTick这个方法作用是当数据被修改后使用这个方法 回调函数获取更新后的dom再渲染出来【Vue在
更新 DOM
时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列
,视图需要等队列中所有数据变化完成之后,再统一进行更新,即不在此轮【1轮】更新,等到下一轮【2轮】更新】。
- 语法:
-
不要$nextTick这个API行不行?不行。
- 因为下面这段代码【没有
$nextTick
】执行到this.$refs.inputTitle.focus()
这里时【即点击编辑的时候】,这一轮【1轮】:整个页面模板还未重新解析呢【得先把所有的代码执行完,把需要修改的数据修改完后,再进行模板解析,并展示】,但是这一轮【1轮】执行代码this.$refs.inputTitle.focus()
时,页面中的编辑input框在这一轮【1轮】:模板还未解析,又如何能够拿到input框,并聚焦呢?
- 因为下面这段代码【没有
//编辑
handleEdit(){
// 判断对象this.todo里面是否有isEdit属性
if(this.todo.hasOwnProperty('isEdit')){
this.todo.isEdit=true
}else{
// 把todo.isEdit设置成响应式数据
this.$set(this.todo,'isEdit',true)
}
this.$refs.inputTitle.focus()
},
正确的代码示例
<input type="text"
:value="todo.title"
v-show="todo.isEdit"
ref="inputTitle"
>
//编辑
handleEdit(){
// 判断对象this.todo里面是否有isEdit属性,并设置成true
if(this.todo.hasOwnProperty('isEdit')){
this.todo.isEdit=true
}else{
// 把todo.isEdit设置成响应式数据
this.$set(this.todo,'isEdit',true)
}
this.$nextTick(()=>{
this.$refs.inputTitle.focus()
})
},
也可以放在updated()钩子中
- updated钩子中的事件执行时,页面和数据已经保持同步了,都是最新的,即这一轮【1轮】已经更新过了,在【2轮】重新再次渲染页面。
//编辑
handleEdit(){
// 判断对象this.todo里面是否有isEdit属性,并设置成true
if(this.todo.hasOwnProperty('isEdit')){
this.todo.isEdit=true
}else{
// 把todo.isEdit设置成响应式数据
this.$set(this.todo,'isEdit',true)
}
},
// 也可以放在updated钩子中,
updated() {
this.$refs.inputTitle.focus()
},
展示效果:
(4)input编辑框失去焦点,更新编辑框里输入的数据,并正确展示出来。
-
@blur="handleBlur(todo,$event)"
: 失去焦点触发,并调函数handleBlur(),并传给函数两个参数【todo,$event
】-
vue中关于
$event
的通俗理解$event
是指当前触发的是什么事件(鼠标事件,键盘事件等)【此处的$event:FocusEvent
】$event.target
则指的是事件触发的目标,即哪一个元素触发了事件,这将直接获取该dom元素。【此处的$event.target:<input>
】
-
TodoItem.vue
<span v-show="!todo.isEdit">{{todo.title}}</span>
<input type="text"
:value="todo.title"
v-show="todo.isEdit"
ref="inputTitle"
@blur="handleBlur(todo,$event)"
>
handleBlur(todo,e){
// todo.isEdit设置成false,展示<span>里面的数据【<span>里面的v-show为真】
todo.isEdit = false
if(!e.target.value.trim()) return alert('输入不能为空!')
//利用 数据总线通信,触发事件updateTodo,传递两个参数本DOM元素的id,value
this.$bus.$emit('updateTodo',todo.id,e.target.value)
}
- e.target.value:拿到触发事件里面的值【此处是input框里面的值】
- trim() 方法用于删除字符串的头尾空白符。
App.vue
methods: {
//更新一个todo
updateTodo(id,title){
this.todos.forEach((todo)=>{
if(todo.id === id) todo.title = title
})
},
}
mounted() {
// 利用数据总线去通信,方法写在methods里面
this.$bus.$on('updateTodo',this.updateTodo)
},
beforeDestroy() {
// 解绑事件updateTodo
this.$bus.$off('updateTodo')
},
完整代码:
main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false
//创建vm
new Vue({
el:'#app',
render: h => h(App),
// 生命周期钩子beforeCreate中模板未解析,且this是vm
beforeCreate() {
// this:指的是vm
Vue.prototype.$bus = this //安装全局事件总线$bus
}
})
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<TodoHeader @addTodo="addTodo"/>
<TodoList
:todos="todos"
/>
<TodoFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
import pubsub from "pubsub-js";
//引入App的子组件
import TodoHeader from './components/TodoHeader'
import TodoFooter from './components/TodoFooter'
import TodoList from './components/TodoList'
export default {
name:'App',
components: { TodoHeader,TodoFooter,TodoList },
data() {
return {
//由于todos是TodoHeader组件和TodoFooter组件都在使用,所以放在App中(状态提升)
// todos:拿到的是一个字符串,需要解析成一个对象
//【A||B】:第一个操作数A为true,则不会执行第二个操作B。第一个操作数A为false,则会执行第二个操作B。
todos:JSON.parse(localStorage.getItem('todos')) ||[],
// todos为空时,解析出来的对象为null,即todos:null || [],
// 举例:console.log(null || 3); //3
}
},
// 监视属性todos
watch: {
todos:{
// 深度监视开启
deep:true,
//handler什么时候调用?当todos属性发生改变时
// newValue:该属性变化之后的值
handler(newValue){
// newValue:传过来是一个数组对象,需要通过JSON.stringify()转化成一个字符串存储在本地
localStorage.setItem('todos',JSON.stringify(newValue))
}
}
},
methods: {
//添加一个todo
addTodo(todoObj){
// 在数组的开头添加一个数据
this.todos.unshift(todoObj)
},
//全选or取消全选
checkAllTodo(done){
this.todos.forEach(todo => todo.done = done)
},
// 清除所有已经完成的todo
clearAllTodo(){
this.todos= this.todos.filter(todo =>{
return todo.done == false
// 或者换成 return !todo.done
}
)
},
//更新一个todo
updateTodo(id,title){
this.todos.forEach((todo)=>{
if(todo.id === id) todo.title = title
})
},
},
mounted() {
// 利用数据总线去通信
//勾选or取消勾选一个todo
this.$bus.$on('checkTodo',(id)=>{
this.todos.forEach((todo) => {
if(todo.id === id) todo.done = !todo.done
})
}),
this.$bus.$on('updateTodo',this.updateTodo)
// 利用消息的订阅与发布去通信
// 订阅deleteTodo
this.pubId = pubsub.subscribe('deleteTodo',(_,id)=>{
this.todos=this.todos.filter(
(todo)=>{
return todo.id != id
}
)
})
},
beforeDestroy() {
// this.$bus.$off(['deleteTodo','checkTodo'])
this.$bus.$off(['checkTodo'])
this.$bus.$off('updateTodo')
// 取消订阅
pubsub.unsubscribe(this.pubId)
},
}
</script>
<!-- style没有scoped属性:【全局样式】 -->
<!-- style有scoped属性:样式设置只在本组件里起作用【局部样式】 -->
<style>
/*base*/
body {
background: #fff;
}
.btn {
/* 行内的块级元素 */
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
/* 文本内容居中 */
text-align: center;
/* 垂直居中 */
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
/* 字体颜色设置:白色 */
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
/* 鼠标移动到删除按钮时 */
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn-edit{
/* 字体颜色设置:白色 */
color: #fff;
background-color: #4d79b2;
border: 1px solid #1b57a5;
}
/* 鼠标移动到删除按钮时 */
.btn-edit:hover {
color: #fff;
background-color: #1a67ca;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
/* 上下外边距为0,左右自动,实际效果为左右居中*/
margin: 0 auto;
}
/* 后代选择器(包含选择器),选择到的是todo-container下面的所有后代todo-wrap */
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #67dbd1;
border-radius: 5px;
}
</style>
TodoHeader.vue
<template>
<div class="todo-header">
<!-- @keyup.enter="add" :按下回车按键,调用add方法 -->
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add" v-model="title"/>
</div>
</template>
<script>
// 引入nanoid库生成ID号
import { nanoid } from 'nanoid'
export default {
name: 'TodoHeader',
/* //接收从App组件【父组件】传递过来的addTodo方法
props:['addTodo'], */
data() {
return {
title: '',
}
},
methods: {
add(){
// 如果输入框里为空,就跳过下面的代码,并弹窗
if (!this.title.trim()) return alert('请输入值')
//将用户的输入包装成一个todo对象
const todoObj={id:nanoid(),title:this.title,done:false}
//通知App组件去添加一个todo对象
//触发自定义事件addTodo,并把子组件中的参数todoObj传给父组件
this.$emit('addTodo',todoObj)
//清空输入
this.title = ''
}
},
};
</script>
<style scoped>
/* 头部样式设置 */
/* 后代选择器(包含选择器),选择到的是todo-header下面的所有后代input */
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
/* 内边距:上下4px,左右7px */
padding: 4px 7px;
}
/* :focus获得焦点,并设置其新样式:例如:用户单击一个input输入框获取焦点,然后这个input输入框的边框样式就会发生改变,和其他的输入框区别开来,表明已被选中。 */
.todo-header input:focus {
/* outline 与 border 相似,不同之处在于 outline 在整个元素周围画了一条线;它不能像 border 那样,指定在元素的一个面上设置轮廓,也就是不能单独设置顶部轮廓、右侧轮廓、底部轮廓或左侧轮廓。 */
outline: none;
/* 定义边框的颜色 */
border-color: rgba(82, 168, 236, 0.8);
/* boxShadow 属性把一个或多个下拉阴影添加到框上 */
/* 设置inset:内部阴影,不设置inset:外部阴影 */
/* 【0 0】:不设置X轴与Y轴偏移量 */
/* 第三个值【如10px,8px】:设置值阴影模糊半径为15px */
box-shadow: inset 0 0 10px rgba(124, 56, 207, 0.075), 0 0 8px rgba(224, 58, 17, 0.6);
background-color: bisque;
}
</style>
TodoList.vue
<template>
<ul class="todo-main">
<TodoItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
/>
</ul>
</template>
<script>
import TodoItem from './TodoItem'
export default {
name: 'TodoList',
components:{TodoItem},
//声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件TodoItem用的
props: ['todos']
};
</script>
<style scoped>
/*main*/
.todo-main {
/* 左外边距:0px 【盒子贴着盒子】*/
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
TodoItem.vue
<template>
<li>
<label>
<input type="checkbox"
:checked="todo.done"
@change="handleCheck()"
/>
<span v-show="!todo.isEdit">{{todo.title}}</span>
<input type="text"
:value="todo.title"
v-show="todo.isEdit"
ref="inputTitle"
@blur="handleBlur(todo,$event)"
>
<!-- <span v-show="!undefined">答案是多所</span> -->
</label>
<button class="btn btn-danger" @click="handleDelete()">删除</button>
<button class="btn btn-edit" @click="handleEdit()">编辑</button>
</li>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: 'TodoItem',
// 声明接受从别的组件中的todoObj对象,todo
props: ['todo'],
data() {
return {
inputValue:''
}
},
methods: {
//勾选or取消勾选【别弄混了:这里的id其实就是上面change事件中的todo.id】
handleCheck(){
//change事件触发后,通知App组件将对应的todo对象的done值取反
// this.checkTodo(id)
this.$bus.$emit('checkTodo',this.todo.id)
// console.log(this.todo.id);
},
//删除
handleDelete(){
if (confirm('Are you sure you want to delete?')) {
//点击后,发布订阅后通知App组件将对应的todo对象删除,参数
pubsub.publish('deleteTodo',this.todo.id)
}
},
//编辑
handleEdit(){
// 判断对象this.todo里面是否有isEdit属性
if(this.todo.hasOwnProperty('isEdit')){
this.todo.isEdit=true
}else{
// 把todo.isEdit设置成响应式数据
this.$set(this.todo,'isEdit',true)
}
this.$nextTick(()=>{
this.$refs.inputTitle.focus()
})
},
handleBlur(todo,e){
// console.log(e)
// console.log(e.target)
// todo.isEdit设置成false,展示1的数据
todo.isEdit = false
if(!e.target.value.trim()) return alert('输入不能为空!')
this.$bus.$emit('updateTodo',todo.id,e.target.value)
}
},
// 也可以放在updated钩子中,
// updated() {
// this.$refs.inputTitle.focus()
// },
};
</script>
<style scoped>
/*item*/
li {
/* ul无序列表 ol有序列表*/
/* 列表前面无标记 */
list-style: none;
/* height定义了一个li盒子的高度 */
height: 36px;
/* 行高:指的是文字占有的实际高度 */
line-height: 36px;
/* 当height和line-height相等时,即盒子的高度和行高一样,内容上下居中 */
padding: 0 5px;
/* 边框底部:1px的实心线 颜色*/
border-bottom: 1px solid #c0abc3;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代label */
li label {
/* 左对齐浮动【元素一旦浮动就会脱离文档流(不占位,漂浮)】 */
float: left;
/* 鼠标放在label元素上时变成小小手 */
cursor: pointer;
}
li label input {
/* 垂直居中 */
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代button */
li button {
/* 向右浮动 */
float: right;
/* 不为被隐藏的对象保留其物理空间,即该对象在页面上彻底消失,通俗来说就是看不见也摸不到。 */
display: none;
/* 上边距为3px */
margin-top: 3px;
}
li:before {
/* initial:它将属性设置为其默认值。 */
content: initial;
}
/* 结构伪类选择器 选择最后一个li元素 */
li:last-child {
/* 边框底部没有线 */
border-bottom: none;
}
li:hover{
background-color: #ddd;
}
/* 鼠标移动到该元素上时,将button按钮显示出来 */
li:hover button{
/* display:block将元素显示为块级元素 */
display: block;
}
</style>
TodoFooter.vue
<template>
<div class="todo-footer" v-show="total">
<label>
<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/>
可以用下面这行代码代替-->
<!--对于type="checkbox",v-model绑定的就是一个布尔值,isAll为false or true -->
<input type="checkbox" v-model="isAll"/>
</label>
<span>
<span>已完成{{ doneTotal }}</span> / 全部{{ total }}
</span>
<button class="btn btn-danger" @click="clearAllDone">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'TodoFooter',
props: ['todos'],
computed:{
//总数
total(){
return this.todos.length
},
// 已完成数
doneTotal(){
//此处使用reduce方法做条件统计
/* return this.todos.reduce(
// todo:遍历数组todos中的每一个元素
// 比如:数组遍历时,把数组todos中的每一个元素分别赋值给todo【包含id='001'】
(pre,todo)=>{
// console.log('@',pre,todo)
return pre + (todo.done ? 1 : 0)
}
,0) */
//简写
return this.todos.reduce((pre,todo)=>pre + (todo.done ? 1 : 0),0)
},
//控制全选框
/* isAll(){
//计算属性简写:isAll属性,只能被读取,不能被修改
return this.total === this.doneTotal && this.total>0
} */
isAll:{
//get有什么作用?当有人读取isAll时,get就会被调用,且返回值就作为isAll的值
//get什么时候调用?1.初次读取isAll时。2.所依赖的数据发生变化时。
get(){
//全选框是否勾选 【&&:且】
return this.total === this.doneTotal && this.total>0
},
//set什么时候调用? 当isAll被修改时。
// value就是:v-model绑定的值false【未勾选】 or true【勾选】
set(value){
console.log(value)
this.$emit('checkAllTodo',value)
}
},
},
methods: {
/* checkAll(e){
console.log(e.target.checked);
// 拿到的是全选或者全不选的布尔值
this.checkAllTodo(e.target.checked)
} */
// 清空所有已完成
clearAllDone(){
// this.clearAllTodo()
this.$emit('clearAllTodo')
}
},
};
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>