目录
(一)分析结构
可大致分为三个结构
(1)输入组件(2)待办事项组件(3)footer组件
其中待办事项组件又可以分为一个一个的事项item组件
代码顺序应该是:先编写组件基本结构,将静态界面搭建出来,再依次完成数据渲染、输入添加功能、勾选功能、删除功能、底部统计功能和交互功能等。
(二)搭建静态组件
拆分的四个组件进行基本的写入
随便写的css样式,此时所有功能还都不能实现
(三)实现功能
1.动态初始化数据
将数据以对象数组形式存储在ListTodos组件中,通过props配置读取数据完成items信息的展示
使用v-for循环写入
在ListTodos.vue中:
// 数据
data() {
return {
todos: [
{ id: '001', title: '打代码', done: false },
{ id: '002', title: '睡觉', done: true },
{ id: '003', title: '吃饭', done: false }
]
}
},
// v-for循环渲染组件
<div id="todos">
<TodosItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" />
</div>
需要注意的是,将todo传入组件TodosItem中需要用v-bind绑定,这样引号内就不是字符串而是一个表达式,即成功传入一个对象todoObj
在TodosItem.vue中:
<div class="item">
<input type="checkbox" name="selectOne" :checked="todo.done">
<span>{{ todo.title }}</span>
<button>删除</button>
</div>
//定义props配置 todo
props: ['todo']
复选框是否勾选的状态由传入的对象中的done来决定,同样的,使用v-bind绑定后可读取引号内的表达式
剩余的根据复选框来修改todoObj的数据操作后续再完成;待办事项数据的存储位置也后续再优化
2.添加待办事项
输入框输入事项后按enter触发添加事件,但是目前还没有学到同级之间的组件如何进行数据传输,所以转换一个思路,把数据放到父组件,这样两个子组件都能访问到
App.vue文件
<div id="app">
<!-- 将添加对象函数传入 在header文件内调用该函数实现父子的连接 -->
<ListHeader :addObj="addObj" />
<!-- 将数据传入 -->
<ListTodos :todos="todoObjs" />
<ListFooter />
</div>
data() {
return {
todoObjs: [
{ id: '001', title: '打代码', done: false },
{ id: '002', title: '睡觉', done: true },
{ id: '003', title: '吃饭', done: false }
]
}
},
methods: {
// 将header文件内新生成的数组加入到父组件app.vue数据中
addObj(obj) {
this.todoObjs.unshift(obj)
}
},
ListTodos文件
利用props属性接收对象数组后直接使用即可:props: ['todos']
ListHeader文件
<header>
<input type="text" v-model="title" @keyup.enter="add"
placeholder="输入待办事项,按enter键添加">
</header>
// 引入nanoid库
import { nanoid } from 'nanoid'
//...
data() {
return {
title: ''
}
},
methods: {
// 添加函数 将事项名和id、完成状态一起打包成对象再加入到todoObj中
add() {
if (this.title === '') return
const Obj = { id: nanoid(), title: this.title, done: false }
this.addObj(Obj)
this.title = '' //清零
}
},
// 接收函数
props: ['addObj']
nanoid库用于生成一个独一无二的字符串作为id使用
引入:import { nanoid } from 'nanoid' 调用:nanoid() // 返回一个字符串
数组也可以通过props属性直接传到子组件里面用!!!
传入的数组实际this指针还是指向父组件,这样就实现了父组件和子组件的数据交互
除了使用v-model绑定事项的title,还可以用e.target.value
3.勾选的双向绑定
实现待办事项done属性的勾选双向数据绑定
(1)v-model绑定
下面这种v-model方式实现的数据绑定是大错特错的!!!
这样写v-model="todo.done"违反了props只读的规定,只不过在对象内的修改不会修改对象的地址,所以不会引起vue的报错
(2)最初级的绑定
还是和添加一条对象的方式一样,通过点击复选框触发事件,向app.vue传递被点击复选框所对应的id,再在app组件中直接修改对应id的对象的done状态(取反即可)
只不过这样较为麻烦,爷爷组件没办法直接传递checkObj函数给孙子组件,只能先传给listTodos组件,再传递给item组件,这是最基础的写法,后面再学高级的
在App.vue中
<!-- 将数据传入 -->
<ListTodos :todos="todoObjs" :checkObj="checkObj" />
// 将item文件内查找到的点击的复选框对应的事项对象的id传给爷爷组件app中,进行修改完成状态的操作
checkObj(id) {
this.todoObjs.forEach((todo) => {
if (id === todo.id) {
todo.done = !todo.done
console.log(this);
}
})
}
在ListTodos文件中
只需要props传递checkObj函数即可
在TodosItem文件中
<input type="checkbox" :checked="todo.done" @click="check(todo.id)">
props: ['todo', 'checkObj'],
methods: {
check(id) {
this.checkObj(id)
}
},
4.删除功能
(1)通过css实现鼠标经过时高亮,删除键display:block
(2)点击删除按钮触发事件删除该对应事项对象的数据
和上面的checkObj函数相似,需要在item文件获取触发对应事项的id,再依次传递给爷爷App.vue
在app文件中编写对应的删除函数操作
这里有两个删除方法
1.通过filter函数过滤传入id对应的对象,将剩余对象组成一个新的数组赋值给this.todoObjs
deleteObj(id) {
// 1.filter方法生成新数组,再覆盖
this.todoObjs = this.todoObjs.filter((todo) => {
return todo.id != id
})
}
2.通过数组删除函数splice(index,1),和findIndex()找到传入id对应的数组索引下标进行删除
deleteObj(id) {
// 2.splice方法:找到对应数组的下标findindex再删除
this.todoObjs.splice(this.todoObjs.findIndex((todo) => {
return todo.id === id
}), 1)
}
5.底部统计与交互功能
(1)底部统计功能
统计已完成数量和总数量
从app把todoObjs数组传入footer文件
总数量就是传入的todos的长度;todos.length
已完成数量可以通过computed计算,用foreach来计算完成事项的数量
computed: {
total() {
return this.todos.length
},
// 用于计算已完成事项的数量
finish() {
// 基础方法
// let num = 0
// this.todos.forEach(todo => {
// if (todo.done) num++
// })
// return num
// reduce方法
return this.todos.reduce((pre, current) => {
return pre + (current.done ? 1 : 0)
}, 0)
}
},
也可以使用reduce方法更高级更简单;reduce((pre,current)=>{},0)
0是pre的初始值,pre是每次递归的值(显示上一次的值),current是当前对象
(2)交互功能
1.实现勾选后每个事项全部勾选;取消勾选后每个事项全部取消勾选,即跟着总的勾选情况来改动;
在ListFooter文件
<input type="checkbox" @click="selectAll">
selectAll(e) {
// 调用将所有对象的done状态根据复选框的状态而改变
this.selectObjs(e.target.checked)
},
在App.vue文件
// 将所有对象的done属性根据复选框的状态变化
selectObjs(value) {
this.todoObjs.forEach(todo => {
todo.done = value
})
},
2.点击全部勾选后,如果取消单个事项的勾选,则全部勾选要取消;若所有单个事项都勾选了,则全部勾选也要自动勾选上,即根据底部统计的已完成和全部数量来判断
<input type="checkbox"
@click="selectAll"
:checked="finish === todos.length && todos.length != 0">
只需将checked属性双向绑定即可::checked="finish === all && all != 0"
这样将表达式写在标签内看着太繁复了,所以写在计算属性里面才是正解
// 判断已完成事项数量是否是全部数量
isAll() {
return this.finish === this.total && this.total != 0
}
也可以使用v-model="isAll"来绑定,和:checked效果一样,因为v-model的绑定值就是checked;
不过还需要给计算属性isAll加上一个setter不然会报错
还可以让功能更完善点儿,待办事项为0的时候底部可以不显示(没有任何意义)
v-show="total"即可
3.点击清除按钮后实现勾选的事项全部删除
只需要在footer函数写一个按钮触发事件,在app文件中写好clean函数即可
// 将所有对象中完成的对象一键删除 可复用删除函数
cleanObj() {
this.todoObjs.forEach(todo => {
if (todo.done) {
// 这里不能用splice的删除方法,每次删掉一个对象后整个数组的下标也跟着改变了
this.deleteObj(todo.id)
}
})
}
复用deleteObj函数实现删除功能
注意!!!不能用上文写到的splice删除方法!
因为每次删掉一个对象后整个数组的下标也都跟着改变了,会出现没办法删干净有遗漏的情况!
6.数据存储优化:本地存储
在App.vue文件内:
使用监视属性,深度监视存储数据的对象数组,一旦发生改变(增加、删除、完成状态改变)就会触发本地存储更新为与todoObjs对象数组一致;todoObjs对象数组的首次读取是读取本地存储的数据,若没有该数据则返回一个空数组
data() {
return {
todoObjs: JSON.parse(localStorage.getItem('todoObjs')) || []
}
},
watch: {
todoObjs: {
deep: true,
handler(value) {
localStorage.setItem('todoObjs', JSON.stringify(value))
}
}
}
7.使用组件自定义事件升级一下
在app.vue中:
<!-- 组件自定义事件 在header文件里触发该事件则调用函数addObj 子传父-->
<ListHeader @addObj="addObj" />
<!-- 组件自定义事件 子传父 todoObjs是父传子的数据,不需要自定义事件 -->
<ListFooter :todos="todoObjs"
@selectObjs="selectObjs"
@cleanObj="cleanObj" />
和直接用props进行父子通信也差不多
8.使用全局事件总线升级一下
在app.vue中:
mounted() {
// 绑定自定义事件
this.$bus.$on('checkObj', this.checkObj)
this.$bus.$on('deleteObj', this.deleteObj)
},
beforeDestroy() {
// 解绑
this.$bus.$off('checkObj')
this.$bus.$off('deleteObj')
},
在todositem中:
methods: {
check(id) {
this.$bus.$emit('checkObj', id)
},
deleteTodo(id) {
this.$bus.$emit('deleteObj', id)
}
},
使用全局总线就可以实现爷孙通信了,不必再通过item的父组件传递props;推荐使用
9.拓展:新增一个修改事项名的功能
实现点击修改后出现输入框,写入新名字失去焦点后将名字更新显示
在todosItem中:
<span v-show="!isEdited">{{ todo.title }}</span>
<input type="text"
@blur="blurTodo(todo, $event)"
:value="todo.title"
v-show="isEdited"
ref="focusIpt">
<button v-show="!isEdited"
@click="editTodo">修改</button>
data() {
return {
isEdited: false
}
},
methods: {
editTodo() {
this.isEdited = true
// vue在所有处理完后才会开始编译模板,如果直接写聚焦,此时模板还未编译,输入框还未出现,执行聚焦也没有效果
// $nextTick实现模板更新编译后再执行内部的代码
this.$nextTick(function () {
this.$refs.focusIpt.focus()
})
// 也可以使用计时器 达到一个异步处理的效果
// setTimeout(() => {
// this.$refs.focusIpt.focus()
// },);
},
blurTodo(todo, e) {
this.isEdited = false
this.$bus.$emit('editObj', todo.id, e.target.value)
}
},
在item组件中新添加一个数据isEdited来判断该组件是否处于修改状态;
在App.vue中:
mounted() {
// 绑定自定义事件
this.$bus.$on('editObj', (id, title) => {
// 修改为空之后取消修改
if (!title.trim()) return alert("不能为空!")
this.todoObjs.forEach((todo) => {
if (todo.id == id) todo.title = title
})
})
},
beforeDestroy() {
// 解绑
this.$bus.$off('editObj')
},
(四)小总结
现实中一个这样的案例不需要拆成很多个小组件,一个就搞定,不过为了更好的了解学习小知识点进行一个拆分是很有意义的。通过这个案例,编写组件的思路更加清晰,一步一步循序渐进,感觉还不错。
学到了很多细碎的知识点。
1.目前还没有学到事件总线,所以同级组件之间的数据交互是通过最基础的方式:数据放在父组件,再将数据传递给子组件;同时学到了重要的一点:父组件的函数也可以通过props属性传递给子组件,从而通过操作子组件实现父组件数据的交互。
2.复习了很多es6的函数(可见我对es6很不熟悉..),关于数组操作的forEach()、filter()、reduce(),数组的增删函数:unshift()、splice()等
3.还了解了一个组件库nanoid,用于生成一个独一无二字符串id
4.v-model操作复选框实际操作的不是value而是checked
5.props属性传过来的值是只读的!!如果需要修改就在data另外保存,不能直接操作这个值!!否则会报错!如果操作的是对象数组则有可能vue监测不到改变,因为地址没变,但是仍然是不建议的,要通过子组件修改父组件的数据建议是通过父组件传给子组件的函数进行修改
6.本地存储webStorage
localStorage:网页关闭保存的数据不会消失,需要手动删除
sessionStorage:网页关闭则保存的数据消失
7.组件通信的方式选择
父子间的通信最好采用props或者自定义事件,不必采用全局事件总线大费周章
同级组件、爷孙组件的通信使用全局总线更加方便