源码在文末。
前言
上篇文章写了个V利用Props
进行组件之间的通信,这不立马就安排上这个案例拉丫。光学不敲等于没学哈(资深大佬除外哈)
目标就是实现如下的样子:
能够进行增删改查,并且是在各个组件之间。
一、环境准备
针对这个页面,我们将他们划分为下面四个组件哈。其实也不是固定的,只是为了更好的展示组件之间的通信。
项目结构:
准备静态页面
MyTodoHeader
头部组件:
<template>
<div class="todo-header-box">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title"/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
export default {
props: {
},
data () {
return {
title: ''
}
},
methods: {
}
}
</script>
<style scoped>
.todo-header-box{
width: 500px;
height: 40px;
margin-top:10px;
}
.todo-header-box input{
width: 460px;
height: 40px;
margin-left: 10px;
border: 1px solid #43B984;
border-radius: 8px;
padding-left: 10px;
}
:focus-visible{
outline:none;
}
</style>
MyTodoList
组件,另外组件内还包含着MyTodoItem
组件
<template>
<ul class="todo-main">
<TodoItem v-for="(todo, index) in todos" :key="index" :todo="todo"/>
</ul>
</template>
<script>
import TodoItem from './MyTodoItem'
export default {
components: {
TodoItem
},
props: {
},
methods: {
}
}
</script>
<style scoped>
/*main*/
.todo-main {
margin-top: 10px;
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;
}
/*item*/
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>
MyTodoItem
组件
<template>
<li :style="{background: bgColor}">
<label>
<input type="checkbox" :checked="todo.done" />
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display:none" v-show="isShow">删除</button>
</li>
</template>
<script>
export default {
props: {
todo: Object
},
data () {
return {
bgColor: 'white',
isShow: false
}
},
methods: {
}
}
</script>
MyTodoFooter
组件
<template>
<div class="todo-footer" v-show="total">
<label>
<!-- // 第一种方式:通过dom元素来判断有没有进行勾选 不是最佳方式 -->
<!-- <input type="checkbox" :checked="isAllCheck" @click="checkAll" /> -->
<!-- 第二种方式: 通过绑定计算属性来进行展示 -->
<input type="checkbox" v-model="isAllCheck" />
</label>
<span
>已完成{{ doneTotal }}<span> / 全部{{ todos.length }} </span>
</span>
<button
class="btn btn-danger"
>
清除已完成任务
</button>
</div>
</template>
<script>
export default {
props: {
todos: Array,
clearDoneTodos: Function,
checkAllTodos: Function
},
computed: {
total () {
return this.todos.length
},
doneTotal () {
return this.todos.reduce((preTotal, todo) => preTotal + (todo.done ? 1 : 0), 0)
},
isAllCheck: {
get () {
return this.doneTotal === this.todos.length && this.doneTotal > 0
}
}
},
methods: {
}
}
</script>
/*footer*/
<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
组件
<template>
<div class="todo-container">
<!-- header模块 -->
<TodoHeader/>
<!-- main 模块 -->
<TodoList :todos="todos"/>
<!-- 主要的内容模块 -->
<TodoFooter :todos="todos"/>
<!-- footer模块 -->
</div>
</template>
<script>
import TodoHeader from './components/MyTodoHeader'
import TodoList from './components/MyTodoList'
import TodoFooter from './components/MyTodoFooter'
export default {
components: {
TodoHeader,
TodoList,
TodoFooter
},
data () {
return {
todos: [
{ id: '001', title: '吃饭', done: true },
{ id: '002', title: '睡觉', done: false },
{ id: '003', title: '敲代码', done: true }
]
}
},
methods: {
}
}
</script>
<style>
* {
margin: 0 0;
padding: 0 0;
}
.todo-container {
margin: 0 auto;
margin-top: 10px;
width: 500px;
height: 500px;
background-color: #ccc;
border: 1px solid #ddd;
border-radius: 8px;
}
</style>
二、在头部组件中实现增加方法
首先说说我们的需求:
就是在头部组件中的输入框中进行输入,然后按下回车键就将数据增加到todos
数组中,并在下面的列表中展示出来。
思路大致如下:
- 首先我们要明确数据我们是存储在
App
组件中的,那么我们真实修改的方法也应该写在App
组件中。由App
组件将方法传递给子组件(MyTodoHead
)组件。 - 在子组件中通过
Props
进行接收 - 最后再在子组件的
input
中定义一个回车事件,触发父组件中的增加方法,进行数据的更新。
App组件中修改:
<template>
<div class="todo-container">
<!-- header模块
:addTodo 定义的是子组件接收的名称
"addTodo" 指向的是此组件中所定义的方法
-->
<TodoHeader :addTodo="addTodo" />
<!-- main 模块 -->
<TodoList :todos="todos" />
<!-- 主要的内容模块 -->
<TodoFooter :todos="todos"/>
<!-- footer模块 -->
</div>
</template>
<script>
import TodoHeader from './components/MyTodoHeader'
import TodoList from './components/MyTodoList'
import TodoFooter from './components/MyTodoFooter'
export default {
components: {
TodoHeader,
TodoList,
TodoFooter
},
data () {
return {
message: 'hello',
todos: [
{ id: '001', title: '吃饭', done: true },
{ id: '002', title: '睡觉', done: false },
{ id: '003', title: '敲代码', done: true }
]
}
},
methods: {
// 回车增加一个todo
addTodo (todo) {
this.todos.unshift(todo)
}
}
}
</script>
我们通过:addTodo="addTodo"
传递给子组件一个方法,然后在子组件中我们用props
来接收。
<template>
<div class="todo-header-box">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
export default {
props: {
// 通过props来接收
addTodo: Function
},
data () {
return {
title: ''
}
},
methods: {
add () {
// 1. 检查输入合法性
const title = this.title.trim()
if (!title) {
alert('请输入内容')
return
}
const id = uuidv4()
// 2. 根据输入生成一个todo对象
const todo = { id, title, done: false }
// 3. 这里的this.addTodo 调用的实际上就是执行父组件中的addTodo函数
//添加到todos
this.addTodo(todo)
// 4. 清除输入
this.title = ''
}
}
}
</script>
这里我使用到了uuid
生成全局唯一id。
安装方式:
npm install uuid --save # 引用的话直接 import { v4 as uuidv4 } from 'uuid'; # 用法: 直接调用这个函数即可 uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
三、完善 MyTodoList 组件 | 根据id删除一条todo&判断是否选中
先说说需求:
- 鼠标经过每个todo上显示删除按钮,可以点击删除
- 判断是否勾选,即判断是否已完成
其实还有第三个的哈,我没写了(懒了),第三个是编辑,大家可以试一试.
如下图:
思路其实蛮简单的哈:
- 点击删除,只要传个id即可,虽然是祖孙组件之间传值,但其实就是将方法传了两层,借助了List组件做个中介,接收完再传递给item组件而已
- 判断是否已完成也一样,都是借助了list组件传递,实现祖孙组件通信.
MyTodoList
组件
<template>
<ul class="todo-main">
<TodoItem v-for="(todo, index) in todos" :key="index" :todo="todo" :deleteTodo="deleteTodo" :checkTodo="checkTodo" />
</ul>
</template>
<script>
import TodoItem from './MyTodoItem'
export default {
components: {
TodoItem
},
props: {
todos: Array,
deleteTodo: Function,
checkTodo: Function
},
methods: {
}
}
</script>
MyTodoItem
组件
<template>
<li @mouseenter="handleEnter(true)" @mouseleave="handleEnter(false)" :style="{background: bgColor}">
<label>
<input type="checkbox" :checked="todo.done" @click="handlerCheck(todo.id)" />
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display:none" v-show="isShow" @click="handlerDeleteItem(todo.id)">删除</button>
</li>
</template>
<script>
export default {
props: {
todo: Object,
checkTodo: Function,
deleteTodo: Function
},
data () {
return {
bgColor: 'white',
isShow: false
}
},
methods: {
handleEnter (isEnter) {
if (isEnter) {
this.bgColor = '#aaa'
this.isShow = true
} else {
this.bgColor = 'white'
this.isShow = false
}
},
// 修改勾选状态
handlerCheck (id) {
console.log(id)
this.checkTodo(id)
},
// 根据id删除
handlerDeleteItem (id) {
if (window.confirm('确定删除吗')) {
this.deleteTodo(id)
}
}
}
}
</script>
App组件见下文哈
四、完善尾部组件 | 判断是否全部勾选及清除全部已完成任务
照常先谈谈我们的需求:
1、判断是否全部勾选,修改数据状态。
2、清除选中的任务
3、当没有任何数据时,底部栏不展示
先讲讲第一个的思路:判断有没有全选,其实就是判断todos数组的长度是否等于已经选中的数量(另外就是注意就是数组长度必须要大于零)
第二个:清除选中的任务,其实就是根据id
删除掉 App父组件中 todos
中我们选中的数据。
第三个:使用v-show指令即可,直接用todos
数组的长度即可,当数组长度为0时,v-show自然为”false“,反之为true
理清楚,直接看代码哈
<template>
<div class="todo-footer" v-show="total">
<label>
<!-- // 第一种方式:通过dom元素来判断有没有进行勾选 不是最佳方式 -->
<!-- <input type="checkbox" :checked="isAllCheck" @click="checkAll" /> -->
<!-- 第二种方式: 通过绑定计算属性来进行展示 -->
<input type="checkbox" v-model="isAllCheck" />
</label>
<span
>已完成{{ doneTotal }}<span> / 全部{{ todos.length }} </span>
</span>
<button
class="btn btn-danger"
@click="deleteDoneAll"
>
清除已完成任务
</button>
</div>
</template>
<script>
export default {
props: {
todos: Array,
clearDoneTodos: Function,
checkAllTodos: Function
},
computed: {
total () {
return this.todos.length
},
doneTotal () {
return this.todos.reduce((preTotal, todo) => preTotal + (todo.done ? 1 : 0), 0)
},
isAllCheck: {
get () {
return this.doneTotal === this.todos.length && this.doneTotal > 0
},
// 通过计算属性来判断是否全选或全不选
set (checked) {
this.checkAllTodos(checked)
}
}
},
methods: {
deleteDoneAll () {
this.clearDoneTodos()
}
// 通过dom元素来判断有没有进行勾选 不是最佳方式
// checkAll (e) {
// console.log(e.target.checked)
// this.checkAllTodos(e.target.checked)
// }
}
}
</script>
为什么不选择通过dom元素来判断有没有进行勾选呢?
Vue框架中并不建议我们直接操作Dom元素,更多的是希望我们通过vue框架自带的方式来实现.
App组件:
<template>
<div class="todo-container">
<!-- :message 对应的是子组件 prop 中接收变量的名称
"message" 对应的父组件中data中定义的数据
-->
<!-- header模块 -->
<TodoHeader :addTodo="addTodo" />
<!-- main 模块 -->
<TodoList :todos="todos" :deleteTodo="deleteTodo" :checkTodo="checkTodo" />
<!-- 主要的内容模块 -->
<TodoFooter
:todos="todos"
:clearDoneTodos="clearDoneTodos"
:checkAllTodos="checkAllTodos"
/>
<!-- footer模块 -->
</div>
</template>
<script>
import TodoHeader from './components/MyTodoHeader'
import TodoList from './components/MyTodoList'
import TodoFooter from './components/MyTodoFooter'
export default {
components: {
TodoHeader,
TodoList,
TodoFooter
},
data () {
return {
message: 'hello',
todos: [
{ id: '001', title: '吃饭', done: true },
{ id: '002', title: '睡觉', done: false },
{ id: '003', title: '敲代码', done: true }
]
}
},
methods: {
// 回车增加一个todo
addTodo (todo) {
this.todos.unshift(todo)
},
// 判断勾选不勾选
checkTodo (id) {
this.todos.forEach((todo) => {
if (todo.id === id) todo.done = !todo.done
})
},
// 删除一个todo
deleteTodo (id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
// 全选全不选
checkAllTodos (done) {
this.todos.forEach((todo) => { todo.done = done })
},
// 清除所有已完成的任务
clearDoneTodos () {
this.todos = this.todos.filter(todo => !todo.done)
}
}
}
</script>
五 小结
vue中组件通信的方式其实有很多种,就像我已经学过的就有props
| emit
| 全局事件主线
| 发布订阅模式
之后还有Vuex
,另外我们还可以自己定制.还有蛮多我没学到的 捂脸
其实本质都是去做数据的共享,大都数情况都是根据实际情况来选择的,并非那一样就是最好的。
六 源码
后语
大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。
纸上得来终觉浅,绝知此事要躬行。
大家好,我是博主
宁在春
:[主页]https://segmentfault.com/u/wy...)一名喜欢文艺却踏上编程这条道路的小青年。
希望:
我们,待别日相见时,都已有所成
。L
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。