界面完成
components 文件夹下新建 Header.vue来完成头部组件
<template>
<div class="todo-header">
<input type="text" placeholder="请输入任务名,按回车确认">
</div>
</template>
<script>
export default {
name: "Header"
}
</script>
<style scoped>
.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.8);
}
</style>
Footer.vue 来完成底部组件
<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: "Footer"
}
</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>
List.vue 来完成中间内容组件
<template>
<div>
<ul class="todo-main">
<Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
</ul>
</div>
</template>
<script>
import Item from "./Item"
export default {
name: "List",
components: {
Item
},
data(){
return{
todos:[
{id:'001',title:'抽烟',done:true},
{id:'002',title:'喝酒',done:false},
{id:'003',title:'开车',done:true},
]
}
}
}
</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>
Item.vue 来完成每一项的组件
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done">
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display:none">删除</button>
</li>
</template>
<script>
export default {
name: "Item",
props:['todo']
}
</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>
App.vue 中引入并注册组件,同时写一些公共样式
<template>
<div class="root">
<div class="todo-container">
<div class="todo-wrap">
<Header/>
<List/>
<Footer/>
</div>
</div>
</div>
</template>
<script>
//引入组件
import Header from "./components/Header";
import Footer from "./components/Footer";
import List from "./components/List";
export default {
name: 'App',
components: {
Header,
Footer,
List
}
}
</script>
<style>
body {
background: #ffffff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertica1-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgb(255, 255, 255, 0.2), 0 1px 2px rgb(255, 255, 255, 0.5);
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-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
main.js 中
//引入Vue
import Vue from 'vue';
//引入App
import App from './App';
//关闭vue的生产提示
Vue.config.productionTip = false
//创建vm
new Vue({
el: "#app",
render: h => h(App)
})
添加功能实现
我们需要在 Header.vue 编写代码,首先给 input 中增加键盘抬起时的事件 add,当按下回车时获取输入内容,然后包装成一个对象,也就是 List.vue 中相同的数据类型{id:'001',title:'抽烟',done:true}
其中 id 需要是唯一标识,可以使用 npm i nanoid
来安装 nanoid,用来生成唯一 id
其中 title 就是输入的内容,通 add(e) 中的参数 e.target.value 就可以拿到
最后 done 默认 false
<template>
<div class="todo-header">
<input type="text" placeholder="请输入任务名,按回车确认" @keyup.enter="add">
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name: "Header",
methods: {
add(e) {
const todoObj = {id: nanoid(), title: e.target.value, done: false}
console.log(todoObj);
}
//或者在input中增加双向绑定v-model="title"
/*add(){
console.log(this.title);
}*/
}
}
</script>
<style scoped>
.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.8);
}
</style>
在头部输入123,然后按回车,查看打印的内容
好了现在需要把这个对象添加给 List.vue 中的 todos 中。我们之前学过父组件向子组件中传值,但目前没有学兄弟组件之间的传值,所以,我们可以把 List.vue 中的数据 todos 剪切走放到 App.vue 中(后边学习其他方法)
App.vue 中
......
<List :todos="todos"/>
......
<script>
//引入组件
......
export default {
name: 'App',
components: {
......
},
data(){
return{
todos:[
{id:'001',title:'抽烟',done:true},
{id:'002',title:'喝酒',done:false},
{id:'003',title:'开车',done:true},
]
}
}
}
</script>
然后 List.vue 中接收一下就可以了
......
<script>
import Item from "./Item"
export default {
name: "List",
props:["todos"],
components: {
Item
}
}
</script>
接着,当 Header.vue 中组装了一个对象后,传给 App.vue,需要这样做:首先 App.vue 中需要先传一个方法 addTodo 给 Header.vue,然后 Header.vue 接收到这个方法调用即可
修改 App.vue
<Header :addTodo="addTodo"/>
<script>
......
export default {
......
methods:{
addTodo(todoObj){
this.todos.unshift(todoObj)
}
}
}
</script>
修改 Header.vue
......
<script>
......
export default {
......
props:["addTodo"],
methods: {
add(e) {
if(!e.target.value.trim()) return alert("输入不能为空")
const todoObj = {id: nanoid(), title: e.target.value, done: false}
this.addTodo(todoObj)
e.target.value = ""
}
//或者在input中增加双向绑定v-model="title"
/*add(){
console.log(this.title);
}*/
}
}
</script>
......
勾选
实现方法一
思路:勾选后需要改变数据中的 done 的值,任务列表 todos 在 App.vue 中,所以在 App.vue 中增加 checkTodo 方法。由于 App.vue - List.vue - Item.vue 是嵌套关系,我们需要逐层传递,因此 先把这个方法传给 List.vue
App.vue
<List :todos="todos" :checkTodo="checkTodo"/>
<script>
......
export default {
......
methods:{
......
//勾选或取消勾选一个todo
checkTodo(id){
this.todos.forEach((todo)=>{
if(todo.id === id) todo.done = !todo.done
})
}
}
}
</script>
List.vue 中接收并再传递给 Item.vue
<Item v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
:checkTodo="checkTodo"
/>
<script>
......
export default {
......
props:["todos","checkTodo"],
......
}
</script>
......
Item.vue 中接收调用,在 checkbox 中增加 change 事件,获取选中或取消勾选的 id
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)">
<script>
export default {
name: "Item",
props:['todo','checkTodo'],
methods:{
handleCheck(id){
this.checkTodo(id)
}
}
}
</script>
到此为止我们就实现了勾选改变值的功能,运行看下效果:
实现方法二
我们可以换另一种写法,v-model
对应的 data 属性是 Boolean 时,返回的是checkbox
的checked
的 boolean 值,所以第一种方法中的 传方法之类的全都不写了,直接修改 Item.vue
<input type="checkbox" v-model="todo.done">
虽然同样可以实现上边的效果,但是这种方法是不建议的,因为我们之前说过,传过来的 props 是不能修改的,为什么 vue 还没报错呢?
我们看着段代码
let obj = {a:1,b:2}
obj.a = 2//第一种修改方式
obj = {x:11,b:22}//第二种修改方式
vue 能检测到的是第二种修改方式
不建议这种写法哦
删除
先修改 App.vue,增加一个删除方法 deleteTodo,传给 List.vue
<template>
<div class="root">
<div class="todo-container">
<div class="todo-wrap">
<Header :addTodo="addTodo"/>
<List :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
<Footer/>
</div>
</div>
</div>
</template>
<script>
......
export default {
......
methods: {
......
//删除todo
deleteTodo(id) {
//过滤不改变原数组
/*this.todos = this.todos.filter((todo)=>{
return todo.id !== id
})*/
//可以精简为
this.todos = this.todos.filter(todo => todo.id !== id)
}
}
}
</script>
......
List.vue 接收,并传给 Item.vue
<template>
<div>
<ul class="todo-main">
<Item ......
:deleteTodo="deleteTodo"
/>
</ul>
</div>
</template>
<script>
export default {
......
props:["todos","checkTodo","deleteTodo"],
......
}
</script>
Item.vue 接收调用,并把要删除的 id 传过去
<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 {
......
props:['todo','checkTodo','deleteTodo'],
methods:{
......
//删除
handleDelete(id){
if(confirm("确认删除吗?")){
this.deleteTodo(id)
}
}
}
}
</script>
<style scoped>
......
li:hover{
background-color: #dddddd;
}
li:hover button{
display: block;
}
</style>
底部统计功能实现
底部要实现统计功能,必须知道 todos 的长度和选中个数,所以需要将 todos 传过来
App.vue 将 todos 传给 Footer.vue
<Footer :todos="todos"/>
Footer.vue 接收并计算,使用 reduce 函数计算选中的个数,关于reduce的用法
如果选中个数和数组个数相等,那么前边的 checkbox 将被选中。使用计算属性返回选中个数和数组个数是否相等即可
如果把项目全部删除,那么底部将整体不展示,可以给 Footer.vue 增加一个 v-show 即可
点击 checkbox 将把项目全部选中,可以给 checkbox 增加 change 事件来全选或全部选,所以需要在 App.vue 中增加方法
App.vue
<Footer :todos="todos" :checkAllTodo="checkAllTodo"/>
<script>
......
export default {
......
methods: {
......
//全选 or 取消全选
checkAllTodo(done){
this.todos.forEach((todo)=>{
todo.done = done
})
}
}
}
</script>
Footer.vue
<template>
<div class="todo-footer" v-show="total">
<label>
<input type="checkbox" :checked="isAll" @change="checkAll">
</label>
<span>
<span>已完成{{ doneTotal }}</span>/全部{{ total }}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "Footer",
props: ["todos","checkAllTodo"],
computed: {
total(){
return this.todos.length
},
doneTotal() {
/*return this.todos.reduce((pre, current) => {
return pre + (current.done ? 1 : 0)
}, 0)*/
//精简为
return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
},
isAll(){
return this.doneTotal === this.total
}
},
methods:{
checkAll(e){
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 的 checkbox 可以使用 v-model 来双向绑定数据
<input type="checkbox" v-model="isAll">
<script>
export default {
name: "Footer",
props: ["todos","checkAllTodo"],
computed: {
......
isAll:{
get(){
return this.doneTotal === this.total
},
set(value){
this.checkAllTodo(value)
}
}
}
}
</script>
底部清除已完成任务
App.vue中增加 clearAllTodo 方法,返回未被选中的项目,然后把这个方法传给 Footer.vue
App.vue中
<Footer :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
<script>
......
export default {
......
methods: {
......
//清除全部选中的任务
clearAllTodo(){
this.todos = this.todos.filter((todo)=>{
return !todo.done
})
}
}
}
</script>
Footer.vue接收并使用
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
<script>
export default {
......
methods:{
clearAll(){
this.clearAllTodo()
}
}
}
</script>
总结
1、组件化编码流程:
(1).拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突
(2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
------①一个组件在用:放在组件自身即可
------②一些组件在用:放在他们共同的父组件上(状态提升)
(3).实现交互:从绑定事件开始
2、props适用于:
(1).父组件=>子组件通信
(2).子组件=>父组件通信(要求父先给子一个函数)
3、使用v-model
时要切记:v-model
绑定的值不能是 props 传过来的值,因为 props 是不可以修改的!!
4、props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错。但不推荐这样做