目录
- 1、前期准备
- 2、组件化编码流程
- 3、拆分静态组件
- 3.1 app组件
- 3.2 TodoList组件
- 3.2.1 TodoItem组件
- 3.3 TodoFooter组件
- 4、实现动态组件
- 5、实现交互
- 5.1 渲染页面
- 5.2 添加功能
- 5.3 勾选or取消勾选一个todo
- 5.4 删除一个todo
- 5.5 渲染TodoFooter底部内容
- 5.6 全选or取消全选
- 5.7清除所有已经完成的todo
- 5.8 改为本地存储版
- 5.9 编辑数据
- 5.10 添加一个动画
- 6、完整代码
实现效果:
源码在主页资源,可自行下载
1、前期准备
- 先搭建好Vue脚手架
具体步骤
第一步(仅第一次执行):全局安装@vue/cli
npm install -g @vue/cli
第二步:切换到你要创建项目的目录,然后使用命令创建项目
vue create xxxx
注:xxxx是你的项目名称
第三步:启动项目
npm run serve
2、创建好我们的结构目录
2、组件化编码流程
- (1).拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
- (2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
- 一个组件在用:放在组件自身即可。
- 一些组件在用:放在他们共同的父组件上(
状态提升
)。
- (3).实现交互:从绑定事件开始。
3、拆分静态组件
我们将整体结构分为一个主组件app
和3个子组件
TodoHeader,TodoList,TodoFooter,在TodoList中有一个组件TodoItem。
根据组件的功能
拆分静态资源
3.1 app组件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader />
<TodoList />
<TodoFooter />
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name:'App',
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
};
</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.2 TodoList组件
<template>
<ul class="todo-main">
<!-- 使用组件 -->
<TodoItem/>
</ul>
</template>
<script>
// 导入组件
import TodoItem from './TodoItem'
export default {
name:'TodoList',
// 注册组件
components:{
TodoItem
}
};
</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>
3.2.1 TodoItem组件
<template>
<li>
<label>
<input type="checkbox" />
<span>xxxxx</span>
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script>
export default {
name:'TodoItem'
};
</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;
}
</style>
3.3 TodoFooter组件
<template>
<div class="todo-footer">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成0</span> / 全部2
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name:'TodoFooter'
}
</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>
4、实现动态组件
我们的数据很多个组件都需要用到,所以我们应该将数据放在最大的App组件
里面
data() {
return {
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos: [
{ id: "001", title: "抽烟", done: true },
{ id: "002", title: "喝酒", done: false },
{ id: "003", title: "开车", done: true },
],
};
},
};
},
5、实现交互
5.1 渲染页面
-
要实现TodoList组件里面的数据是响应式的,我们就需要将App组件里面的数据发送给它,这个时候我们就需要用到
组件间的通信
。 -
App组件通过
:todos="todos"
的props通信方式,将数据传给TodoList组件<TodoList :todos="todos" />
-
TodoList组件通过props配置项接收数据
//接收App传过来的数据 props:['todos'],
-
TodoList接收到数据后,对子组件进行列表循环,并且每一个对象传给TodoItem子组件,让它渲染里面的具体数据。
<TodoItem v-for="todoObj in todos" :key='todoObj.id' :todo='todoObj' />
-
TodoItem组件通过props配置项接收数据
props:['todo']
-
TodoItem接收到数据之后渲染页面
<template> <li> <label> <input type="checkbox" /> <span>{{todo.title}}</span> </label> <button class="btn btn-danger" style="display: none">删除</button> </li> </template>
5.2 添加功能
-
实现添加功能我们就需要对
TodoHeader组件
进行设置,将数据发送给App组件
。 -
由于是
子组件 ===> 父组件
传递数据,所以我将使用组件的自定义事件
完成这个功能
App组件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader @addTodo='addTodo'/>
<TodoList :todos="todos" />
<TodoFooter />
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
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>
TodoHeader组件
<template>
<div class="todo-header">
<input
type="text"
placeholder="请输入你的任务名称,按回车键确认"
v-model="title"
@keyup.enter="add"
/>
</div>
</template>
<script>
// 导入nanoid包,这个包采用的是分别暴露的方式
import { nanoid } from "nanoid";
export default {
name: "TodoHeader",
data() {
return {
// 用于接收用户输入的数据
title: "",
};
},
methods: {
add() {
// 如果用户输入为空就终止下面语句
if (!this.title.trim()) return alert("内容不能为空");
// 将用户输入的数据包装成一个对象
const todoObj = { id: nanoid(), title: this.title, done: false };
this.$emit("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.3 勾选or取消勾选一个todo
- 这个功能我们需要根据每一个todo的复选框的状态来修改数据里面的
done值
- 由于这个功能的通信是
孙子==>爷爷
(TodoItem==>App),所以我们采用全局事件总线的方式来完成通信。
main文件
安装全局事件总线
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus = this//安装全局事件总线
}
}).$mount('#app')
App组件
作为接收数据放,所以App组件想接收数据,则在App组件中给$bus绑定自定义事件,事件的回调留在App组件自身。
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader @addTodo="addTodo" />
<TodoList :todos="todos" />
<TodoFooter />
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos: [
{ id: "001", title: "抽烟", done: true },
{ id: "002", title: "喝酒", done: false },
{ id: "003", title: "开车", done: true },
],
};
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj);
},
// 勾选or取消勾选一个todo
checkTodo(id) {
this.todos.forEach((item) => {
if (item.id === id) item.done = !item.done;
});
},
},
// 全局事件总线通信方式
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
},
beforeDestroy() {
this.$bus.$off("checkTodo");
},
};
</script>
TodoItem组件
作为提供数据者:需要调用这个事件this.$bus.$emit('xxxx',数据)
<template>
<li>
<label>
<input type="checkbox" :checked='todo.done' @change='handelCheck(todo.id)'/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script>
export default {
name:'TodoItem',
// 接收TodoList传过来的数据
props:['todo'],
methods: {
handelCheck(id) {
this.$bus.$emit('checkTodo',id)
}
},
};
</script>
5.4 删除一个todo
- 同样采用事件总线的通信方式
App
组件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader @addTodo="addTodo" />
<TodoList :todos="todos" />
<TodoFooter />
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos: [
{ id: "001", title: "抽烟", done: true },
{ id: "002", title: "喝酒", done: false },
{ id: "003", title: "开车", done: true },
],
};
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj);
},
// 勾选or取消勾选一个todo
checkTodo(id) {
this.todos.forEach((item) => {
if (item.id === id) item.done = !item.done;
});
},
// 删除一个todo
DeleteTodo(id) {
this.todos = this.todos.filter((item) => {
return item.id != id;
});
},
},
// 全局事件总线通信方式
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
this.$bus.$on("DeleteTodo", this.DeleteTodo);
},
beforeDestroy() {
this.$bus.$off("checkTodo");
},
};
</script>
TodoItem组件
<template>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="handelCheck(todo.id)"
/>
<span>{{ todo.title }}</span>
</label>
<button class="btn btn-danger" @click="handelDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: "TodoItem",
// 接收TodoList传过来的数据
props: ["todo"],
methods: {
handelCheck(id) {
this.$bus.$emit("checkTodo", id);
},
handelDelete(id) {
if(confirm('确定要删除吗?')) this.$bus.$emit("DeleteTodo", id);
},
},
};
</script>
5.5 渲染TodoFooter底部内容
- 我们需要将数据todos发送给TodoFooter组件,这里采用最简单的props通信方式。
App组件
<TodoFooter :todos="todos"/>
TodoFooter组件
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span>
<span>{{ doneTotal }}</span> / {{ total }}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "TodoFooter",
props: ["todos"],
computed: {
// 总的todo
total() {
return this.todos.length;
},
// 已完成todo
doneTotal() {
return this.todos.reduce((pre, current) => pre + (current.done ? 1 : 0),0);
},
},
};
</script>
5.6 全选or取消全选
- 由于我们是
子组件==>父组件进行通信
(TodoFooter==>App),所以我们采用组件自定义事件来完成组件之间的通信
App组件
:
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader @addTodo="addTodo" />
<TodoList :todos="todos" />
<TodoFooter :todos="todos" @checkAllTodo='checkAllTodo'/>
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos: [
{ id: "001", title: "抽烟", done: true },
{ id: "002", title: "喝酒", done: false },
{ id: "003", title: "开车", done: true },
],
};
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj);
},
// 勾选or取消勾选一个todo
checkTodo(id) {
this.todos.forEach((item) => {
if (item.id === id) item.done = !item.done;
});
},
// 删除一个todo
DeleteTodo(id) {
this.todos = this.todos.filter((item) => {
return item.id != id;
});
},
// 全选或全不选
checkAllTodo(value) {
this.todos.forEach(item=>item.done = value)
}
},
// 全局事件总线通信方式
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
this.$bus.$on("DeleteTodo", this.DeleteTodo);
},
beforeDestroy() {
this.$bus.$off("checkTodo");
},
};
</script>
TodoFooter组件
<template>
<div class="todo-footer" v-if ='total'>
<label>
<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: "TodoFooter",
props: ["todos"],
computed: {
// 总的todo
total() {
return this.todos.length;
},
// 已完成todo
doneTotal() {
return this.todos.reduce((pre, current) => pre + (current.done ? 1 : 0),0);
},
// 全选或全不选
isAll:{
get() {
return this.doneTotal === this.total && this.total > 0
},
set(value){
this.$emit('checkAllTodo',value)
}
}
},
};
</script>
5.7清除所有已经完成的todo
- 由于我们还是
子组件==>父组件进行通信
(TodoFooter==>App),所以我们采用组件自定义事件来完成组件之间的通信
App组件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader @addTodo="addTodo" />
<TodoList :todos="todos" />
<TodoFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos: [
{ id: "001", title: "抽烟", done: true },
{ id: "002", title: "喝酒", done: false },
{ id: "003", title: "开车", done: true },
],
};
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj);
},
// 勾选or取消勾选一个todo
checkTodo(id) {
this.todos.forEach((item) => {
if (item.id === id) item.done = !item.done;
});
},
// 删除一个todo
DeleteTodo(id) {
this.todos = this.todos.filter((item) => {
return item.id != id;
});
},
// 全选或全不选
checkAllTodo(value) {
this.todos.forEach((item) => (item.done = value));
},
// 清除所有已完成的todo
clearAllTodo() {
this.todos = this.todos.filter((item) => !item.done);
},
},
// 全局事件总线通信方式
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
this.$bus.$on("DeleteTodo", this.DeleteTodo);
},
beforeDestroy() {
this.$bus.$off("checkTodo");
},
};
</script>
TodoFooter组件
<template>
<div class="todo-footer" v-if ='total'>
<label>
<input type="checkbox" v-model="isAll" />
</label>
<span>
<span>{{ doneTotal }}</span> / {{ total }}
</span>
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "TodoFooter",
props: ["todos"],
computed: {
// 总的todo
total() {
return this.todos.length;
},
// 已完成todo
doneTotal() {
return this.todos.reduce((pre, current) => pre + (current.done ? 1 : 0),0);
},
// 全选或全不选
isAll:{
get() {
return this.doneTotal === this.total && this.total > 0
},
set(value){
this.$emit('checkAllTodo',value)
}
},
},
methods: {
// 清除所有已完成todo
clearAll() {
this.$emit('clearAllTodo')
}
},
};
</script>
5.8 改为本地存储版
- 要实现页面刷新内容不丢失这个功能,我们就需要用到
localStorage 属性
来实现本地存储机制。同时需要用到watch属性
来检测数据。
相关API:
-
xxxxxStorage.setItem('key', 'value');
该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。 -
xxxxxStorage.getItem('person');
该方法接受一个键名作为参数,返回键名对应的值。
-
xxxxxStorage.removeItem('key');
该方法接受一个键名作为参数,并把该键名从存储中删除。
-
xxxxxStorage.clear()
该方法会清空存储中的所有数据。
App组件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader @addTodo="addTodo" />
<TodoList :todos="todos" />
<TodoFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos: JSON.parse(localStorage.getItem('todos'))|| []
};
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj);
},
// 勾选or取消勾选一个todo
checkTodo(id) {
this.todos.forEach((item) => {
if (item.id === id) item.done = !item.done;
});
},
// 删除一个todo
DeleteTodo(id) {
this.todos = this.todos.filter((item) => {
return item.id != id;
});
},
// 全选或全不选
checkAllTodo(value) {
this.todos.forEach((item) => (item.done = value));
},
// 清除所有已完成的todo
clearAllTodo() {
this.todos = this.todos.filter((item) => !item.done);
},
},
watch:{
todos:{
handler(value) {
localStorage.setItem('todos',JSON.stringify(value))
}
}
},
// 全局事件总线通信方式
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
this.$bus.$on("DeleteTodo", this.DeleteTodo);
},
beforeDestroy() {
this.$bus.$off("checkTodo");
},
};
</script>
5.9 编辑数据
这个功能我们需要根据每一个todo的id
和title
的值来修改数据里面的titel值
由于这个功能的通信是孙子==>爷爷(TodoItem==>App),所以我们采用全局事件总线的方式来完成通信。
App
组件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader @addTodo="addTodo" />
<TodoList :todos="todos" />
<TodoFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos: JSON.parse(localStorage.getItem("todos")),
};
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj);
},
// 勾选or取消勾选一个todo
checkTodo(id) {
this.todos.forEach((item) => {
if (item.id === id) item.done = !item.done;
});
},
// 删除一个todo
DeleteTodo(id) {
this.todos = this.todos.filter((item) => {
return item.id != id;
});
},
// 全选或全不选
checkAllTodo(value) {
this.todos.forEach((item) => (item.done = value));
},
// 清除所有已完成的todo
clearAllTodo() {
this.todos = this.todos.filter((item) => !item.done);
},
// 更新一个todo
updateTodo(id, value) {
this.todos.forEach((item) => {
if (item.id === id) item.title = value;
});
},
},
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem("todos", JSON.stringify(value));
},
},
},
// 全局事件总线通信方式
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
this.$bus.$on("DeleteTodo", this.DeleteTodo);
this.$bus.$on("updateTodo", this.updateTodo);
},
beforeDestroy() {
this.$bus.$off("checkTodo");
this.$bus.$off("updateTodo");
},
};
</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-edit {
color: #fff;
background-color: #8ed2f1;
border: 1px solid #419ce7;
margin-right: 5px;
}
.btn-edit:hover {
color: #fff;
background-color: #3ab3eb;
border: 1px solid #419ce7;
margin-right: 5px;
}
.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>
TodoItem
组件
<template>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="handelCheck(todo.id)"
/>
<span v-show="!todo.isEdit">{{ todo.title }}</span>
<input
type="text"
:value="todo.title"
v-show="todo.isEdit"
ref="inputFocus"
@blur="handleBlur(todo,$event)"
>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<button class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
</li>
</template>
<script>
export default {
name: "TodoItem",
// 接收TodoList传过来的数据
props: ["todo"],
methods: {
handelCheck(id) {
this.$bus.$emit("checkTodo", id);
},
handleDelete(id) {
if(confirm('确定要删除吗?')) this.$bus.$emit("DeleteTodo", id);
},
handleEdit(todo) {
if(todo.hasOwnProperty('isEdit')) {
todo.isEdit = true
} else {
this.$set(todo,'isEdit',true)
}
this.$nextTick(function() {
this.$refs.inputFocus.focus()
})
},
handleBlur(todo,e){
todo.isEdit = false
if(!e.target.value.trim()) return alert('输入不能为空!')
this.$bus.$emit('updateTodo',todo.id,e.target.value)
}
},
};
</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:hover button {
display: block;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
5.10 添加一个动画
- 给TodoItem组件添加一个动画
代码示例:
<template>
<transition name="animation1" appear>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="handleCheck(todo.id)"
/>
<span v-show="!todo.isEdit">{{ todo.title }}</span>
<input
type="text"
:value="todo.title"
v-show="todo.isEdit"
@blur="handleBlur(todo, $event)"
ref="inputTitle"
/>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<button
v-show="!todo.isEdit"
class="btn btn-edit"
@click="handleEdit(todo)"
>
编辑
</button>
</li>
</transition>
</template>
<script>
export default {
props: ["todo"],
methods: {
// 勾选or不勾选todo
handleCheck(id) {
// this.CheckTodo(id);
this.$bus.$emit("CheckTodo", id);
},
// 删除一个todo
handleDelete(id) {
if (confirm("确定要删除吗?")) {
// this.DeleteTodo(id)
this.$bus.$emit("DeleteTodo", id);
}
},
//编辑
handleEdit(todo) {
if(todo.hasOwnProperty('isEdit')){
todo.isEdit = true
}else{
this.$set(todo,'isEdit',true)
}
this.$nextTick(function(){
this.$refs.inputTitle.focus()
})
},
// 失去焦点
handleBlur(todo, e) {
todo.isEdit = false;
if (!e.target.value.trim()) return alert("输入不能为空!");
this.$bus.$emit("updateTodo", todo.id, e.target.value);
},
},
};
</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:hover {
background: #ddd;
}
li:hover button {
display: block;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
/* 进入过程中 */
.animation1-enter-active {
animation: animations 0.5s linear;
}
/* 离开过程中 */
.animation1-leave-active {
animation: animations 0.5s linear reverse;
}
@keyframes animations {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
</style>
6、完整代码
main文件
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus = this//安装全局事件总线
}
}).$mount('#app')
App组件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 使用组件 -->
<TodoHeader @addTodo="addTodo" />
<TodoList :todos="todos" />
<TodoFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
// 导入子组件
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 注册组件
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos: JSON.parse(localStorage.getItem("todos")),
};
},
methods: {
// 添加一个todo
addTodo(todoObj) {
this.todos.unshift(todoObj);
},
// 勾选or取消勾选一个todo
checkTodo(id) {
this.todos.forEach((item) => {
if (item.id === id) item.done = !item.done;
});
},
// 删除一个todo
DeleteTodo(id) {
this.todos = this.todos.filter((item) => {
return item.id != id;
});
},
// 全选或全不选
checkAllTodo(value) {
this.todos.forEach((item) => (item.done = value));
},
// 清除所有已完成的todo
clearAllTodo() {
this.todos = this.todos.filter((item) => !item.done);
},
// 更新一个todo
updateTodo(id, value) {
this.todos.forEach((item) => {
if (item.id === id) item.title = value;
});
},
},
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem("todos", JSON.stringify(value));
},
},
},
// 全局事件总线通信方式
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
this.$bus.$on("DeleteTodo", this.DeleteTodo);
this.$bus.$on("updateTodo", this.updateTodo);
},
beforeDestroy() {
this.$bus.$off("checkTodo");
this.$bus.$off("updateTodo");
},
};
</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-edit {
color: #fff;
background-color: #8ed2f1;
border: 1px solid #419ce7;
margin-right: 5px;
}
.btn-edit:hover {
color: #fff;
background-color: #3ab3eb;
border: 1px solid #419ce7;
margin-right: 5px;
}
.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>
TodoHeader组件
<template>
<div class="todo-header">
<input
type="text"
placeholder="请输入你的任务名称,按回车键确认"
v-model="title"
@keyup.enter="add"
/>
</div>
</template>
<script>
// 导入nanoid包,这个包采用的是分别暴露的方式
import { nanoid } from "nanoid";
export default {
name: "TodoHeader",
data() {
return {
// 用于接收用户输入的数据
title: "",
};
},
methods: {
add() {
// 如果用户输入为空就终止下面语句
if (!this.title.trim()) return alert("内容不能为空");
// 将用户输入的数据包装成一个对象
const todoObj = { id: nanoid(), title: this.title, done: false };
this.$emit("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>
TodoList组件
<template>
<ul class="todo-main">
<!-- 使用组件 -->
<TodoItem
v-for="todoObj in todos"
:key='todoObj.id'
:todo='todoObj'
/>
</ul>
</template>
<script>
// 导入组件
import TodoItem from './TodoItem'
export default {
name:'TodoList',
//接收App传过来的数据
props:['todos'],
// 注册组件
components:{
TodoItem
},
};
</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>
TodoFooter组件
<template>
<div class="todo-footer" v-if ='total'>
<label>
<input type="checkbox" v-model="isAll" />
</label>
<span>
<span>{{ doneTotal }}</span> / {{ total }}
</span>
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "TodoFooter",
props: ["todos"],
computed: {
// 总的todo
total() {
return this.todos.length;
},
// 已完成todo
doneTotal() {
return this.todos.reduce((pre, current) => pre + (current.done ? 1 : 0),0);
},
// 全选或全不选
isAll:{
get() {
return this.doneTotal === this.total && this.total > 0
},
set(value){
this.$emit('checkAllTodo',value)
}
},
},
methods: {
// 清除所有已完成todo
clearAll() {
if(confirm('确定要删除已经完成的事项吗?'))this.$emit('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>
TodoItem组件
<template>
<transition name="animation1" appear>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="handleCheck(todo.id)"
/>
<span v-show="!todo.isEdit">{{ todo.title }}</span>
<input
type="text"
:value="todo.title"
v-show="todo.isEdit"
@blur="handleBlur(todo, $event)"
ref="inputTitle"
/>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<button
v-show="!todo.isEdit"
class="btn btn-edit"
@click="handleEdit(todo)"
>
编辑
</button>
</li>
</transition>
</template>
<script>
export default {
props: ["todo"],
methods: {
// 勾选or不勾选todo
handleCheck(id) {
// this.CheckTodo(id);
this.$bus.$emit("CheckTodo", id);
},
// 删除一个todo
handleDelete(id) {
if (confirm("确定要删除吗?")) {
// this.DeleteTodo(id)
this.$bus.$emit("DeleteTodo", id);
}
},
//编辑
handleEdit(todo) {
if(todo.hasOwnProperty('isEdit')){
todo.isEdit = true
}else{
this.$set(todo,'isEdit',true)
}
this.$nextTick(function(){
this.$refs.inputTitle.focus()
})
},
// 失去焦点
handleBlur(todo, e) {
todo.isEdit = false;
if (!e.target.value.trim()) return alert("输入不能为空!");
this.$bus.$emit("updateTodo", todo.id, e.target.value);
},
},
};
</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:hover {
background: #ddd;
}
li:hover button {
display: block;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
/* 进入过程中 */
.animation1-enter-active {
animation: animations 0.5s linear;
}
/* 离开过程中 */
.animation1-leave-active {
animation: animations 0.5s linear reverse;
}
@keyframes animations {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
</style>