相关文章
todoList案例-全局总线实现组件间通信
本篇内容在前面几篇实现的基础上添加个编辑按钮,要求
(1)点击编辑按钮后,编辑事项,此时编辑按钮隐藏,输入框自动获取焦点
(2)编辑完后当输入框失去焦点,保存编辑后的内容
实现效果如下:
依旧使用组件自定义事件和全局事件总线两种方式一起实现组件间通信
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
beforeCreate(){
// 安装全局事件总线
Vue.prototype.$bus=this
}
}).$mount('#app')
<template>
<div id="app">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader @addTodo="addTodo">MyHeader>
<MyList
:todos="todos"
>MyList>
<MyFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
>MyFooter>
div>
div>
div>
template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
export default {
name: "App",
components: {
MyHeader,
MyFooter,
MyList,
},
data() {
return {
todos:JSON.parse(localStorage.getItem("todos")) || [],
};
},
methods:{
// 添加一个列表事项
addTodo(e){
// console.log("我是App组件,我收到了数据",x)
this.todos.unshift(e)
},
// 勾选or取消勾选todo事项
checkTodo(id){
// 拿到todos中的每一项,找到对应id的那一项
this.todos.forEach((todo)=>{
if(todo.id === id)
todo.done = !todo.done
})
},
// 点击删除一个todo
deleteTodo(id){
this.todos = this.todos.filter((todo)=>{
return todo.id !== id
})
},
// 控制全选或全不选
checkAllTodo(done){
this.todos.forEach((todo)=>{
todo.done = done;
})
},
// 清除已经完成的事项
clearAllTodo(){
this.todos = this.todos.filter((todo)=>{
return !todo.done
})
},
// 更新一个todo
updateTodo(id,title){
this.todos.forEach((todo)=>{
if(todo.id===id)
todo.title=title
})
}
},
watch:{
// 监视todos,当todos发生改变时,触发
todos:{
handler(value){
localStorage.setItem("todos",JSON.stringify(value))
}
}
},
mounted(){
this.$bus.$on('checkTodo',this.checkTodo)
this.$bus.$on('deleteTodo',this.deleteTodo)
this.$bus.$on("updateTodo",this.updateTodo)
},
beforeDestroy(){
this.$bus.$off('checkTodo')
this.$bus.$off('deleteTodo')
this.$bus.$oof('updateTodo')
}
}
script>
<style>
/* 整体样式 */
body {
background: rgba(230, 241, 245, 0.816);
}
.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-edit {
color: #fff;
background-color: skyblue;
border: 1px solid rgb(73, 189, 197);
margin-right: 5px;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
style>
<template>
<div>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,回车键确认"
@keyup.enter="add"
/>
div>
div>
template>
<script>
import {nanoid} from "nanoid"
export default {
name: "MyHeader",
methods:{
add(event){
// 如果输入为空,则结束方法,不添加任何数据
if(!event.target.value){
return alert("输入不能为空")
}
// 使用能生成唯一标识的库nanoid或者uuid生成id,这里用nanoid(轻量级的uuid)
// 将用户的输入包装成一个todo对象
const todoObj = {id:nanoid(),title:event.target.value,done:false}
this.$emit("addTodo",todoObj)
event.target.value='' // 回车后清空输入框中的内容
}
},
};
script>
<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(82, 168, 236, 0.6);
}
style>
<template>
<div>
<ul ul="todo-main">
<MyItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
>MyItem>
ul>
div>
template>
<script>
import MyItem from "./MyItem.vue";
export default {
name: "MyList",
components: { MyItem },
props:['todos']
};
script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
style>
<template>
<div>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="handleCheck(todo.id)"
/>
<span v-show="!todo.isEdit">{{ todo.title }}span>
<input
type="text"
:value="todo.title"
v-show="todo.isEdit"
@blur="handleBlur(todo,$event)"
ref="inputTitle"
/>
label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">
删除
button>
<button class="btn btn-edit" @click="handleEdit(todo)" v-show="!todo.isEdit">编辑button>
li>
div>
template>
<script>
export default {
name: "MyItem",
props: ["todo"], // 这里todo是个对象,由MyList传过来的,存储了id、title、done属性
methods: {
handleCheck(id) {
// 通过事件总线触发事件checkTodo,并传入id
this.$bus.$emit("checkTodo", id);
},
// 删除事项
handleDelete(id) {
if (confirm("确定删除吗?")) {
// 通过事件总线触发事件deleteTodo,并传入id
this.$bus.$emit("deleteTodo", id);
}
},
// 编辑事项
handleEdit(todo) {
// 如果todo上已经有了isEdit,直接修改isEdit的值
if (todo.hasOwnProperty("isEdit")) {
todo.isEdit = true;
} else {
// todo上没有isEdit时,追加isEdit到todo上
this.$set(todo, "isEdit", true); // 给todo上添加一个新的属性(可以匹配getter,setter方法)
}
// $nextTick能够让dom节点更新后在执行里面的代码
this.$nextTick(function(){
// 让输入框在点击编辑按钮后自动聚焦
this.$refs.inputTitle.focus()
})
},
// 失去焦点回调,执行修改逻辑
handleBlur(todo,e) {
todo.isEdit = false;
if(!e.target.value)
return alert("输入不能为空")
this.$bus.$emit('updateTodo',todo.id,e.target.value)
},
},
};
script>
<style scoped>
<template>
<div>
<div class="todo-footer" v-show="total">
<label>
<input type="checkbox" :checked="isAll" @change="checkAll"/>
label>
<span> <span>已完成{{doneTotal}}span>/ 全部{{total}}span>
<button class="btn btn-danger" @click="clearAll">清除已完成任务button>
div>
div>
template>
<script>
export default {
name: "MyFooter",
props:['todos'],
computed:{
total(){
return this.todos.length
},
doneTotal(){
// 高端一点的方法
// return this.todos.reduce((pre,current)=>{
// return pre + (current.done ? 1 : 0)
// },0)
// 普通的方法
let i=0
this.todos.forEach((todo)=>{
if(todo.done) i++
})
return i
},
isAll:{
// 看全选框是否勾选
get(){
return this.doneTotal === this.total && this.total>0
},
// isAll被修改时set被调用
set(value){
// this.checkAllTodo(value)
this.$emit("checkAllTodo",value);
}
}
},
methods:{
// 全选或取消全选
checkAll(event){
// this.checkAllTodo(event.target.checked)
this.$emit("checkAllTodo",event.target.checked)
},
// 清除已完成事项
// clearAll(){
// this.clearAllTodo()
// }
clearAll(){
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>
当不使用$nextTick时,分析如下代码
在编辑事项时,是先执行完整个handleEidt方法中的代码再去进行模板解析的。
执行完if…else这段代码后,并未直接进行模板解析,而是继续找下面的代码执行,因此这里的focus聚焦在模板解析前就执行了,而此时输入框还没有出现再页面上,因此并没有发挥作用。
而通过 n e x t T i c k 可 以 让 这 串 代 码 ‘ t h i s . nextTick可以让这串代码`this. nextTick可以让这串代码‘this.refs.inputTitle.focus`在完成一次模板解析后再执行,也就是让dom节点在更新后再执行$nextTick里面的代码。
$nextTick的使用:通常在改变数据后,要基于更新后的新DOM进行某些操作时,在nextTick所指定的回调函数中执行。
此外,通过使用定时器setTimeOut也可以实现该功能
setTimeOut(()=>{
this.$refs.inputTitle.focus();
},200)