一、整体思路
1.分析结构
我们对大盒子拆分,分成header、list、footer,但是list最好也进行拆分,因为它里面的每个小盒子结构一样就是字不一样,可以用一个组件多次调用完成,所以分成app>header、list、footer>item
2.实现静态页面效果
抽取组件形成静态页面,当前的item个数还是我们自己写死的,里面的内容都是xxx
3.展示动态数据
现在我们要把里面的内容改成吃饭、睡觉、抽烟、喝酒,存到list.vue的data中(以数组内{}的形式,多个对象写成数组,每个对象不光有title还有id以及勾选情况用{})
<myItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
item.vue中只有一个组件,我们用v-for进行遍历list中数组对象的个数,每一个都用item小盒子装起来。传递list中的数据给item,在item里用props:['todo'],就把数组对象传过去了。
然后我们就要实现动态的给这些被选中的爱好打勾了
<input type="checkbox" :checked="todo.done"/>
如果后面是true,那么就拥有checked选项
4.添加todo功能
首先得在header的input表单里设置读取键盘输入的内容
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
然后我们得把用户输入的名字包装成一个todo对象
小tips:如果有服务器的话,应该是由服务器给数据生成id,但是我们这里没有
id可以用uuid包来生成全球唯一的id,nanoid是uuid的精简版,省空间
add(event){
const todoObj={id:nanoid(),title:event.target.value,done:false}
console.log(todoObj)
}
接下来就把这个对象放进数组的前方
但是我们目前学过的不同组件传数据只能是父亲往儿子传,然后用儿子的标签里面写数据再用props传,但是我们写进去的数据在hearer里,跟list是兄弟
解决方法:我们把原本list的那些数组里的对象写在app里,也就是他俩的父亲上,list用props方法从父亲身上拿到,header就把新添加的数组对象给给父亲
实现儿子传给父亲:就是父亲提前给儿子一个函数,然后儿子在自己那里调函数
app父亲:
methods:{
receive(x){
this.todos.unshift(x)
}
}
//加入数组中
<myHeader :receive="receive"/>
hearder儿子:
props:['receive'],
methods:{
add(event){
const todoObj={id:nanoid(),title:event.target.value,done:false}
this.receive(todoObj)
}
}
我写进去爱好之后在header里面调用了app里的函数receive,然后那个函数就开始工作了,将我添加的新添一个对象到数组中,todos数组变了,vue就开始重新解析模版。
最后再完善一下,添加进去爱好之后把input里的文字清空
event.target.value=''
//但是这块就是在操作dom了,如果不用value的话,在input表单用v-model
注意:我们的computed、data、props、methods最终都会出现在vc中,所以避免重名的问题!
5.实现勾选功能
(1)第一种方法
我们数组对象里的done都写死了,不管勾不勾选都不变
思路:我们勾选之后先拿到这件事的id,然后找到它的done再取反就行
我们要修改的是app的数据的情况,所以增删改查都得在app 里做,app里设置函数然后在item调用,但是item是app的孙子,就先给她爸,传递下去
item:
methods: {
handleCheck(id){
this.checkTodo(id)
}
app:
checkTodo(id)
{
this.todos.forEach((todo)=>{
if(todo.id==id) todo.done=!todo.done
})
}
(2)第二种方法(不推荐)
还有一种方式是,:checked是检查初始值然后显示出来,change是检查后来的改变值然后进行数据更改,这两个我混成v-model="todo.done"(属于藏在对象里的改,vue发现不了;“a”就发现饿了),直接双向修改,我点了之后页面就改变,也不用爷爷传给孙子了methods了,直接在孙子这里就能改(需要props传过来的data),但是props不是只读不改吗?因为修改的是数组里面的对象的某一个属性,就识别不出来可以修改,但是原则上这样不好所以不使用这种
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
6.实现删除功能
核心思想:点击之后拿到id然后删除就行
和上一个相似,这个我自己写出来了,但是注意
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<!-- 这里的click触动的方法不能再用deleteTodo了因为那是app里定义的方法你可以拿来用不能再重新声明用 -->
app:返回与点击元素的id不同的id对应的todos
deleteTodo(id)
{
this.todos=this.todos.filter((todo)=>{
return todo.id!==id
})
},
item:
handleDelete(id){
if(confirm('确定要删除吗?')){//确定的话返回true
this.deleteTodo(id)
}
}
7.实现底部统计功能
(1)已完成/全部
全部就是todos.length
已完成就得遍历然后看谁是true了:
computed:{
doneTotal(){
let i=0
this.todos.forEach(todo => {
if(todo.done==true) i++
});
return i
}
}
这种写法有点低级:用reduce方法
doneTotal(){
//第二次的pre是第一次运行结果的返回值,第一次pre是0但是没有return所以第二次pre=undefined
//current是每一个todo项
// return this.todos.reduce((pre,current)=>{
// return pre+(current.done?1:0)
// },0)
return this.todos.reduce((pre,current)=>pre+(current.done?1:0),0)
//最后一次调用箭头函数的返回值就作为reduce的返回值
}
我觉得这种方法更麻烦
(2)全选
首先是如果所有爱好被全选那么全选框就得打勾,需要遍历一下(computed都需要return)
isAll(){
if(this.total==this.doneTotal&&this.doneTotal>0) return true
return false
//return this.total==this.doneTotal
},
<input type="checkbox" :checked="isAll"
然后就是如果全选框打勾了那么所有爱好都得打勾,也就是动App里的数据
<input type="checkbox" :checked="isAll" @click="checkAll"/>
methods:{
checkAll(e){
this.checkAllTodo(e.target.checked)
//还是得动app里的数据,得this.checkAllTodo,methods又没有
},
app:
checkAllTodo(done){
this.todos.forEach((todo)=>{
todo.done=done
})
},
(3)清除
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
clearAll(){
this.clearAllTodo()
}
点击之后动app里的数据
clearAllTodo(){
this.todos=this.todos.filter((todo)=>{
return !todo.done
})
},
二、代码文件总览
1.App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<myHeader :receive="receive"/>
<!-- 把一个方法传给myHeader -->
<myList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
<myFooter :todos="todos"
:checkAllTodo="checkAllTodo"
:clearAllTodo="clearAllTodo"/>
</div>
</div>
</div>
</template>
<script>
import myFooter from "./components/myFooter";
import myHeader from "./components/myHeader";
import myList from "./components/myList.vue";
//import myItem from './components.myItem'//这块不用引入item了因为它是包括在list中的
// 交互的样式
export default {
name: "App",
components: {
myFooter,
myHeader,
myList,
},
//数据在app里,那么对数据的所有增删改查都应该在app里
data(){
return {
todos:[
{id:'001',title:'吃饭',done:true},
{id:'002',title:'睡觉',done:true},
{id:'003',title:'抽烟',done:false},
{id:'004',title:'喝酒',done:false},
// 一般id都用字符串,因为数字是有尽头的
]
}
},
methods:{
//添加一个todo
receive(todoObj){
this.todos.unshift(todoObj)
},
//勾选/取消勾选
checkTodo(id)
{
this.todos.forEach((todo)=>{
if(todo.id==id) todo.done=!todo.done
})
},
deleteTodo(id)
{
this.todos=this.todos.filter((todo)=>{
return todo.id!==id
})
},
checkAllTodo(done){
this.todos.forEach((todo)=>{
todo.done=done
})
},
clearAllTodo(){
this.todos=this.todos.filter((todo)=>{
return !todo.done
})
},
}
};
</script>
<style scoped>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
2.MyHeader.vue
<template>
<div>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
</div>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
//nanoid是一个函数,直接调用就行
export default {
name: "myHeader",
props:['receive'],
methods:{
add(event){
//校验数据,前后不能为空
if(!event.target.value.trim()) return alert('不能输入空信息')
const todoObj={id:nanoid(),title:event.target.value,done:false}
this.receive(todoObj)
event.target.value=''
//但是这块就是在操作dom了,如果不用value的话,在input表单用v-model
}
}
};
</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>
3.MyList.vue
<template>
<ul class="todo-main">
<!-- 删除一个,取走一个 -->
<myItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"/>
</ul>
</template>
<script>
import myItem from "./myItem.vue";
export default {
name: "myList",
components: {
myItem,
},
props:['todos','checkTodo','deleteTodo']
};
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
4.MyItem.vue
<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>
<!-- 这里的click触动的方法不能再用deleteTodo了因为那是app里定义的方法你可以拿来用不能再重新声明用 -->
</li>
</template>
<script>
export default {
name: "myItem",
//接收todo对象
props:['todo','checkTodo','deleteTodo'],
methods: {
handleCheck(id){
this.checkTodo(id)
},
handleDelete(id){
if(confirm('确定要删除吗?')){//确定的话返回true
this.deleteTodo(id)
}
}
},
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
/* display: none; */
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover{
background-color: #ddd;
}
</style>
5.MyFooter.vue
<template>
<div class="todo-footer" v-show="total">
<label>
<input type="checkbox" :checked="isAll" @click="checkAll"/>
</label>
<span> <span>已完成{{doneTotal}}</span> / 全部{{total}} </span>
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "myFooter",
props:['todos','checkAllTodo','clearAllTodo'],
computed:{
total(){
return this.todos.length
},
doneTotal(){
//第二次的pre是第一次运行结果的返回值,第一次pre是0但是没有return所以第二次pre=undefined
//current是每一个todo项
// return this.todos.reduce((pre,current)=>{
// return pre+(current.done?1:0)
// },0)
return this.todos.reduce((pre,current)=>pre+(current.done?1:0),0)
//最后一次调用箭头函数的返回值就作为reduce的返回值
},
isAll(){
if(this.total==this.doneTotal&&this.doneTotal>0) return true
return false
//return this.total==this.doneTotal
},
},
methods:{
checkAll(e){
this.checkAllTodo(e.target.checked)
//还是得动app里的数据,得this.checkAllTodo,methods又没有
},
clearAll(){
this.clearAllTodo()
}
}
};
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
三、总结
1.组件化编码流程:
(1)拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
(2)实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
一个组件在用:放在组件自身即可。
一些组件在用:放在他们共同的父组件上(状态提升)。
(3)实现交互:从绑定事件开始。
2.props适用于:
(1).父组件 ==> 子组件 通信
(2).子组件 ==> 父组件 通信(要求父先给子一个函数)
3.使用v-model时要切记
v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
4.关于props
props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。