文章目录
- 组件化编码流程
- 拆分静态组件
- 实现动态组件
- 实现交互
- 实现添加
- 实现勾选
- 实现删除
- 实现全选与全不选
- 实现清除已完成任务
- TodoList案例小细节
Vue学习目录
上一篇:(二十七)Vue组件的样式
先看一个需求:TodoList案例
功能:添加任务;任务项可以勾选,表示已完成任务;鼠标移动到任务项高亮并显示删除按键,点击删除按键可以进行删除单个任务项;左下角展示已完成和全部的任务项数,有一个勾选框,勾选可以让任务项全部勾选上,有一个清除已完成任务的按钮,点击可以清除勾选的任务项。
下面我们就来完成这个功能
组件化编码流程
- (1)拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
- (2)实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
- 1.一个组件在用:放在组件自身即可。
- 2.一些组件在用:放在他们共同的父组件上(状态提升)。
- (3).实现交互:从绑定事件开始。
拆分静态组件
一般拆分组件会按功能进行拆分
需求的拆分为:App、Top、List、Item、Footer组件
先实现静态页面
App组件:
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<Top/>
<List/>
<Footer/>
</div>
</div>
</div>
</template>
<script>
import Top from "@/components/Top";
import List from "@/components/List";
import Footer from "@/components/Footer";
export default {
name: "App",
components:{
Footer,
List,
Top,
}
}
</script>
<style>
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:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Top组件:
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
</template>
<script>
import {nanoid} from "nanoid";
export default {
name: "Top",
}
</script>
<style scoped>
.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);
}
/*main*/
.todo-main {
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>
List组件:
<template>
<ul class="todo-main">
<Item/>
<Item/>
<Item/>
</ul>
</template>
<script>
import Item from "@/components/Item";
export default {
name: "List",
components: {Item},
}
</script>
<style scoped>
</style>
Item组件:
<template>
<li>
<label>
<input type="checkbox"/>
<span>xxxxx</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</template>
<script>
export default {
name: "Item",
}
</script>
<style scoped>
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover{
background-color: cyan;
}
li:hover button{
display: block;
}
</style>
Footer组件:
<template>
<div class="todo-footer">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成</span> / 全部
</button>-->
</div>
</template>
<script>
export default {
name: "Footer",
}
</script>
<style scoped>
.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>
效果:
实现动态组件
我们需要先创建数据,由于所有组件都在使用,所以放在共同的父组件App身上
- id:任务项的唯一标识
- title:任务项的名字
- done:任务项是否完成
data(){
return{
todos:[
{id:'001',title:'吃饭',done:true},
{id:'002',title:'睡觉',done:false},
{id:'003',title:'玩游戏',done:true}
]
}
},
数据创建好之后需要传给List组件做展示,而List组件通过v-for对数据进行遍历,并把数据传到Item组件进行展示,Footer组件里展示已完成和全部的任务项也需要数据,所以也需要App传过去
App组件:
传递数据
<Top/>
<List :todos="todos"/>
<Footer :todos="todos"/>
List组件:
接收数据
props:['todos']
遍历数据并给Item传递数据
<Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
Item组件:
接收数据
props:['todo']
展示数据
<input type="checkbox" :checked="todo.done"/>
<span>{{todo.title}}</span>
<!-- <span>xxxxx</span>-->
Footer组件:
接收数据
props:['todos']
展示数据,因为全部任务项逻辑比较简单,所以使用插值表达式是可以的,但是对已完成的任务项展示,需要遍历数据读取done属性,所以需要写成计算属性
<span>已完成{{doneTodo}}</span> / 全部{{todos.length}}
doneTodo计算属性,这里有两种写法
- 1.创建计数i,对数据进行遍历、操作,最后返回计数
- 2.调用数组的reduce方法,这个方法两个参数
-
第一个是函数,这个函数会受到的参数
- 第一个total:必需。初始值, 或者计算结束后的返回值。
- 第二个currentValue:必需。数组的当前元素
- 第三个currentIndex:可选。当前元素的索引
- 第四个arr:可选。当前元素所属的数组对象。
-
第二个参数initialValue:可选。传递给函数的初始值
-
computed:{
doneTodo() {
/*let i=0
this.todos.forEach((todo)=>{
if (todo.done) i++
})
return i;*/
return this.todos.reduce((pre, todo) => {
return pre + (todo.done ? 1 : 0)
}, 0)
}
}
效果:
实现交互
实现添加
因为需要收取到用户传递的数据,可以给文本框双向绑定一个空数据title,也可以在绑定键盘事件,在事件回调通过event接收数据,当然我们是在使用Vue,所以尽量避免操作DOM
Top组件:
空数据
data(){
return{
title: ''
}
}
绑定数据和键盘事件
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
将用户的输入包装成一个todo对象 ,这里要生产id,可以使用uuid的变种,nanoid,安装命令 npm i nanoid
这个todo对象需要添加到App里的todos数组里,所以需要App进行配合,首先app要创建一个回调函数,这个函数里往todos数组添加Top里包装成的对象,如何把这个回调函数通过props传给Top进行调用即可
App组件:
声明回调
methods:{
//添加一个todo
addTodo(todoObj){
//console.log('App组件',todoObj)
this.todos.unshift(todoObj)
}
}
传递回调
<Top :addTodo="addTodo"/>
Top组件:
接收回调
props:['addTodo']
在键盘事件里调用回调
methods:{
add(){
if (!this.title.trim()) return alert("输入不能为空")
const todoObj = {id:nanoid(),title:this.title,done:false}
this.addTodo(todoObj)
this.title=''
}
}
效果:成功添加
实现勾选
我们当前勾选任务项时,是不会改变数据的done属性值的,并且下方已完成任务项也不会变
实现任务项的勾选可以绑定两种事件,一种是click点击事件,另一种是change改变事件
同样需要在App声明一个回调函数,给Item组件调用
由于我们还没有学全局事件总线、消息订阅与发布等等,我们只能把这个回调函数传给List组件,通过List组件再传给Item组件
App组件:
声明回调
//修改done值(勾选与取消勾选)
checkTodo(id){
this.todos.forEach((todo)=>{
if (todo.id === id) todo.done = !todo.done
})
}
传递回调
<List :todos="todos" :checkTodo="checkTodo"/>
List组件:
接收回调
props:['todos','checkTodo']
传递回调
<Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" :checkTodo="checkTodo"/>
Item组件:
接收回调
props:['todo','checkTodo']
定义事件
<!-<input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>-->
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
定义事件回调
methods: {
handleCheck(id) {
//通知App组件将对应的todo对象的done取反
this.checkTodo(id)
},
}
效果:
实现删除
于勾选相同
App组件:
声明回调
//删除一个todo
deleteTodo(id){
this.todos = this.todos.filter((todo)=>{
return todo.id !== id
})
},
传递回调
<List :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
List组件:
接收回调
props:['todos','checkTodo','deleteTodo']
传递回调
<Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
Item组件:
接收回调
props:['todo','checkTodo','deleteTodo']
定义事件
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
定义事件回调
handleDelete(id){
if (confirm('确定删除吗?')) {
//通知App组件将对应的todo对象
this.deleteTodo(id)
}
}
效果:
实现全选与全不选
可以借助已完成和全部做文章,这时需要把全部任务项显示那里改为计算属性形式
<span>已完成{{doneTodo}}</span> / 全部{{total}}
isAll(){
return this.doneTodo === this.total && this.total > 0
}
实现有两种方式:
- 一种是与前面一样
App组件:
声明回调
//全选or取消全选
checkAllTodo(done){
this.todos.forEach((todo)=>{
todo.done = done
})
}
传递回调
<Footer :todos="todos" :checkAllTodo="checkAllTodo"/>
Footer组件:
接收回调
props:['todos','checkAllTodo']
定义事件
<input type="checkbox" :checked="isAll" @change="checkAll"/>
定义事件回调
methods: {
checkAll(e){
//通知App组件全选or取消全选操作
this.checkAllTodo(e.target.checked)
}
}
- 另一种是把给勾选框双向绑定计算属性isAll,这时要求isAll只能完整写法
<input type="checkbox" v-model="isAll"/>
isAll完整写法:当然也要借助App里的回调
isAll:{
get(){
return this.doneTodo === this.total && this.total > 0
},
set(value){
this.checkAllTodo(value)
}
}
效果:
实现清除已完成任务
与前面一样,借助App的回调
App组件:
声明回调
//清除所有以完成的todo
clearAllTodo(){
this.todos = this.todos.filter((todo) =>{
return !todo.done
})
}
传递回调
<Footer :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
Footer组件:
接收回调
props: ['todos', 'checkAllTodo','clearAllTodo']
绑定事件
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
定义事件回调
clearAll(){
this.clearAllTodo()
}
效果:
TodoList案例小细节
-
props适用于:
(1).父组件 ==> 子组件 通信
(2).子组件 ==> 父组件 通信(要求父先给子一个函数)
-
使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
在Item勾选事件我们可以进行v-model绑定
<input type="checkbox" v-model="todo.done"/>
虽然功能也能实现,当是因为官方规定props是不可以修改的,props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。
并且IDEA会进行报错,(“todo”属性的意外突变。)虽然这个报错不会影响程序运行