Vue2 --- Todo-List 案例
0. 效果展示

1. 拆分静态组件
以功能为粒度抽取组件,使用组件实现静态页面效果
0. App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header/>
<List/>
<Footer/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
}
}
</script>
<style>
body{
background: #ffffff;
}
.btn{
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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,0.05);
border-radius: 4px;
}
.btn-danger{
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover{
color: #ffffff;
background-color: #bd362f;
}
.btn:focus{
outline: none;
}
.todo-container{
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp{
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
1. 添加任务
src/components/Header.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车确认">
</div>
</template>
<script>
export default {
name: "HeaderInfo"
}
</script>
<style scoped>
.todo-header{
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. 任务列表
src/components/List.vue
<template>
<div>
<ul class="todo-main">
<TodoItem/>
</ul>
</div>
</template>
<script>
import TodoItem from "@/components/TodoItem";
export default {
name: "ListInfo",
components: {
TodoItem
}
}
</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>
3. 具体某项任务
src/components/Item.vue
<template>
<li>
<label for="">
<input type="checkbox">
<span>xxxxxx</span>
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script>
export default {
name: "ItemInfo"
}
</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;
}
</style>
4. 任务页码展示
src/components/Footer.vue
<template>
<div class="todo-footer">
<label for="">
<input type="checkbox">
</label>
<span>
<span>已完成0</span> / 全部2
</span>
<button class="btn btn-danger">清除已完成的任务</button>
</div>
</template>
<script>
export default {
name: "FooterInfo"
}
</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. 动态数据展示
1. 数据的类型,名称是什么
数组中有多个对象的嵌套结构,名字为todos
2. 数据保存在哪个组件
第一种情况: 哪个组件用到了就存在哪里
src/components/List.vue
<template>
<div>
<ul class="todo-main">
<!-- 2. 遍历todos,并将每一个对象传到每一个子组件 -->
<TodoItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
</ul>
</div>
</template>
<script>
import TodoItem from "@/components/TodoItem";
export default {
name: "ListInfo",
components: {
TodoItem
},
data() {
return {
// 1. 定义事件列表
todos: [
{id: "1", title: "吃饭", done: false},
{id: "2", title: "睡觉", done: true},
{id: "3", title: "打豆豆", done: false},
]
}
}
}
</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>
src/components/Item.vue
<template>
<li>
<label for="">
<!-- 3.radio标签的勾选状态,使用动态属性checked,由父组件中todo对象的done字段决定是否勾选 -->
<input type="checkbox" :checked="todo.done">
<span>{{ todo.title}}</span> <!-- 2. 展示要做事的名称 -->
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script>
export default {
name: "ItemInfo",
props: ["todo"], // 1. 声明子组件接收的参数
}
</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;
}
</style>
3. 添加一个需要做的事情,默认为未完成
src/components/Header.vue
<template>
<div class="todo-header">
<!-- 1. 绑定回车键按下再抬起的事件,并将input中输入的值和title双向绑定 -->
<input type="text" placeholder="请输入你的任务名称,按回车确认" v-model="title" @keyup.enter="addTodo">
</div>
</template>
<script>
// 3.2 导入nanoid包,用来生成唯一随机字符串
import {nanoid} from 'nanoid'
export default {
name: "HeaderInfo",
data() {
return {
title: "" // 2. 定义input框中输入值的双向绑定
}
},
methods: {
// 3. 用户输入完成后点击回车,将数据保存到 todos 中
addTodo() {
console.log(this.title)
// 3.1 将用户的输入封装成一个todo对象,id可以用uuid的变种nanoid生成的字符串来表示,npm i nanoid 下载这个包
const todoObj = {id: nanoid(), title: this.title, done: false}
console.log(todoObj)
}
}
}
</script>
<style scoped>
.todo-header {
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>
写到这里遇到了一个问题,Header组件如何向同级的兄弟组件 List组件 传递参数呢?,以后会有事件总线之类的高级写法,这里还没学到,所以用第二种情况中的方法
第二种情况: 解决第一种情况的问题
所以todos就不能放在List组件中了,需要定义在App组件内,借助App组件,来对每个子组件传递参数
App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header/>
<List :todos="todos"/>
<Footer/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
// 1. 定义事件列表
todos: [
{id: "1", title: "吃饭", done: false},
{id: "2", title: "睡觉", done: true},
{id: "3", title: "打豆豆", done: false},
]
}
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
List.vue
<template>
<div>
<ul class="todo-main">
<!-- 2. 遍历appTodos,并将每一个对象传到每一个子组件 -->
<TodoItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
</ul>
</div>
</template>
<script>
import TodoItem from "@/components/TodoItem";
export default {
name: "ListInfo",
components: {
TodoItem
},
props:["todos"] // 1.声明从App组件接收的参数名
}
</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>
Item.vue不变
Header组件向App组件传参数
App.vue中定义一个自定义函数,并将这个函数传到Header组件内
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<!-- 2. 将 receiveTodos 函数传到Header组件内 -->
<Header :receiveTodos="receiveTodos"/>
<List :todos="todos"/>
<Footer/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
todos: [
{id: "1", title: "吃饭", done: false},
{id: "2", title: "睡觉", done: true},
{id: "3", title: "打豆豆", done: false},
]
}
},
methods:{
// 1. 定义自定义函数 receiveTodos(todos)
receiveTodos(todoObj){
console.log("我是App组件,我接受到了来自子组件的参数: ",todoObj)
// 3. 将todo对象加入到todos中
this.todos.unshift(todoObj)
}
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Header.vue
<template>
<div class="todo-header">
<!-- 1. 绑定回车键按下再抬起的事件,并将input中输入的值和title双向绑定 -->
<input type="text" placeholder="请输入你的任务名称,按回车确认" v-model="title" @keyup.enter="addTodo">
</div>
</template>
<script>
// 3.2 导入nanoid包,用来生成唯一随机字符串
import {nanoid} from 'nanoid'
export default {
name: "HeaderInfo",
props:["receiveTodos"], // 1. 声明接收来自App组件的参数名
data() {
return {
title: ""
}
},
methods: {
addTodo() {
if (!this.title.trim()) return alert("输入不能为空")
const todoObj = {id: nanoid(), title: this.title, done: false}
// 2. 调用receiveTodos(todObj), 并将用户生成的todo对象传到父组件定义好的函数中
this.receiveTodos(todoObj)
// 3. 清空input框中的数据
this.title = ""
}
}
}
</script>
<style scoped>
.todo-header {
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. 交互
1. 勾选和取消勾选
上面代码也可以按照下面写,如果 input 是 checkbox,并且使用v-model绑定了一个布尔值,那么由于v-model是双向绑定,这个checkbox的勾选状态会直接影响源数据中布尔值的变化,但是不建议这样用,因为props是只读的,这样双向数据绑定是修改了props,vue监测props的对象地址的变化,没有更深层的监测其中元素的地址变化
Item.vue
<template>
<li>
<label for="">
<!-- 使用v-model绑定了一个布尔值,那么由于v-model是双向绑定,这个checkbox的勾选状态会直接影响源数据中布尔值的变化 -->
<input type="checkbox" :checked="todo.done" v-model="todo.done">
<span>{{ todo.title}}</span>
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script>
export default {
name: "ItemInfo",
props: ["todo"], // 1. 声明子组件接收的参数
}
</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;
}
</style>
2. 删除
App.vue 中定义删除item的方法
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header :receiveTodos="receiveTodos"/>
<List :todos="todos" :changeTodoStatus="changeTodoStatus" :deleteTodoObj="deleteTodoObj"/>
<Footer/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
todos: [
{id: "1", title: "吃饭", done: false},
{id: "2", title: "睡觉", done: true},
{id: "3", title: "打豆豆", done: false},
]
}
},
methods:{
receiveTodos(todoObj){
console.log("我是App组件,我接受到了来自子组件的参数: ",todoObj)
this.todos.unshift(todoObj)
},
changeTodoStatus(id){
this.todos.forEach((todo)=>{
if (id === todo.id) todo.done = !todo.done
})
console.log(this.todos)
},
// 1. 定义删除Item的方法
deleteTodoObj(id){
this.todos = this.todos.filter((todo)=>{
return todo.id !== id
})
}
// 简写: 只有一个参数
deleteTodoObj(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
}
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
List.vue 接收父组件App的 deleteTodoObj 并下传给子组件Item
<template>
<div>
<ul class="todo-main">
<!-- 2. 将deleteTodoObj方法传给Item组件 -->
<TodoItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
:changeTodoStatus="changeTodoStatus"
:deleteTodoObj="deleteTodoObj"
/>
</ul>
</div>
</template>
<script>
import TodoItem from "@/components/TodoItem";
export default {
name: "ListInfo",
components: {
TodoItem
},
// 1.声明从App组件接收的deleteTodoObj
props:["todos","changeTodoStatus","deleteTodoObj"]
}
</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>
Item.vue
<template>
<!-- 4. 绑定点击事件,得到勾选的某项的ID -->
<li>
<label for="">
<!-- 3.radio标签的勾选状态,使用动态属性checked,由父组件中todo对象的done字段决定是否勾选 -->
<input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
<span>{{ todo.title}}</span> <!-- 2. 展示要做事的名称 -->
</label>
<button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: "ItemInfo",
// 1. 声明接收deleteTodoObj
props: ["todo", "changeTodoStatus","deleteTodoObj"],
methods: {
changeTodo(id) {
this.changeTodoStatus(id)
},
deleteTodo(id) {
if (confirm("确定删除吗?")){
// 2. 调用deleteTodoObj,将操作的哪条的ID传回给App
this.deleteTodoObj(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: #cccccc;
}
li:hover button {
display: block;
}
</style>
3. 底部统计
使用计算属性,计算出已完成的数量
<template>
<div class="todo-footer">
<label for="">
<input type="checkbox">
</label>
<span>
<!-- 使用计算出来的已完成的数量的计算属性 -->
<span>已完成{{doneTodototal}}</span> / 全部{{total}}
</span>
<button class="btn btn-danger">清除已完成的任务</button>
</div>
</template>
<script>
export default {
name: "FooterInfo",
props: ["todos"],
computed: {
total(){
return this.todos.length
},
// 1. 第一种方法: 计算出已完成的数量
doneTodototal() {
let num = 0
this.todos.forEach(todo => {
if (todo.done) {
num++
}
})
return num
},
// 第二种方法: reduce是循环整个Array,参数1为自定义函数,参数2为统计条件,
// pre是每次调用的上次调用reduce的返回值,current是当前的值,最后一次调用函数的返回值是整个reduce的返回值
doneTodototal() {
return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
},
}
}
</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>
4. 全选
App.vue 下传到Footer组件 全选或全不选的方法
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header :receiveTodos="receiveTodos"/>
<List
:todos="todos"
:changeTodoStatus="changeTodoStatus"
:deleteTodoObj="deleteTodoObj"
/>
<!-- 2. 将checkAllTodo方法传给Footer组件 -->
<Footer
:todos="todos"
:checkAllTodo="checkAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
// 1. 定义事件列表
todos: [
{id: "1", title: "吃饭", done: false},
{id: "2", title: "睡觉", done: true},
{id: "3", title: "打豆豆", done: false},
]
}
},
methods: {
receiveTodos(todoObj) {
console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
this.todos.unshift(todoObj)
},
changeTodoStatus(id) {
this.todos.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done
})
console.log(this.todos)
},
deleteTodoObj(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
// 1. 定义全选或取消全选
checkAllTodo(done){
this.todos.forEach((todo)=>{
todo.done = done
})
}
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Footer.vue
<template>
<div class="todo-footer" v-show="total">
<label for="">
<!-- 2. 绑定底部checkbox的点击事件 -->
<input type="checkbox" :checked="isAll" @change="checkAll">
</label>
<span>
<span>已完成{{doneTodototal}}</span> / 全部{{total}}
</span>
<button class="btn btn-danger">清除已完成的任务</button>
</div>
</template>
<script>
export default {
name: "FooterInfo",
// 1. 接收父组件的 checkAllTodo
props: ["todos","checkAllTodo"],
computed: {
total(){
return this.todos.length
},
doneTodototal() {
return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll(){
return this.doneTodototal === this.total && this.total > 0
}
},
methods:{
// 3. 点击全选或全不选
checkAll(e){
// 3.1 e.target.checked 底部的按钮是否被选中
// 3.2 调用checkAllTodo,将勾选状态传回给App组件
this.checkAllTodo(e.target.checked)
}
}
}
</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>
对上面版本的优化
Footer.vue
<template>
<!-- 3. 当总数等于0时,隐藏Footer组件 -->
<div class="todo-footer" v-show="total">
<label for="">
<input type="checkbox" v-model="isAll">
</label>
<span>
<span>已完成{{doneTodototal}}</span> / 全部{{total}}
</span>
<button class="btn btn-danger">清除已完成的任务</button>
</div>
</template>
<script>
export default {
name: "FooterInfo",
props: ["todos","checkAllTodo"],
computed: {
total(){
return this.todos.length
},
doneTodototal() {
return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll:{
// 下面代码写的是计算属性的简写形式,只读取,修改就会报错
// return this.doneTodototal === this.total && this.total > 0
// 所以需要写上get()和set()
get(){
return this.doneTodototal === this.total && this.total > 0
},
set(value){ // value是 isAll被修改的值
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>
5. 清除已完成任务
App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header :receiveTodos="receiveTodos"/>
<List
:todos="todos"
:changeTodoStatus="changeTodoStatus"
:deleteTodoObj="deleteTodoObj"
/>
<!-- 2. 将 clearAllDoneTodo 传给 Footer 组件-->
<Footer
:todos="todos"
:checkAllTodo="checkAllTodo"
:clearAllDoneTodo="clearAllDoneTodo"
/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
// 1. 定义事件列表
todos: [
{id: "1", title: "吃饭", done: false},
{id: "2", title: "睡觉", done: true},
{id: "3", title: "打豆豆", done: false},
]
}
},
methods: {
receiveTodos(todoObj) {
console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
this.todos.unshift(todoObj)
},
changeTodoStatus(id) {
this.todos.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done
})
console.log(this.todos)
},
deleteTodoObj(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
// 1. 定义 清除已完成任务
clearAllDoneTodo() {
if (confirm("确定清除吗?")) {
this.todos = this.todos.filter(todo => !todo.done)
}
}
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Footer.vue
<template>
<div class="todo-footer" v-show="total">
<label for="">
<input type="checkbox" v-model="isAll">
</label>
<span>
<span>已完成{{doneTodototal}}</span> / 全部{{total}}
</span>
<!-- 2. 绑定点击事件 clearAllDone -->
<button class="btn btn-danger" @click="clearAllDone">清除已完成的任务</button>
</div>
</template>
<script>
export default {
name: "FooterInfo",
// 1. 声明接收 clearAllDoneTodo
props: ["todos","checkAllTodo","clearAllDoneTodo"],
computed: {
total(){
return this.todos.length
},
doneTodototal() {
return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll:{
get(){
return this.doneTodototal === this.total && this.total > 0
},
set(done){
this.checkAllTodo(done)
}
}
},
methods:{
// 3. 调用App组件传过来的clearAllDoneTodo
clearAllDone(){
this.clearAllDoneTodo()
}
}
}
</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>
4. 总结
- 组件化编程流程
- 拆分静态组件: 组件要按照功能点拆分,命名不要与html元素冲突
- 实现动态组件: 考虑好数据的存放位置,数据是一个组件在用,还是多个组件同时在用
- 一个组件在用: 放在组件自身即可
- 多个组件在用: 放在他们共同的父组件上(官方对此行为叫做
状态提升)
- 实现交互: 从绑定事件开始
props适用于:- 父组件 ==> 子组件 的通信
- 子组件 ==> 父组件,需要父组件先下传一个函数,然后在子组件调用
- 使用
v-model时切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的 props传过来的如果是对象类型的值,修改对象中的属性时Vue不会报错,但是不推荐这样做
5. 加入本地存储机制
使用 watch() 监视todos的变化
App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header :receiveTodos="receiveTodos"/>
<List
:todos="todos"
:changeTodoStatus="changeTodoStatus"
:deleteTodoObj="deleteTodoObj"
/>
<!-- 2. 将 clearAllDoneTodo 传给 Footer 组件-->
<Footer
:todos="todos"
:checkAllTodo="checkAllTodo"
:clearAllDoneTodo="clearAllDoneTodo"
/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
// 1. 事件列表从 localstorage 中读取
todos: JSON.parse(localStorage.getItem("todos"))
}
},
methods: {
receiveTodos(todoObj) {
console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
this.todos.unshift(todoObj)
},
changeTodoStatus(id) {
this.todos.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done
})
console.log(this.todos)
},
deleteTodoObj(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
// 1. 定义 清除已完成任务
clearAllDoneTodo() {
if (confirm("确定清除吗?")) {
this.todos = this.todos.filter(todo => !todo.done)
}
}
},
// 2. 如果localstorage发生变化就将新的对象写入localstorage中,覆盖之前的
watch: {
todos(value) {
localStorage.setItem("todos",JSON.stringify(value))
}
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
上面代码存在的BUG
-
当
localStorage中没有todos这个key时,此时的todos为null,Footer组件读null.length时就会报错
data() { return { // 1. 取值的时候,加个或空数组 todos: JSON.parse(localStorage.getItem("todos")) || [] } }, -
勾选某项任务完成的时候,
localStorage中的done并未发生变化// 改为深度监视 watch: { todos: { deep:true, handler(value) { localStorage.setItem("todos", JSON.stringify(value)) } }, }
6. 子传父改为自定义事件
App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<!-- 1. 删除自定义函数的方式,改为自定义事件方式 -->
<!-- <Header :receiveTodos="receiveTodos"/> -->
<Header @receiveTodos="receiveTodos"/>
<List
:todos="todos"
:changeTodoStatus="changeTodoStatus"
:deleteTodoObj="deleteTodoObj"
/>
<!-- 3. 将自定义函数 clearAllDoneTodo ,改为自定义事件方式 -->
<Footer
:todos="todos"
:checkAllTodo="checkAllTodo"
@clearAllDoneTodo="clearAllDoneTodo"
/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
todos: JSON.parse(localStorage.getItem("todos")) || []
}
},
methods: {
// 2. 改为 receiveTodos 事件的回调函数
receiveTodos(todoObj) {
console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
this.todos.unshift(todoObj)
},
changeTodoStatus(id) {
this.todos.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done
})
console.log(this.todos)
},
deleteTodoObj(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
clearAllDoneTodo() {
if (confirm("确定清除吗?")) {
this.todos = this.todos.filter(todo => !todo.done)
}
}
},
watch: {
todos: {
deep:true,
handler(value) {
localStorage.setItem("todos", JSON.stringify(value))
}
},
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Header.vue
<template>
<div class="todo-header">
<!-- 2. 这里触发App中定义好的 receiveTodos 事件 -->
<input type="text" placeholder="请输入你的任务名称,按回车确认" v-model="title" @keyup.enter="addTodo">
</div>
</template>
<script>
// 3.2 导入nanoid包,用来生成唯一随机字符串
import {nanoid} from 'nanoid'
export default {
name: "HeaderInfo",
// props:["receiveTodos"], // 1. 不再接收自定义函数
data() {
return {
title: ""
}
},
methods: {
addTodo() {
if (!this.title.trim()) return alert("输入不能为空")
const todoObj = {id: nanoid(), title: this.title, done: false}
// 3. 将调用自定义函数,改为触发自定义事件
// this.receiveTodos(todoObj)
this.$emit("receiveTodos",todoObj)
this.title = ""
}
}
}
</script>
<style scoped>
.todo-header {
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>
Footer.vue
<template>
<div class="todo-footer" v-show="total">
<label for="">
<input type="checkbox" v-model="isAll">
</label>
<span>
<span>已完成{{doneTodototal}}</span> / 全部{{total}}
</span>
<!-- 2. 在这里触发自定义事件 clearAllDoneTodo -->
<button class="btn btn-danger" @click="clearAllDone">清除已完成的任务</button>
</div>
</template>
<script>
export default {
name: "FooterInfo",
// 1. 删除接收自定义函数 checkAllTodo,clearAllDoneTodo
props: ["todos"],
computed: {
total(){
return this.todos.length
},
doneTodototal() {
return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll:{
get(){
return this.doneTodototal === this.total && this.total > 0
},
set(done){
// 4. 删除自定义函数checkAllTodo,修改为触发自定义事件
// this.checkAllTodo(done)
this.$emit("checkAllTodo",done)
}
}
},
methods:{
clearAllDone(){
// 3. 删除自定义函数,修改为触发自定义事件
// this.clearAllDoneTodo()
this.$emit("clearAllDoneTodo")
}
}
}
</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>
7. 加入事件总线(推荐)
main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this; // 安装全局事件总线,$bus就是当前应用的vm
}
}).$mount('#app')
App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header @receiveTodos="receiveTodos"/>
<!-- 1. 删除changeTodoStatus和deleteTodoObj 自定义函数-->
<List :todos="todos"/>
<Footer
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllDoneTodo="clearAllDoneTodo"
/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
todos: JSON.parse(localStorage.getItem("todos")) || []
}
},
// 2. 在mounted中声明给$bus 事件总线绑定 changeTodoStatus和deleteTodoObj自定义事件
mounted() {
this.$bus.$on("changeTodoStatus",this.changeTodoStatus)
this.$bus.$on("deleteTodoObj",this.deleteTodoObj)
},
// 3. 在beforeDestory中解绑自定义事件 changeTodoStatus deleteTodoObj
beforeDestroy() {
this.$bus.$off(["changeTodoStatus","deleteTodoObj"])
},
methods: {
receiveTodos(todoObj) {
console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
this.todos.unshift(todoObj)
},
changeTodoStatus(id) {
this.todos.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done
})
console.log(this.todos)
},
deleteTodoObj(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
clearAllDoneTodo() {
if (confirm("确定清除吗?")) {
this.todos = this.todos.filter(todo => !todo.done)
}
}
},
watch: {
todos: {
deep:true,
handler(value) {
localStorage.setItem("todos", JSON.stringify(value))
}
},
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
List.vue
<template>
<div>
<ul class="todo-main">
<!-- 1. 删除changeTodoStatus和deleteTodoObj 自定义函数 -->
<TodoItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
/>
</ul>
</div>
</template>
<script>
import TodoItem from "@/components/TodoItem";
export default {
name: "ListInfo",
components: {
TodoItem
},
props:["todos"] // 2.删除changeTodoStatus和deleteTodoObj
}
</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>
Item.vue
<template>
<!-- 4. 绑定点击事件,得到勾选的某项的ID -->
<li>
<label for="">
<!-- 3.radio标签的勾选状态,使用动态属性checked,由父组件中todo对象的done字段决定是否勾选 -->
<input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
<span>{{ todo.title}}</span> <!-- 2. 展示要做事的名称 -->
</label>
<button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: "ItemInfo",
props: ["todo"], // 1. 删除changeTodoStatus和deleteTodoObj
methods: {
changeTodo(id) {
// 2. 删除调用父组件定义的自定义函数,改为事件总线触发 changeTodoStatus 自定义事件
// this.changeTodoStatus(id)
this.$bus.$emit("changeTodoStatus",id)
},
deleteTodo(id) {
if (confirm("确定删除吗?")){
// 3. 删除调用父组件定义的自定义函数,改为事件总线触发 deleteTodoObj 自定义事件
// this.deleteTodoObj(id)
this.$bus.$emit("deleteTodoObj",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: #cccccc;
}
li:hover button {
display: block;
}
</style>
8. 改为消息订阅与发布
下载 pubsub-js
npm i pubsub-js
main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
// 1. 卸载全局事件总线
// beforeCreate() {
// Vue.prototype.$bus = this; // 安装全局事件总线,$bus就是当前应用的vm
// }
}).$mount('#app')
App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header @receiveTodos="receiveTodos"/>
<!-- 1. 删除changeTodoStatus和deleteTodoObj 自定义函数-->
<List :todos="todos"/>
<Footer
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllDoneTodo="clearAllDoneTodo"
/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
// 1. 引入pubsub-js
import pubsub from 'pubsub-js'
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
todos: JSON.parse(localStorage.getItem("todos")) || []
}
},
// 2. 在mounted() 中定义消息订阅与发布
mounted() {
// 2.1 删除事件总线方式,改为消息订阅与发布方式
// this.$bus.$on("changeTodoStatus",this.changeTodoStatus)
// this.$bus.$on("deleteTodoObj",this.deleteTodoObj)
this.changPubID = pubsub.subscribe("changeTodoStatus",this.changeTodoStatus)
this.deletePubID = pubsub.subscribe("deleteTodoObj",this.deleteTodoObj)
},
// 3. 在beforeDestory()中取消订阅
beforeDestroy() {
// this.$bus.$off(["changeTodoStatus","deleteTodoObj"])
pubsub.unsubscribe(this.changPubID)
pubsub.unsubscribe(this.deletePubID)
},
methods: {
receiveTodos(todoObj) {
console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
this.todos.unshift(todoObj)
},
changeTodoStatus(_,id) {
this.todos.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done
})
console.log(this.todos)
},
deleteTodoObj(_,id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
clearAllDoneTodo() {
if (confirm("确定清除吗?")) {
this.todos = this.todos.filter(todo => !todo.done)
}
}
},
watch: {
todos: {
deep:true,
handler(value) {
localStorage.setItem("todos", JSON.stringify(value))
}
},
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Item.vue
<template>
<li>
<label for="">
<input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
<span>{{ todo.title}}</span> <!-- 2. 展示要做事的名称 -->
</label>
<button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
</li>
</template>
<script>
// 1. 引入pubsub-js
import pubsub from 'pubsub-js'
export default {
name: "ItemInfo",
props: ["todo"],
methods: {
// 2. changeTodo 和 deleteTodo改为发布消息方式
changeTodo(id) {
// this.$bus.$emit("changeTodoStatus",id)
pubsub.publish("changeTodoStatus",id)
},
deleteTodo(id) {
if (confirm("确定删除吗?")){
// this.$bus.$emit("deleteTodoObj",id)
pubsub.publish("deleteTodoObj",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: #cccccc;
}
li:hover button {
display: block;
}
</style>
9. 添加编辑按钮
Item.vue
<template>
<li>
<label for="">
<input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
<!-- 4. 和span标签相反出现,用!isEdit控制-->
<span v-show="!todo.isEdit">{{ todo.title}}</span>
<!-- 3. 准备input框,绑定失去焦点事件,失去焦点的瞬间保存数据 添加v-fbind自定义指令,默认获取焦点-->
<input type="text" v-show="todo.isEdit" :value="todo.title" v-fbind @blur="confirmEdit(todo,$event)">
</label>
<button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
<!-- 1. 添加编辑按钮,并绑定事件,传的是todo和event事件对象,用todo.isEdit控制编辑按钮是否展示 -->
<button v-show="!todo.isEdit" class="btn btn-edit" @click="editTodo(todo,$event)">编辑</button>
</li>
</template>
<script>
export default {
name: "ItemInfo",
props: ["todo"],
directives: {
// fbind自定义指令,默认获取焦点
fbind(element){
element.focus() // 获取焦点
}
},
methods: {
changeTodo(id) {
this.$bus.$emit("changeTodoStatus", id)
},
deleteTodo(id) {
if (confirm("确定删除吗?")) {
this.$bus.$emit("deleteTodoObj", id)
}
},
// 2. 编辑Todo
editTodo(todo, e) {
console.log("e>>>>>", e)
// 这样是不会触发Vue对todo的监视的,需要用this.$set()来设置属性
// todo.isEdit = true
// todo身上有isEdit,用todo.isEdit设置
// eslint-disable-next-line no-prototype-builtins
if (todo.hasOwnProperty("isEdit")) {
// 如果有isEdit,就直接修改即可
todo.isEdit = true
} else {
// 如果没有isEdit,需要用this.$set()来设置属性
this.$set(todo, "isEdit", true)
}
},
confirmEdit(todo, e) {
// 3. 先将input隐藏,改为文本框
todo.isEdit = false
if (!e.target.value.trim()) return alert("输入不能为空") // 判断不为空
this.$bus.$emit("updateTodo", todo.id, e.target.value) // 传需要改哪条和修改后的input值
}
}
}
</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: #cccccc;
}
li:hover button {
display: block;
}
</style>
App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-warp">
<Header @receiveTodos="receiveTodos"/>
<List :todos="todos"/>
<Footer
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllDoneTodo="clearAllDoneTodo"
/>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/TodoHeader";
import List from "@/components/TodoList";
import Footer from "@/components/TodoFooter";
export default {
name: 'App',
components: {
Footer,
List,
Header
},
data() {
return {
todos: JSON.parse(localStorage.getItem("todos")) || []
}
},
mounted() {
this.$bus.$on("changeTodoStatus", this.changeTodoStatus)
this.$bus.$on("deleteTodoObj", this.deleteTodoObj)
// 1. 给全局事件总线添加 updateTodo 自定义事件
this.$bus.$on("updateTodo", this.updateTodo)
},
beforeDestroy() {
// 3. 解绑updateTodo
this.$bus.$off(["changeTodoStatus", "deleteTodoObj","updateTodo"])
},
methods: {
receiveTodos(todoObj) {
console.log("我是App组件,我接受到了来自子组件的参数: ", todoObj)
this.todos.unshift(todoObj)
},
changeTodoStatus(id) {
this.todos.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done
})
console.log(this.todos)
},
deleteTodoObj(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
checkAllTodo(done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
clearAllDoneTodo() {
if (confirm("确定清除吗?")) {
this.todos = this.todos.filter(todo => !todo.done)
}
},
// 2. 修改todos中的元素title
updateTodo(id, title) {
this.todos.forEach((todo) => {
if (id === todo.id) todo.title = title
})
}
},
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem("todos", JSON.stringify(value))
}
},
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
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, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #ffffff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-edit {
color: #ffffff;
background-color: deepskyblue;
border: 1px solid #bd362f;
margin-right: 5px;
}
.btn-danger:hover {
color: #ffffff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-warp {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
对于获取焦点,除了使用自定义命令外,也可以用ref属性来的到DOM元素
Item.vue
<template>
<li>
<label for="">
<input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
<span v-show="!todo.isEdit">{{ todo.title}}</span>
<!-- 使用ref绑定DOM元素 -->
<input type="text" v-show="todo.isEdit" :value="todo.title" ref="updateElement" @blur="confirmEdit(todo,$event)">
</label>
<button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
<button v-show="!todo.isEdit" class="btn btn-edit" @click="editTodo(todo,$event)">编辑</button>
</li>
</template>
<script>
export default {
name: "ItemInfo",
props: ["todo"],
methods: {
changeTodo(id) {
this.$bus.$emit("changeTodoStatus", id)
},
deleteTodo(id) {
if (confirm("确定删除吗?")) {
this.$bus.$emit("deleteTodoObj", id)
}
},
editTodo(todo, e) {
if (todo.hasOwnProperty("isEdit")) {
todo.isEdit = true
} else {
this.$set(todo, "isEdit", true)
}
// 执行这句话的时候,Vue还没有渲染出input框,Vue是等所有的代码执行完毕后,再统一渲染页面,所以页面没有效果
// this.$refs.updateElement.focus()
// 使用$nextTick()解决,它指定的回调会在Dom元素更新完毕之后再执行
this.$nextTick(function(){
this.$refs.updateElement.focus()
})
},
confirmEdit(todo, e) {
todo.isEdit = false
if (!e.target.value.trim()) return alert("输入不能为空")
this.$bus.$emit("updateTodo", todo.id, e.target.value)
}
}
}
</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: #cccccc;
}
li:hover button {
display: block;
}
</style>
8. 加入动画效果
8.1 每项Todo加动画效果
Item.vue
<template>
<!-- 1. 整个外层套上transition -->
<transition name="leftSlide" appear>
<li>
<label for="">
<input type="checkbox" :checked="todo.done" @change="changeTodo(todo.id)">
<span v-show="!todo.isEdit">{{ todo.title}}</span>
<input type="text" v-show="todo.isEdit" :value="todo.title" ref="updateElement"
@blur="confirmEdit(todo,$event)">
</label>
<button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
<button v-show="!todo.isEdit" class="btn btn-edit" @click="editTodo(todo,$event)">编辑</button>
</li>
</transition>
</template>
<script>
export default {
name: "ItemInfo",
props: ["todo"],
methods: {
changeTodo(id) {
this.$bus.$emit("changeTodoStatus", id)
},
deleteTodo(id) {
if (confirm("确定删除吗?")) {
this.$bus.$emit("deleteTodoObj", id)
}
},
// eslint-disable-next-line no-unused-vars
editTodo(todo, e) {
// eslint-disable-next-line no-prototype-builtins
if (todo.hasOwnProperty("isEdit")) {
todo.isEdit = true
} else {
this.$set(todo, "isEdit", true)
}
this.$nextTick(function () {
this.$refs.updateElement.focus()
})
},
confirmEdit(todo, e) {
todo.isEdit = false
if (!e.target.value.trim()) return alert("输入不能为空")
this.$bus.$emit("updateTodo", todo.id, e.target.value)
}
}
}
</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: #cccccc;
}
li:hover button {
display: block;
}
/* 2. 编写动画效果 */
/* 进入的起点 */
.leftSlide-enter, .leftSlide-leave-to {
transform: translateX(100%);
}
/* 进入和离开过程中,需要激活的动画效果 */
.leftSlide-enter-active, .leftSlide-leave-active {
transition: 0.7s linear;
}
/* 进入的终点 */
.leftSlide-enter-to, .leftSlide-leave {
transform: translateX(0);
}
</style>
8.2 Item组件加动画效果
这里必须用 transition-group 标签,因为是多个Item组件
List.vue
<template>
<div>
<ul class="todo-main">
<!-- 1. Item组件外层套 transition-group 标签 -->
<transition-group name="leftSlide" appear>
<TodoItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
/>
</transition-group>
</ul>
</div>
</template>
<script>
import TodoItem from "@/components/TodoItem";
export default {
name: "ListInfo",
components: {
TodoItem
},
props: ["todos"]
}
</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;
}
/* 2. 编写动画效果 */
/* 进入的起点 */
.leftSlide-enter, .leftSlide-leave-to {
transform: translateX(100%);
}
/* 进入和离开过程中,需要激活的动画效果 */
.leftSlide-enter-active, .leftSlide-leave-active {
transition: 0.7s linear;
}
/* 进入的终点 */
.leftSlide-enter-to, .leftSlide-leave {
transform: translateX(0);
}
</style>

浙公网安备 33010602011771号