目录
一、TodoList案例介绍:
TodoList案例有点类似于我们平时使用的“备忘录”的功能。在这次案例中主要是为了体验“组件化编程的流程”,组件化编程的流程有以下三个:
-
拆分静态组件;组件要按照功能点拆分,命名不要与html元素冲突
-
实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用;
-
实现交互:从绑定事件开始
其中实现组件化又分为了两种:
-
一个组件在用:放在组件自身即可.
-
一些组件在用:放在他们共同的父组件上(状态提升)
项目拆分组件及功能如图所示:
二、案例全部完整代码:
下面是项目实现的完整代码,可以参考学习
2.1 Mylist.vue
Mylist.vue是列表显示组件,它有一个子组件item,也是每个“要做的事儿~”
<template>
<ul class="todo-main">
<Item v-for="todoobj in todos"
:key="todoobj.id"
:todo="todoobj"
:checkTodo='checkTodo'
:deleteTodo='deleteTodo'/>
</ul>
</template>
<script>
import Item from "./Myitem.vue";
export default {
name:"List",
components:{Item},
props:[
'todos',
'checkTodo',
'deleteTodo'
]
}
</script>
<style scoped>
.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>
2.2 Myitem.vue
Myitem组件是每一个具体要做的事情`
<template>
<li>
<label>
<!-- checked = true表示勾选 :checked="todo.done动态绑定是否勾选-->
<!-- 可以綁定@change事件或者是@click事件 -->
<input type="checkbox" :checked="todo.done" @change="handlecheck(todo.id)" />
<!-- 以下代碼也可以完成功能,但是不建議使用,只是转了v-model空子 -->
<!-- <input type="checkbox" v-model="todo.done"/> -->
<!-- 父传子,拿到title数据 -->
<span>{{ todo.title }}</span>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: "Item",
data() {
return {
}
},
// 声明接收todo对象(父传子)
props: ["todo", "checkTodo","deleteTodo"],
methods: {
handlecheck(id) {
console.log(id)
this.checkTodo(id);
},
handleDelete(id) {
if (confirm("确定删除吗?")) {
this.deleteTodo(id);
}
}
},
}
</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: #ddd;
}
li:hover button {
display: block;
}
</style>
2.3 MyHeader.vue
MyHeader组件是头部添加部分
<template>
<div class="todo-header">
<input type="text" v-model="title" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from "nanoid"
export default {
name:"MyHeader",
data() {
return {
title:'',
}
},methods: {
add(){
// 校验输入的数据不能为空
if(!this.title.trim()) return alert("输入不能为空")
// event.target.value获取发生事件的元素的值,这里使用的是双向数据绑定,两种都可以
// 将用户的输出包装成todo对象 uuid用于生成全球唯一字符编码id nanoid是在uuid的基础上做的精简
const todoobj ={id:nanoid(),title:this.title,done:false}
this.addTodo(todoobj)
// 清空
this.title=''
}
},props:[
'addTodo'
]
}
</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);
}
</style>
2.4 MyFooter.vue
MyFooter是底部“完成件儿数和事件的总数以及清空已完成部分”这一部分的内容
<template>
<div class="todo-footer" v-show="total">
<label>
<!-- <input type="checkbox" checked="isAll" @change="checkAll" /> -->
<input type="checkbox" v-model="isAll" />
</label>
<span>
<span>已完成{{ doneTotal }}</span> / 全部{{ total }}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "Footer",
data() {
return {
}
},
props: [
'todos','checkAlltodo'
],
methods: {
checkAll(e) {
this.checkAlltodo(e.target.checked)
}
},
computed: {
total() {
return this.todos.length
},
doneTotal() {
// reduce做条件查询的 第一个参数是函数(调用的次数是数组的长度),第二个参数是初始符合条件的个数
// pre第一次调用是0,第二次pre是第一次pre的返回值
// current是当前todo对象
return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll:{
get(){
return this.doneTotal === this.total && this.total > 0
},
set(value){
this.checkAlltodo(value)
}
}
}
}
</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>
2.5 App.vue
App组件是前面所有组件的父组件以及“爷爷”组件。
<template>
<div id="app">
<div class="todo-container">
<div class="todo-wrap">
<Header :addTodo="addTodo" />
<List :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" />
<Footer :todos="todos" :checkAlltodo="checkAlltodo"/>
</div>
</div>
</div>
</template>
<script>
import Header from './components/MyHeader.vue';
import List from "./components/Mylist.vue";
import Footer from "./components/MyFooter.vue";
export default {
name: 'App',
components: {
Header,
List,
Footer
},
data() {
return {
todos: [
// id:唯一标识
// title:需要做的事
//done:是否完成 true完成 FALSE失败
{
id: '001',
title: '吃饭',
done: true
}, {
id: '002',
title: '喝酒',
done: false
}, {
id: '003',
title: '吃饭',
done: true
}
]
}
},
methods: {
// 添加
addTodo(x) {
console.log("我收到了数据", x);
// 对数组进行修改
this.todos.unshift(x)
},
// 勾選or或取消勾選
checkTodo(id) {
this.todos.forEach((todo) => {
if (todo.id == id) todo.done = !todo.done;
console.log(todo.done)
})
},
// 删除
deleteTodo(id) {
this.todos= this.todos.filter((todo) => {
return todo.id !== id
})
},
// 全选、全不选
checkAlltodo(done){
this.todos.forEach((todo)=>{
todo.done=done
})
}
},
}
</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>
三、配置项props:
在剖析todolist案例前呢,我们一定要先熟练地使用props(父传子传值),因为在这次案例中使用较多。
功能:让组件接收外部传过来的数据
使用 :
(1)、父组件传递数据:
<Student name="李四" :age="11"></Student>
(2)、子组件使用props接收数据,接收的同时对数据类型进行限制+默认值的制定+必要性限制
props:
{
name: {
type: String,
required: true
},
age: {
type: Number,
default:99
}
}
备注:props是只读的。vue底层会检监测你对props的修改,如果修改了,就会发出警告,
若业务需求需要修改,那么复制props的内容到data中一份,然后去修改data中的数据
四:部分功能分析:
4.1 list列表显示
由于todos是有很多个组件进行操作,所以在定义的时候为了方便操作,我们会把它方法App.vue(公用父组件)中,由于列表的显示是在item组件中,在我们没有学会消息订阅与发布以及全局事件总线前,我们只能先将todos通过props先传递给mylist组件、然后使用v-for进行数据的遍历这样就可以得到list列表显示的效果了
<ul class="todo-main">
<Item v-for="todoobj in todos"
:key="todoobj.id"
/>
</ul>
</template>
<script>
import Item from "./Myitem.vue";
export default {
name:"List",
components:{Item},
props:[
'todos',
]
}
</script>
4.2 添加功能
由于添加功能的时候,所处的组件关系不是父子关系,所以添加功能相对于其他的功能会复杂一些,在我们没有学过全局事件总线以及消息的订阅与发布之前,我们可以使用组件自定义事件进行myheader组件对vue.app组件 的传值(子传父),而再将拿到的数据通过父传子传给mylist子组件即可。子组件向父组件传值怎么传递呢?方法如下:
子传父的方式:
方式一:通过父组件给子组件擦魂帝函数类型的props实现;子给父传递函数
方式二:通过父组件给子组件绑定一个自定义事件实现:子传父传递数据
方式三:通过ref ,灵活性更强(方式三是方式二的另一种实现方式)
假定有两个组件,app.vue为父组件,school,student为子组件
(1)、通过父组件给子组件擦魂帝函数类型的props实现;子给父传递函数
app组件: <School :getSchoolName="getSchoolName"></School>
getSchoolName(name) {
this.msg = name
}
School组件:<button @click="sendSchoolName">点我将学校名传给APP组件</button>
methods: {
sendSchoolName(){
this.getSchoolName(this.name)
}
},
props:[
'getSchoolName'
]
(2)、通过父组件给子组件绑定一个自定义事件实现:子传父传递数据
app组件 : <Student v-on:atguigu="getStudentName"></Student>
getStudentName(name) {
console.log("收到了"+name)
this.msg = name
}
Student组件:
<button @click="sendStudentName">点我将学生名传给APP</button>
methods: {
sendStudentName(){
// 触发Student组件实例身上的atguigu事件
this.$emit('atguigu',this.name)
}
},
(3)、通过ref ,灵活性更强(方式三是方式二的另一种实现方式)
APP组件:
<Student ref="student"></Student>
getStudentName(name) {
console.log("收到了"+name)
this.msg = name
},
mounted() {
setTimeout(() => {
// 绑定自定义事件
// this.$refs.student.$on('atguigu', this.getStudentName)
// 绑定自定义事件,只执行一次
this.$refs.student.$once('atguigu', this.getStudentName)
}, 5000)
},
Student组件:
methods: {
sendStudentName(){
// 触发Student组件实例身上的atguigu事件
this.$emit('atguigu',this.name)
}
},
按照这三种方式,也可以完成子组件向父组件间的传值功能,可以体会一下~后期学了“消息的订阅与发布”和“全局事件总线”后,会有更加便捷的方法~