目录
1 拿到一个功能模块首先需要拆分组件:
2 使用组件实现静态页面的效果
3 分析数据保存在哪个组件
4 实现添加数据
5 实现复选框勾选
6 实现数据的删除
7 实现底部组件中数据的统计
8 实现勾选全部的小复选框来实现大复选框的勾选
9 实现勾选大复选框来实现所有的小复选框都被勾选
10 清空所有数据
11 实现案例中的数据存入本地存储
12 案例中使用自定义事件完成组件间的数据通信
13 案例中实现数据的编辑
14 实现数据进出的动画效果
【分析】组件化编码的流程
1. 实现静态组件:抽取组件,使用组件实现静态页面效果
2.展示动态数据:
2.1 数据的类型、名称是什么?
2.2 数据保存在哪个组件?
3.交互---从绑定事件监听开始
1 拿到一个功能模块首先需要拆分组件:
2 使用组件实现静态页面的效果
【main.js】
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
el: '#app',
render: h => h(App)
})
【MyHeader】
<template>
<div class="todo-header">
<input type="text"/>
</div>
</template>
<script>
export default {
name: 'MyHeader',
}
</script>
<style scoped>
/*header*/
.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>
【Item】
<template>
<li>
<label>
<input type="checkbox"/>
<span v-for="todo in todos" :key="todo.id">{{todo.title}}</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</template>
<script>
export default {
name: 'Item',
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '追剧', done: true},
]
}
},
}
</script>
<style scoped>
/*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;
}
li:hover {
background-color: rgb(196, 195, 195);
}
li:hover button{
display: block;
}
</style>
【List】
<template>
<ul class="todo-main">
<Item></Item>
<Item></Item>
</ul>
</template>
<script>
import Item from './Item.vue'
export default {
name: 'List',
components:{
Item
},
}
</script>
<style scoped>
/*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>
【MyFooter】
<template>
<div class="todo-footer">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成 0</span> / 3
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
}
</script>
<style scoped>
/*footer*/
.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 id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader></MyHeader>
<List></List>
<MyFooter></MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue'
import List from './components/List.vue'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{
MyHeader,
List,
MyFooter
}
}
</script>
<style>
/*base*/
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>
通过以上代码就可以实现静态页面的效果了!!!
3 分析数据保存在哪个组件
在上述代码中数据是保存在Item组件中的,但是如果想要在后续实现一系列交互效果:在MyHeader组件中需要添加数据,而MyHeader组件和Item组件没有直接的关系, 就当前学习阶段的知识而言,并不能实现这两个组件之间的通信(后续会有解决方案),同理MyFooter也一样。
【分析】因为App组件是所有组件的父组件,所以数据放在App组件中,再使用props配置,所有的子组件就都可以访问到。
【App】(同时需要将数据传递到Item组件中,在当前阶段只能通过props配置一层一层往下传,所以是 App-->List,List-->Item)
1. 实现 App-->List 传递todos数据
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader></MyHeader>
<List :todos="todos"></List>
<MyFooter></MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue'
import List from './components/List.vue'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{
MyHeader,
List,
MyFooter
},
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '追剧', done: true},
]
}
},
}
</script>
<style>
/*base*/
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. List接受todos数据
<template>
<ul class="todo-main">
<Item v-for="todo in todos"
:key="todo.id"></Item>
</ul>
</template>
<script>
import Item from './Item.vue'
export default {
name: 'List',
components:{
Item
},
props: ['todos']
}
</script>
<style scoped>
/*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>
通过上述的代码便可以根据数据的数量来渲染出几个Item了,但是此时Item里面是没有内容的,所以需要 List-->Item 再次传递每条数据
3. 实现 List-->Item 传递todo数据
<template>
<ul class="todo-main">
<Item v-for="todo in todos"
:key="todo.id" :todo="todo"></Item>
</ul>
</template>
<script>
import Item from './Item.vue'
export default {
name: 'List',
components:{
Item
},
props: ['todos']
}
</script>
<style scoped>
/*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>
4. Item接受todo数据
<template>
<li>
<label>
<input type="checkbox"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</template>
<script>
export default {
name: 'Item',
// 声明接收todo对象
props:['todo'],
}
</script>
<style scoped>
/*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;
}
li:hover {
background-color: rgb(196, 195, 195);
}
li:hover button{
display: block;
}
</style>
4 实现添加数据
【App】定义接收数据的回调函数
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"></MyHeader>
<List :todos="todos"></List>
<MyFooter></MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue'
import List from './components/List.vue'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{
MyHeader,
List,
MyFooter
},
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '追剧', done: true},
]
}
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj)
}
}
}
</script>
<style>
/*base*/
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】实现添加数据的方法
<template>
<div class="todo-header">
<!-- 绑定键盘回车事件 -->
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add" v-model="title"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid' // 生成id
export default {
name: 'MyHeader',
data() {
return {
title: ''
}
},
props: ['addTodo'], // 接收父组件传过来的addTodo函数
methods: {
add(e) {
// 校验数据
if (!this.title.trim()) return alert('输入不能为空')
// 将用户的输入包装成为一个todo对象
const todoObj = {
id: nanoid(),
/* title: e.target.value, */
title: this.title,
done: false
}
console.log(todoObj)
// console.log(e.target.value)
// console.log(this.title)
// 通知App组件去添加一个todo对象
this.addTodo(todoObj)
// 清空输入
this.title = ''
}
}
}
</script>
<style scoped>
/*header*/
.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>
5 实现复选框勾选
【App】也是要逐层传递
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"></MyHeader>
<List :todos="todos" :changeTodo="changeTodo"></List>
<MyFooter></MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue'
import List from './components/List.vue'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{
MyHeader,
List,
MyFooter
},
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '追剧', done: true},
]
}
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj)
},
// 勾选或者取消勾选一个todo
changeTodo(id) {
this.todos.forEach((todo) => {
if (todo.id === id) todo.done = !todo.done
})
},
}
}
</script>
<style>
/*base*/
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>
【List】
<template>
<ul class="todo-main">
<Item v-for="todo in todos"
:key="todo.id" :todo="todo"
:changeTodo="changeTodo">
</Item>
</ul>
</template>
<script>
import Item from './Item.vue'
export default {
name: 'List',
components:{
Item
},
props: ['todos', 'changeTodo']
}
</script>
<style scoped>
/*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>
【Item】
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</template>
<script>
export default {
name: 'Item',
// 声明接收todo对象
props:['todo', 'changeTodo'],
methods: {
// 勾选 or 取消勾选
handleCheck(id) {
// 通知 App组件将对应的todo对象的状态改变
this.changeTodo(id)
}
}
}
</script>
<style scoped>
/*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;
}
li:hover {
background-color: rgb(196, 195, 195);
}
li:hover button{
display: block;
}
</style>
6 实现数据的删除
【App】
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"></MyHeader>
<List
:todos="todos"
:changeTodo="changeTodo"
:deleteTodo="deleteTodo">
</List>
<MyFooter></MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue'
import List from './components/List.vue'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{
MyHeader,
List,
MyFooter
},
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '追剧', done: true},
]
}
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj)
},
// 勾选或者取消勾选一个todo
changeTodo(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)
},
}
}
</script>
<style>
/*base*/
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>
【List】
<template>
<ul class="todo-main">
<Item v-for="todo in todos"
:key="todo.id"
:todo="todo"
:changeTodo="changeTodo"
:deleteTodo="deleteTodo">
</Item>
</ul>
</template>
<script>
import Item from './Item.vue'
export default {
name: 'List',
components:{
Item
},
props: ['todos', 'changeTodo', 'deleteTodo']
}
</script>
<style scoped>
/*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>
【Item】
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: 'Item',
// 声明接收todo对象
props:['todo', 'changeTodo', 'deleteTodo'],
methods: {
// 勾选 or 取消勾选
handleCheck(id) {
// 通知 App组件将对应的todo对象的状态改变
this.changeTodo(id)
},
// 删除操作
handleDelete(id) {
// console.log(id)
if (confirm("确定删除吗?")) {
// 通知App删除
this.deleteTodo(id)
}
}
}
}
</script>
<style scoped>
/*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;
}
li:hover {
background-color: rgb(196, 195, 195);
}
li:hover button{
display: block;
}
</style>
7 实现底部组件中数据的统计
【分析】如果想要统计数据的数量,就需要将数据传递到MyFooter组件中
【App】
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"></MyHeader>
<List
:todos="todos"
:changeTodo="changeTodo"
:deleteTodo="deleteTodo">
</List>
<MyFooter
:todos="todos"
>
</MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue'
import List from './components/List.vue'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{
MyHeader,
List,
MyFooter
},
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '追剧', done: true},
]
}
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj)
},
// 勾选或者取消勾选一个todo
changeTodo(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)
},
}
}
</script>
<style>
/*base*/
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>
【MyFooter】
在这个组件中可以使用计算属性实现数据的总长度和被勾选的数据的计算
<template>
<div class="todo-footer" v-if="todosLength">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成 {{doneTotal}}</span> / {{todosLength}}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
props: ['todos'],
computed: {
todosLength() {
return this.todos.length
},
doneTotal() {
return this.todos.filter(todo => todo.done).length
// 也可以使用下面求和来实现
// return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
}
}
}
</script>
<style scoped>
/*footer*/
.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>
8 实现勾选全部的小复选框来实现大复选框的勾选
:checked="isAll"
isAll也是通过计算属性计算得来的
【MyFooter】
<template>
<div class="todo-footer" v-if="todosLength">
<label>
<input type="checkbox" :checked="isAll"/>
</label>
<span>
<span>已完成 {{doneTotal}}</span> / {{todosLength}}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
props: ['todos'],
computed: {
todosLength() {
return this.todos.length
},
doneTotal() {
return this.todos.filter(todo => todo.done).length
// 也可以使用下面求和来实现
// return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll() {
return this.doneTotal === this.todosLength && this.todosLength > 0
},
}
}
</script>
<style scoped>
/*footer*/
.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>
9 实现勾选大复选框来实现所有的小复选框都被勾选
【App】
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"></MyHeader>
<List
:todos="todos"
:changeTodo="changeTodo"
:deleteTodo="deleteTodo">
</List>
<MyFooter
:todos="todos"
:checkAllTodo="checkAllTodo"
>
</MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue'
import List from './components/List.vue'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{
MyHeader,
List,
MyFooter
},
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '追剧', done: true},
]
}
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj)
},
// 勾选或者取消勾选一个todo
changeTodo(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)
},
// 全选or取消全选
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
}
}
</script>
<style>
/*base*/
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>
【MyFooter】
<template>
<div class="todo-footer" v-if="todosLength">
<label>
<input type="checkbox" :checked="isAll" @change="checkAll"/>
</label>
<span>
<span>已完成 {{doneTotal}}</span> / {{todosLength}}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
props: ['todos', 'checkAllTodo'],
computed: {
todosLength() {
return this.todos.length
},
doneTotal() {
return this.todos.filter(todo => todo.done).length
// 也可以使用下面求和来实现
// return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll() {
return this.doneTotal === this.todosLength && this.todosLength > 0
},
},
methods: {
checkAll(e) {
this.checkAllTodo(e.target.checked)
}
}
}
</script>
<style scoped>
/*footer*/
.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>
10 清空所有数据
【App】
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"></MyHeader>
<List
:todos="todos"
:changeTodo="changeTodo"
:deleteTodo="deleteTodo">
</List>
<MyFooter
:todos="todos"
:checkAllTodo="checkAllTodo"
:clearAllTodo="clearAllTodo"
>
</MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader.vue'
import List from './components/List.vue'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{
MyHeader,
List,
MyFooter
},
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '追剧', done: true},
]
}
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj)
},
// 勾选或者取消勾选一个todo
changeTodo(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)
},
// 全选or取消全选
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
// 清空所有已经完成的todo
clearAllTodo() {
this.todos = this.todos.filter((todo) => !todo.done)
}
}
}
</script>
<style>
/*base*/
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>
【MyFooter】
<template>
<div class="todo-footer" v-if="todosLength">
<label>
<input type="checkbox" :checked="isAll" @change="checkAll"/>
</label>
<span>
<span>已完成 {{doneTotal}}</span> / {{todosLength}}
</span>
<button class="btn btn-danger" @click="clearTodo">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
props: ['todos', 'checkAllTodo', 'clearAllTodo'],
computed: {
todosLength() {
return this.todos.length
},
doneTotal() {
return this.todos.filter(todo => todo.done).length
// 也可以使用下面求和来实现
// return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll() {
return this.doneTotal === this.todosLength && this.todosLength > 0
},
},
methods: {
checkAll(e) {
this.checkAllTodo(e.target.checked)
},
clearTodo() {
this.clearAllTodo()
}
}
}
</script>
<style scoped>
/*footer*/
.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>
11 实现案例中的数据存入本地存储
【分析】首先我们要知道什么时候需要将数据存入本地存储?所以这就用到了watch监听,当todos的值发生变化时,将新的值存入本地存储。
又因为当我们勾选复选框时,我们发现本地存储中的 done 值并没有发生变化?这主要是因为监听默认只会监听第一层,如果想要监听对象中某个数据发生变化时,就需要深度监视了。
【App】
这里使用 || 运算可以防止一开始本地存储中没有数据而报错
12 案例中使用自定义事件完成组件间的数据通信
这边以添加数据为例
【App】
给发送数据的组件绑定自定义事件
<MyHeader @addTodo="addTodo"></MyHeader>
...
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj)
},
}
【MyHeader】
13 案例中实现数据的编辑
需求分析:当点击编辑按钮时,变成input表单修改数据,此时编辑按钮隐藏,当失去焦点时,编辑完成,显示编辑后的数据,同时编辑按钮显示。
这边使用全局事件总线来实现通信
【App】
methods: {
...
// 更改
updateTodo(id,title) {
this.todos.forEach((todo) => {
if (todo.id === id) todo.title = title
})
},
...
},
mounted() {
this.$bus.$on('updateTodo', this.updateTodo)
},
beforeDestroy() {
this.$bus.$off('updateTodo')
}
【Item】
因为如果想要失去焦点时实现数据的修改,那么你必须提前获取焦点,但是由于Vue的执行机制,当Vue底层监视到数据发生改变时,它并不会立即去重新渲染模板,而是继续执行后面的代码,所以如果不加以处理的话,直接获取焦点,肯定会报错,因为页面中的元素还没有加载解析出,找不到获取焦点的input元素,所以可以通过以下的代码实现
this.$nextTick(function() { // 告诉Vue,DOM渲染完毕后,再执行focus()方法
this.$refs.inputTiltle.focus()
})
14 实现数据进出的动画效果
【Item】
使用<transtion></transtion>标签包裹