Todo-list 案例
一、组件化编码流程
- 拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突。
- 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件再用:
(1)一个组件在用:放在组件自身即可。
(2)一些组件再用:放在他们共同的父组件上(状态提示)。 - 交互——从绑定事件监听开始。
二、props 扩展
-
props 适用于:
(1)父组件 ===> 子组件 通信
(2)子组件 ===> 父组件 通信(要求父先给子一个函数) -
使用 v-model 时要切记:
v-model 绑定的值不能是 props 传过来的值,因为 props 是不可修改的! -
props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐这样做。
三、webStorage
-
存储内容大小一般支持 5MB 左右(不同浏览器可能不一样)
-
浏览器通过 Window.sessionStorage 和 Window.localStorage 属性实现本地存储机制。
-
相关 API:
(1)xxxxxStorage.setItem('key', 'value');
该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。
(2)xxxxxStorage.getItem('person');
该方法接受一个键名作为参数,返回键名对应的值。
(3)xxxxxStorage.removeItem('key');
该方法接受一个键名作为参数,并把该键名从存储中删除。
(4)xxxxxStorage.clear();
该方法会清空存储中的所有数据。 -
备注:
(1)SessionStorage 存储的内容会随着浏览器窗口关闭而消失。
(2)LocalStorage 存储的内容,需要手动清除才会消失。
(3)xxxxxStorage.getItem(xxx);
如果 xxx 对应的 value 获取不到,那么 getItem 的返回值是 null。
(4)JSON.parse(null)
的结果依然是 null。
四、自定义事件
-
自定义事件是一种组件间通信的方式,适用于:【子组件 ===> 父组件】
-
使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)
-
绑定自定义事件:
(1)第一种方式,通过v-on
,在父组件中:Demo @atguigu="test"
或Demo v-on:atguigu="test"
(2)第二种方式,通过ref
,在父组件中:<demo ref="demo"> methods:{ test(){ ... } }, mounted(){ this.$refs.demo.$on('ayguigu', this.test) }
(3)若想让自定义事件只能触发一次,可以用
.once
修饰符,或$once
方法。 -
触发自定义事件:
this.$emit('atguigu', 传给父的参数数据)
-
解绑自定义事件:解绑一个
this.$off('atguigu')
、解绑多个this.$off(['atguigu', 'demo', ...])
、解绑全部this.$off()
-
组件上也可以绑定原生 DOM 事件,需要使用
native
修饰符。<student @click.native="show"></student>
-
注意:通过
this.$refs.demo.$on('ayguigu', 回调)
绑定自定义时间时,回调要么配置在 methods 中,要么用箭头函数,否则回调中的 this 会指向子组件。
五、全局事件总线
-
全局事件总线是一种组件间通信的方式,适用于任意组件间的通信。
-
安装全局事件总线:
-
使用全局事件总线:
(1)接收数据:A组件想接收数据,则在A组件中给 $bus 绑定自定义事件,事件的回调留在A组件自身。methods() { demo(data){......} } ...... mounted() { this.$bus.$on('xxx', this.demo) }
(2)提供数据:
this.$bus.$emit('xxx', 数据)
-
最好在 beforeDestroy 钩子中,用
$off
去解绑当前组件所用到的事件。
六、消息订阅与发布
-
消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信。
-
使用步骤:
(1)安装第三方库 pubsub:npm i pubsub-js
(2)引入:import pubsub from "pubsub-js";
(3)接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。methods() { demo(data){......} } ...... mounted() { this.pid = pubsub.subscribe('xxx', this.demo) // 订阅消息 }
(4)提供数据:
pubsub.publish('xxx', 数据)
-
最好在 beforeDestroy 钩子中,用
pubsub.unsubscribe(this.pid);
去取消订阅。
七、nextTick
-
语法:
this.$nextTick(回调函数)
-
作用:nextTick 是一个生命周期钩子,在下一次 DOM 更新结束后执行其指定的回调。
-
什么时候用:当改变数据后,要基于更新后的新 DOM 进行某些操作时,要在 nextTick 所指定的回调函数中执行。
八、动画与过度
-
作用:在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名。
-
图示:
-
写法:
(1)准备好样式:
元素进入的样式:
① v-enter:进入的起点
② v-enter-active:进入过程中
① v-enter-to:进入的终点
元素离开的样式:
① v-leave:离开的起点
② v-leave-active:离开过程中
① v-leave-to:离开的终点 -
使用
<transition>
包裹要过度的元素,并配置 name 属性:<transition> <h1 v-show="isShow">你好啊!</h1> </transition>
-
备注:若有多个元素需要过度,则需要使用:
<transition-group>
,且每个元素都要指定key
值。
1. 动画 animation
<template>
<div>
<transition-group name="hello" appear>
<h2 v-show="show" key="1">学校名称:{{ name }}</h2>
<h2 v-show="show" key="2">学校地址:{{ address }}</h2>
</transition-group>
</div>
</template>
<style lang="css" scoped>
h2 {
background-color: pink;
}
.hello-enter-active {
animation: identifier 1s;
}
.hello-leave-active {
animation: identifier 1s reverse;
}
@keyframes identifier {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
</style>
2. 过渡 transition
<template>
<div>
<transition-group name="hello" appear>
<h2 v-show="show" key="1">学校名称:{{ name }}</h2>
<h2 v-show="show" key="2">学校地址:{{ address }}</h2>
</transition-group>
</div>
</template>
<style lang="css" scoped>
h2 {
background-color: pink;
}
.hello-enter,
.hello-leave-to {
transform: translateX(-100%);
}
.hello-enter-to,
.hello-leave {
transform: translateX(0);
}
.hello-enter-active,
.hello-leave-active {
transition: 1s;
}
</style>
3. 第三方样式库 animate.css
-
安装
npm i animate.css
-
引入,在
<script>
中引入<script> import 'animate.css' </script>
-
使用
<template> <div> <transition-group name="animate__animated animate__bounce" enter-active-class="animate__jello" leave-active-class="animate__backOutUp" appear > <h2 v-show="!show" key="1">学校名称:{{ name }}</h2> <h2 v-show="show" key="2">学校地址:{{ address }}</h2> </transition-group> </div> </template> <script> import "animate.css"; </script> <style lang="css" scoped> h2 { background-color: pink; } </style>
九、案例代码
1. Header
<template>
<div class="todo-header">
<input
type="text"
placeholder="请输入你的任务名称,按回车键确认"
v-model="title"
@keyup.enter="add"
/>
</div>
</template>
<script>
import { nanoid } from "nanoid";
export default {
name: "AddHeader",
data() {
return {
title: "",
};
},
methods: {
add() {
if (!this.title.trim()) return alert("输入不能为空");
// 包装成对象
const todo = {
id: nanoid(),
title: this.title,
done: false,
};
this.$emit('headerAddTodo',todo)
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>
2. List
<template>
<ul class="todo-main" v-if="todo.length">
<transition-group
name="animate__animated animate__bounce"
enter-active-class="animate__bounceInRight"
leave-active-class="animate__backOutRight"
appear
>
<todo-item v-for="item in todo" :key="item.id" :todo="item" />
</transition-group>
</ul>
</template>
<script>
import "animate.css";
import TodoItem from "./TodoItem.vue";
export default {
components: { TodoItem },
name: "TodoList",
props: ["todo"],
};
</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. Item
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handle(todo.id)" />
<span v-show="!todo.isEdit"> {{ todo.title }}</span>
</label>
<button class="btn btn-danger" @click="ListDelete(todo.id)">删除</button>
<button v-show="!todo.isEdit" class="btn btn-edit" @click="editItem(todo)">编辑</button>
<input
v-show="todo.isEdit"
type="text"
:value="todo.title"
ref="itemInput"
@keyup.enter="inputBlur(todo, $event)"
@blur="inputBlur(todo, $event)"
/>
</li>
</template>
<script>
export default {
name: "TodoItem",
props: ["todo"],
methods: {
handle(id) {
this.$bus.$emit("checkTodo", id);
},
ListDelete(id) {
if (confirm("确定删除吗?")) {
this.$bus.$emit("deleteTodo", id);
}
},
editItem(todo) {
if (todo.hasOwnProperty("isEdit")) {
todo.isEdit = true;
} else {
this.$set(todo, "isEdit", true);
}
this.$nextTick(function () {
this.$refs.itemInput.focus();
});
},
inputBlur(todo, e) {
todo.isEdit = false;
// console.log(todo.id, e.target.value);
if (e.target.value.trim() === "") return alert("你没有输入值");
this.$bus.$emit("todotitle", 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:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: #ddd;
}
li:hover button {
display: block;
}
</style>
4. Footer
<template>
<div class="todo-footer" v-if="todo.length">
<label>
<input type="checkbox" v-model="checkAll" />
</label>
<span>
<span>已完成{{ doneTotal }}</span> / 全部{{ todo.length }}
</span>
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "SelectFooter",
props: ["todo"],
computed: {
doneTotal() {
return this.todo.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
},
checkAll: {
get() {
return this.todo.length === this.doneTotal;
},
set(value) {
this.$emit("checkAllTodo", value);
},
},
},
methods: {
clearAll() {
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>
5. App.vue
<template>
<div id="app">
<div class="todo-container">
<div class="todo-wrap">
<add-header @headerAddTodo="addTodo" />
<todo-list :todo="todo" />
<select-footer
:todo="todo"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
import AddHeader from "./components/AddHeader";
import SelectFooter from "./components/SelectFooter";
import TodoList from "./components/TodoList";
export default {
name: "App",
components: {
AddHeader,
SelectFooter,
TodoList,
},
data() {
return {
todo: JSON.parse(localStorage.getItem("todo")) || [],
};
},
methods: {
// 添加一个todo
addTodo(todo) {
this.todo.unshift(todo);
},
// 勾选or取消勾选
checkTodo(id) {
this.todo.forEach((todo) => {
if (id === todo.id) todo.done = !todo.done;
});
},
// 修改
editTodo(id, val) {
this.todo.forEach((todo) => {
if (id === todo.id) todo.title = val.trim();
});
},
// 删除
deleteTodo(id) {
this.todo = this.todo.filter((todo) => todo.id !== id);
},
checkAllTodo(check) {
this.todo.forEach((todo) => (todo.done = check));
},
clearAllTodo() {
const x = this.todo.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
if (x <= 0) {
alert("没有勾选任何TODO");
} else {
if (confirm("确定删除所有已完成吗?")) {
this.todo = this.todo.filter((todo) => !todo.done);
}
}
},
},
watch: {
todo: {
deep: true,
handler(value) {
localStorage.setItem("todo", JSON.stringify(value));
},
},
},
created() {
this.todo.forEach((todo) => {
if (todo.hasOwnProperty("isEdit")) {
todo.isEdit = false;
}
});
},
mounted() {
this.$bus.$on("checkTodo", this.checkTodo);
this.$bus.$on("deleteTodo", this.deleteTodo);
this.$bus.$on("todotitle", this.editTodo);
},
beforeDestroy() {
this.$bus.$off(["checkTodo", "deleteTodo", "todotitle"]);
},
};
</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: skyblue;
border: 1px solid rgb(106, 160, 182);
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>
6. 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;
},
}).$mount('#app')