前言
最近学习了Vue,跟着教程完成了一个简单的demo—todoList清单,功能主要有添加、删除待办事件,统计已完成的事件数,一键清空列表等,运用了组件化的编码流程,巩固了父子组件传值的知识点
最终得到的效果如下
一、静态页面
根据功能拆分页面,项目的组织结构如下
App.vue文件
<template>
<div id="app">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader/>
<MyList/>
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
// 引入组件
import MyHeader from "@/components/MyHeader";
import MyList from "@/components/MyList";
import MyFooter from "@/components/MyFooter";
export default {
name: 'app',
components:{MyList, MyHeader,MyFooter} // 注册
}
</script>
<style lang="less">
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>
输入框部分拆分为头部组件
MyHeader.vue文件
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
</template>
<script>
export default {
data(){
return{
};
},
methods:{
}
};
</script>
<style lang="less" 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);
}
</style>
事件列表的每一行拆分为一个组件,因为每一行的结构相同,如果有多行,就可以在其父组件里面通过 v-for 指令遍历
MyItem.vue文件
<template>
<li>
<label>
<input type="checkbox"/>
<span>吃饭</span>
</label>
<button class="btn btn-danger" >删除</button><!--style="display:none"-->
</li>
</template>
<script>
export default {
name: "MyItem",
data(){
return{
}
}
}
</script>
<style lang="less" 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;
}
</style>
事件列表拆分为一个组件,作为MyItem.vue组件的父组件
MyList.vue文件
<template>
<ul class="todo-main">
<MyItem/>
<MyItem/>
</ul>
</template>
<script>
// 引入子组件
import MyItem from "@/components/MyItem";
export default {
name: "MyList",
components:{MyItem} // 注册子组件
}
</script>
<style scoped>
.todo-main{
margin-left: 0;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
底部统计与全选功能部分拆分为一个组件
MyFooter.vue文件
<template>
<div class="todo-footer">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成0</span> / 全部
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "MyFooter"
}
</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>
效果图如下
二、绑定动态数据
1、数据存储的方式
一堆要做的事情是一个数组,一个个要做的事情是对象。因此可以使用数组来存储多个要做的事情,每个事情是一个对象,包含属性id(标识)、title(名称)、done(是否完成)。
因为在MyList.vue组件中展示数据,所以该数组写在MyList文件中。
data(){
return{
todos:[
{id:'001',title:'吃饭',done:true},
{id:'002',title:'写代码',done:false},
{id:'003',title:'看电视',done:false}
]
}
}
2、数据展示方式
因为存储事情的数组todos在MyList中,而每一个事情todoObj在MyItem中展示,所以需要把每一个事情传递到MyItem文件中,相当于父组件给子组件传值,用到props。
MyList文件中
使用 v-for 遍历todos数组, :todo=“todoObj” 将每一个事情传到MyItem组件
<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
MyItem文件中
使用props接收父组件传递的数据
props:[‘todo’],接收父组件传递的每一个todoObj,注意是 todo
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done"/><!--checked动态绑定todo的done属性,判断事情是否完成(完成打钩) -->
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" >删除</button><!--style="display:none"-->
</li>
</template>
<script>
export default {
name: "MyItem",
props:['todo'],//接收父组件传递的每一个todoObj
data(){
return{
}
}
}
3、添加数据
每一个事件需要有一个唯一标识即id,这样才能通过标识来删除和选择该事件
id生成方法,使用 nanoid JavaScript库
node.js安装 nanoid 的方式
npm install nanoid
引入nanoid
import {nanoid} from 'nanoid'
使用方式
add(e){
const obj={id:nanoid(),title:e.target.value,done:false};
console.log(obj);
}
可以看到,生成的id是唯一的。
输入的待办事件需要从MyHeader组件传到MyList组件中,涉及兄弟组件传值。
兄弟组件传值的方法有三种
- 使用vuex,把数据放在store仓库中,这样任意组件都可以获取数据
- 使用bus总线传值
- 子组件1–>父组件–>子组件2
下面使用方法3,比较通俗易懂,实现起来也比较简单,但是如果有很多数据需要传递,则建议使用vuex
(1)修改为父子组件传值
这里将保存事件的数组放到App.vue文件中,转为父子组件传值方式,即组件MyHeader传输入的待办事件给父组件App,App再传给子组件MyList
App.vue文件修改为
<template>
<div id="app">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"/><!--传递方法 -->
<MyList :todos="todos"/>
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from "@/components/MyHeader";
import MyList from "@/components/MyList";
import MyFooter from "@/components/MyFooter";
export default {
name: 'app',
components:{MyList, MyHeader,MyFooter},
data(){
return{
todos:[
{id:'001',title:'吃饭',done:true},
{id:'002',title:'写代码',done:false},
{id:'003',title:'看电视',done:false}
]
};
},
methods:{
//添加事件对象
addTodo(todoObj){
this.todos.unshift(todoObj);//unshift添加到数组首位
}
}
}
</script>
MyList文件修改为
<template>
<ul class="todo-main">
<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
</ul>
</template>
<script>
import MyItem from "@/components/MyItem";
export default {
name: "MyList",
components:{MyItem},
props:['todos'],
data(){
return{
}
}
}
</script>
MyHeader文件修改为
<template>
<div class="todo-header">
<!--绑定键盘事件keyup.enter -->
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name:'MyHeader',
props:['addTodo'], // 接受父组件传递的方法
data(){
return{
};
},
methods:{
add(e){
// 校验输入的内容
if(e.target.value.trim() === ''){
alert('输入不能为空')
return;
}
// 将用户的输入包装成一个对象
const todoObj={id:nanoid(),title:e.target.value,done:false};
// 调用父组件传过来的函数,往数组中添加一个事件对象
this.addTodo(todoObj);
e.target.value = '';// 添加完后将输入框清空
}
}
};
</script>
4、勾选或取消勾选
涉及多层组件传值:App—>MyList —>MyItem
App文件中添加方法
// 勾选或取消勾选
checkTodo(id){
this.todos.forEach((todo)=>{
if(todo.id === id)
todo.done = !todo.done;
})
}
传给MyList
<MyList :todos="todos" :checkTodo="checkTodo"/>
MyList中用props接收后再传给MyItem,MyItem接收
<input type="checkbox" :checked="todo.done" @click="handleTodo(todo.id)"/>
methods:{
handleTodo(id){
this.checkTodo(id);//调用父组件传递的checkTodo方法
}
}
详细代码,后面附上。
5、删除事件
控制删除按钮显示与隐藏的css代码
li button{
float: right;
display: none;
margin-top: 3px;
}
li:hover button{
display: block;// 控制删除按钮显示(鼠标悬浮在该li上)与隐藏
}
类似于勾选或取消勾选,都是多层组件传值。
App文件中添加方法
// 删除一个事件
deleteTodo(id){
// filter方法会生成一个新数组,所以需要重新给todos赋值
this.todos = this.todos.filter((todo) => {
return todo.id !== id;// 等于该id的会被过滤掉
})
}
将deleteTodo方法先传给MyList文件,在从MyList文件传到MyItem文件。
MyItem文件中button绑定click事件
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
handleDelete(id){
this.deleteTodo(id);//调用父组件传递的方法
}
6、底部统计
分为两部分
(1)统计已完成的事件数和事件总数
使用计算属性computed完成
<span>已完成{{doneTotal}}</span> / 全部{{total}}
computed:{
doneTotal(){
// 方法1,使用计数器
/*let cnt = 0;
this.todos.forEach(todo =>{
if(todo.done)cnt++;
})
return cnt;*/
// 方法2,使用数组reduce方法
return this.todos.reduce((pre,todo)=>{
return pre + (todo.done ? 1:0);
},0)
},
total(){
return this.todos.length;
},
isAll(){
return this.total === this.doneTotal && this.total>0 //保证总数为0时不勾选
}
},
(2)勾选全部
父组件App中传递checkAllTodo方法给MyFooter子组件
<label>
<input type="checkbox" :checked="isAll" @click="checkAll"/>
</label>
// 全选或全不选
checkAllTodo(done){
this.todos.forEach(todo =>{
todo.done=done;
})
},
checkAll(e){
this.checkAllTodo(e.target.checked);//传入的是checked属性,不是value
},
(3)清除已完成的事件
父子组件传函数clearAllTodo
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
// 删除已完成的所有事件
clearAllTodo(){
this.todos = this.todos.filter(todo =>{
return !todo.done;// 过滤掉done值为true的事件,留下done为false的事件
})
}
clearAll(){
this.clearAllTodo();
}
当没有事件时,显示效果应为
使用v-show控制标签显示与隐藏
<div class="todo-footer" v-show="total">
三、总结
1、组件化编码流程
(1)拆分静态组件
拆分页面,抽取组件,组件要按功能点拆分,命名不要与HTML标签重名。
(2)实现动态组件
首先要考虑好数据的存放位置,数据是一个组件再用,还是多个组件同时用。
- 一个组件在用:放在该组件即可
- 多个组件共用:放在他们的共同父组件里(状态提升)
- 全局组件:如果某个组件不涉及路由切换,就可以注册为全局组件,然后在其他组件中不需要引入即可使用该全局组件,比如导航栏就可以注册为全局组件
然后判断数据该使用哪种数据类型来保存
- 数据是单一数据,只有一个值,可以使用字符串、数字、布尔型存储
- 数据有多个值,每个值没有其他属性,可以使用数组存储
- 数据有多个属性,但只有一个值,可以使用对象来存储
- 数据既有多个值,每个值又有多个属性,可以使用数组存储多个值,每个值为一个对象
(3)实现交互效果
给标签绑定事件函数,编写对应的业务逻辑代码
2、props使用范围
- 父组件 —> 子组件 通信
- 子组件 —> 父组件 通信(要求父组件先传给子组件一个函数)
3、其他注意点
- 使用v-model时,绑定的值不能是props传过来的的值,因为props是不可以被修改的;
- props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但是不推荐这样做。
四、全部代码
(1)App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"/>
<MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
<MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from "@/components/MyHeader";
import MyList from "@/components/MyList";
import MyFooter from "@/components/MyFooter";
export default {
name: 'app',
components:{MyList, MyHeader,MyFooter},
data(){
return{
todos:[
{id:'001',title:'吃饭',done:true},
{id:'002',title:'写代码',done:false},
{id:'003',title:'看电视',done:false}
]
};
},
methods:{
// 添加一个事件对象
addTodo(todoObj){
this.todos.unshift(todoObj);
},
// 勾选或取消勾选
checkTodo(id){
this.todos.forEach((todo)=>{
if(todo.id === id)
todo.done = !todo.done;
})
},
// 删除一个事件
deleteTodo(id){
// filter方法会生成一个新数组,所以需要重新给todos赋值
this.todos = this.todos.filter((todo) => {
return todo.id !== id;// 等于该id的会被过滤掉
})
},
// 全选或全不选
checkAllTodo(done){
this.todos.forEach(todo =>{
todo.done=done;
})
},
// 删除已完成的所有事件
clearAllTodo(){
this.todos = this.todos.filter(todo =>{
return !todo.done;// 过滤掉done值为true的事件,留下done为false的事件
})
}
}
}
</script>
<style lang="less">
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>
(2)MyHeader.vue
<template>
<div class="todo-header">
<!--绑定键盘事件keyup.enter -->
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name:'MyHeader',
props:['addTodo'],
methods:{
add(e){
// 校验输入的内容
if(e.target.value.trim() === ''){
alert('输入不能为空')
return;
}
// 将用户的输入包装成一个对象
const todoObj={id:nanoid(),title:e.target.value,done:false};
// 调用父组件传过来的函数,往数组中添加一个事件对象
this.addTodo(todoObj);
e.target.value = '';// 添加完后将输入框清空
}
}
};
</script>
<style lang="less" 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);
}
</style>
(3)MyList.vue
<template>
<ul class="todo-main">
<MyItem v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"/>
</ul>
</template>
<script>
import MyItem from "@/components/MyItem";
export default {
name: "MyList",
components:{MyItem},
props:['todos','checkTodo','deleteTodo'],
}
</script>
<style scoped>
.todo-main{
margin-left: 0;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
(4)MyItem.vue
<template>
<li>
<label>
<!--checked动态绑定todo的done属性,判断事情是否完成(完成打钩) -->
<input type="checkbox" :checked="todo.done" @click="handleTodo(todo.id)"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: "MyItem",
props:['todo','checkTodo','deleteTodo'],
methods:{
handleTodo(id){
this.checkTodo(id);
},
handleDelete(id){
this.deleteTodo(id);
}
}
}
</script>
<style lang="less" 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: #dddddd;
}
li:hover button{
display: block;// 控制删除按钮显示(鼠标悬浮在该li上)与隐藏
}
</style>
(5)MyFooter.vue
<template>
<div class="todo-footer" v-show="total">
<label>
<input type="checkbox" :checked="isAll" @click="checkAll"/>
</label>
<span>
<span>已完成{{doneTotal}}</span> / 全部{{total}}
</span>
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "MyFooter",
props:['todos','checkAllTodo','clearAllTodo'],
computed:{
doneTotal(){
// 方法1,使用计数器
/*let cnt = 0;
this.todos.forEach(todo =>{
if(todo.done)cnt++;
})
return cnt;*/
// 方法2,使用数组reduce方法
return this.todos.reduce((pre,todo)=>{
return pre + (todo.done ? 1:0);
},0)
},
total(){
return this.todos.length;
},
isAll(){
return this.total === this.doneTotal && this.total>0
}
},
methods:{
checkAll(e){
this.checkAllTodo(e.target.checked);
},
clearAll(){
this.clearAllTodo();
}
}
}
</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>
(6)效果展示
勾选删除单个事件
一键全选事件
没有事件时的效果