07.TodoList案例
】一共拆分为4个组件:【因为header和HTML内置的标签
整个TodoList案例的页面结构【html】和样式设置【style】:
TodoList.html文件
【还未实现动态交互】
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<style>
body {
background: #fff;
}
.todo-container {
width: 600px;
/* 上下外边距为0,左右自动,实际效果为左右居中*/
margin: 0 auto;
}
/* 后代选择器(包含选择器),选择到的是todo-container下面的所有后代todo-wrap */
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #67dbd1;
border-radius: 5px;
}
/* 头部样式设置 */
/* 后代选择器(包含选择器),选择到的是todo-header下面的所有后代input */
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
/* 内边距:上下4px,左右7px */
padding: 4px 7px;
}
/* :focus获得焦点,并设置其新样式:例如:用户单击一个input输入框获取焦点,然后这个input输入框的边框样式就会发生改变,和其他的输入框区别开来,表明已被选中。 */
.todo-header input:focus {
/* outline 与 border 相似,不同之处在于 outline 在整个元素周围画了一条线;它不能像 border 那样,指定在元素的一个面上设置轮廓,也就是不能单独设置顶部轮廓、右侧轮廓、底部轮廓或左侧轮廓。 */
outline: none;
/* 定义边框的颜色 */
border-color: rgba(82, 168, 236, 0.8);
/* boxShadow 属性把一个或多个下拉阴影添加到框上 */
/* 设置inset:内部阴影,不设置inset:外部阴影 */
/* 【0 0】:不设置X轴与Y轴偏移量 */
/* 第三个值【如10px,8px】:设置值阴影模糊半径为15px */
box-shadow: inset 0 0 10px rgba(124, 56, 207, 0.075), 0 0 8px rgba(224, 58, 17, 0.6);
background-color: bisque;
}
/*main*/
.todo-main {
/* 左外边距:0px 【盒子贴着盒子】*/
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;
}
/*item*/
li {
/* ul无序列表 ol有序列表*/
/* 列表前面无标记 */
list-style: none;
/* height定义了一个li盒子的高度 */
height: 36px;
/* 行高:指的是文字占有的实际高度 */
line-height: 36px;
/* 当height和line-height相等时,即盒子的高度和行高一样,内容上下居中 */
padding: 0 5px;
/* 边框底部:1px的实心线 颜色*/
border-bottom: 1px solid #c0abc3;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代label */
li label {
/* 左对齐浮动【元素一旦浮动就会脱离文档流(不占位,漂浮)】 */
float: left;
/* 鼠标放在label元素上时变成小小手 */
cursor: pointer;
}
li label input {
/* 垂直居中 */
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代button */
li button {
/* 向右浮动 */
float: right;
/* 不为被隐藏的对象保留其物理空间,即该对象在页面上彻底消失,通俗来说就是看不见也摸不到。 */
display: none;
/* 上边距为3px */
margin-top: 3px;
}
li:before {
/* initial:它将属性设置为其默认值。 */
content: initial;
}
/* 结构伪类选择器 选择最后一个li元素 */
li:last-child {
/* 边框底部没有线 */
border-bottom: none;
}
li:hover{
background-color: #ddd;
}
/* 鼠标移动到该元素上时,将button按钮显示出来 */
li:hover button{
/* display:block将元素显示为块级元素 */
display: block;
}
/* 尾部 */
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
/* 后代选择器:todo-footer里所有的label元素 */
.todo-footer label {
/* inline元素不会独占一行,多个相邻的行内元素会排列在同一行里,直到一行排列不下,才会新换一行,其宽度随元素的内容而变化 */
/* inline-block 的元素既具有 block 元素可以设置宽高的特性,同时又具有 inline 元素默认不换行的特性 */
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.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-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
style>
head>
<body>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
div>
<ul class="todo-main">
<li>
<label>
<input type="checkbox"/>
<span>练功夫span>
label>
<button class="btn btn-danger">删除button>
li>
<li>
<label>
<input type="checkbox"/>
<span>睡觉span>
label>
<button class="btn btn-danger">删除button>
li>
<li>
<label>
<input type="checkbox"/>
<span>打豆豆span>
label>
<button class="btn btn-danger">删除button>
li>
ul>
<div class="todo-footer">
<label>
<input type="checkbox"/>
label>
<span>
<span>已完成:0span> / 全部:3
span>
<button class="btn btn-danger">清除已完成任务button>
div>
div>
div>
div>
body>
html>
静态页面展示:
前期知识准备:
【false:input框不勾选】
【true:input框勾选】
【键盘按键事件:按下回车按键,调用add()方法】
event.target
是会返回触发事件触发的源头元素。【这个源头元素指的是,当我点击子元素,虽然父元素的点击事件也会被触发(冒泡机制),但子元素才是事件的源头元素。】
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
export default {
methods: {
add(e){
console.log(e.target.value);
//e.target拿到的是事件触发的源头input元素节点
//e.target.value:拿到input框里输入的数据
}
},
};
v-model:收集的就是输入框里的value值
生成全球唯一的ID:uuid
nanoid【是uuid的精简版】:安装指令----npm i nanoid
问题一: 把数据添加到list中,由于obj数据是在list中,添加的数据在myHeader中,属于兄弟关系,所以要做调整:把list的数据交给APP,myHeader添加的数据交给APP即可
补充:data,props,methods,computed里面的数据都可以在vc或者vm上访问到。
trim() 方法:用于删除字符串的头尾空白符,空白符包括:空格、制表符 tab、换行符等其他空白符等
给勾选框添加一个@change
事件,只要勾选状态发生改变,就会调用函数【带参数】handleCheck(todo.id)
,并给该回调函数传了一个参数todo.id
,即勾选状态发生改变的勾选框的ID。
<input type="checkbox" @change="handleCheck(todo.id)"/>
注意:props中:父给子组件传数据【非函数】;子组件给父组件传递数据【必须是函数、方法】
注意:props是只读的,vue底层会检测你对props的修改,如果进行了修改,控制台会报错,必须修改,就复制一份到data中,通过data去修改数据。
confirm()
函数用于提供确认功能,它首先显示给定的message参数所包含的信息,并提供两个可选择的回答“ok”和“cancel”,然后等待用户选择其中的一个。如果用户选择“ok”则返回true;否则,如若选择“cancel”则返回false。
@ 是 v-on: 的简写形式
filter()方法的使用:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组
computed与methods实现相比,内部有缓存机制(复用),效率更高,调试方便。
computed计算属性【比如:A】:第一次读取A属性时,就调用了getter,第2,3,4次读取A,就不会调用getter了
js中和数组有关的一个计数方法:reduce()
reduce(A,B) 方法会遍历数组的每一项,他接收两个参数:
data:{
todos:[
{id:'001',title:'练功夫',done:true},
{id:'002',title:'睡觉',done:false},
{id:'003',title:'打豆豆',done:true}
],
},
computed:{
doneTotal(){
//此处使用reduce方法做条件统计
const x = this.todos.reduce(
// todo:遍历数组todos中的每一个元素
// 比如:数组遍历时,把数组todos中的每一个元素分别赋值给todo【包含id='001'】
(pre,todo)=>{
console.log('@',pre,todo)
return pre + (todo.done ? 1 : 0)
}
,0)
//这个0:统计的初始值==》reduce(第一个参数,第二个参数:0)
return x
},
}
//简写
doneTotal(){
return this.todos.reduce((pre,todo)=>pre + (todo.done ? 1 : 0),0)
}
Array.forEach()
:forEach到底会不会改变原数组Array?
总结:
组件化编码流程:
(1).拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
(2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
1).一个组件在用:放在组件自身即可。
2). 一些组件在用:放在他们共同的父组件上(状态提升)。
(3).实现交互:从绑定事件开始。
props适用于:
(1).父组件 ==> 子组件 通信
(2).子组件 ==> 父组件 通信(要求父先给子一个函数)
使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。
App.vue文件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<TodoHeader :addTodo="addTodo"/>
<TodoList
:todos="todos"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"
/>
<TodoFooter
:todos="todos"
:checkAllTodo="checkAllTodo"
:clearAllTodo="clearAllTodo"
/>
div>
div>
div>
template>
<script>
//引入App的子组件
import TodoHeader from './components/TodoHeader'
import TodoFooter from './components/TodoFooter'
import TodoList from './components/TodoList'
export default {
name:'App',
components: { TodoHeader,TodoFooter,TodoList },
data() {
return {
//由于todos是TodoHeader组件和TodoFooter组件都在使用,所以放在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((todo) => {
if(todo.id === id) todo.done = !todo.done
})
},
// 删除一个todo
// deleteTodo里面的id指的是:点击事件对应的id
deleteTodo(id){
// todo:this.todos这个数组的每一个元素
this.todos=this.todos.filter(
(todo)=>{
return todo.id != id
}
)
},
//全选or取消全选
checkAllTodo(done){
this.todos.forEach(todo => todo.done = done)
},
// 清除所有已经完成的todo
clearAllTodo(){
this.todos= this.todos.filter(todo =>{
return todo.done == false
// 或者换成 return !todo.done
}
)
},
},
}
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;
/* 上下外边距为0,左右自动,实际效果为左右居中*/
margin: 0 auto;
}
/* 后代选择器(包含选择器),选择到的是todo-container下面的所有后代todo-wrap */
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #67dbd1;
border-radius: 5px;
}
style>
TodoHeader.vue文件
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add" v-model="title"/>
div>
template>
<script>
// 引入nanoid库生成ID号
import { nanoid } from 'nanoid'
export default {
name: 'TodoHeader',
//接收从App组件【父组件】传递过来的addTodo方法
props:['addTodo'],
data() {
return {
title: '',
}
},
methods: {
add(){
// 如果输入框里为空,就跳过下面的代码,并弹窗
if (!this.title.trim()) return alert('请输入值')
//将用户的输入包装成一个todo对象
const todoObj={id:nanoid(),title:this.title,done:false}
//通知App组件去添加一个todo对象
this.addTodo(todoObj)
//清空输入
this.title = ''
}
},
};
script>
<style scoped>
/* 头部样式设置 */
/* 后代选择器(包含选择器),选择到的是todo-header下面的所有后代input */
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
/* 内边距:上下4px,左右7px */
padding: 4px 7px;
}
/* :focus获得焦点,并设置其新样式:例如:用户单击一个input输入框获取焦点,然后这个input输入框的边框样式就会发生改变,和其他的输入框区别开来,表明已被选中。 */
.todo-header input:focus {
/* outline 与 border 相似,不同之处在于 outline 在整个元素周围画了一条线;它不能像 border 那样,指定在元素的一个面上设置轮廓,也就是不能单独设置顶部轮廓、右侧轮廓、底部轮廓或左侧轮廓。 */
outline: none;
/* 定义边框的颜色 */
border-color: rgba(82, 168, 236, 0.8);
/* boxShadow 属性把一个或多个下拉阴影添加到框上 */
/* 设置inset:内部阴影,不设置inset:外部阴影 */
/* 【0 0】:不设置X轴与Y轴偏移量 */
/* 第三个值【如10px,8px】:设置值阴影模糊半径为15px */
box-shadow: inset 0 0 10px rgba(124, 56, 207, 0.075), 0 0 8px rgba(224, 58, 17, 0.6);
background-color: bisque;
}
style>
TodoList.vue
<template>
<ul class="todo-main">
<TodoItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"
/>
ul>
template>
<script>
import TodoItem from './TodoItem'
export default {
name: 'TodoList',
components:{TodoItem},
//声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件TodoItem用的
props: ['todos','checkTodo','deleteTodo']
};
script>
<style scoped>
/*main*/
.todo-main {
/* 左外边距:0px 【盒子贴着盒子】*/
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>
TodoItem.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>
li>
template>
<script>
export default {
name: 'TodoItem',
// 声明接受从别的组件中的todoObj对象,todo、checkTodo、deleteTodo
props: ['todo','checkTodo','deleteTodo'],
methods: {
//勾选or取消勾选【别弄混了:这里的id其实就是上面change事件中的todo.id】
handleCheck(id){
//change事件触发后,通知App组件将对应的todo对象的done值取反
this.checkTodo(id)
},
//删除
handleDelete(id){
if (confirm('Are you sure you want to delete?')) {
//点击事件触发后通知App组件将对应的todo对象删除
this.deleteTodo(id)
}
}
},
};
script>
<style scoped>
/*item*/
li {
/* ul无序列表 ol有序列表*/
/* 列表前面无标记 */
list-style: none;
/* height定义了一个li盒子的高度 */
height: 36px;
/* 行高:指的是文字占有的实际高度 */
line-height: 36px;
/* 当height和line-height相等时,即盒子的高度和行高一样,内容上下居中 */
padding: 0 5px;
/* 边框底部:1px的实心线 颜色*/
border-bottom: 1px solid #c0abc3;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代label */
li label {
/* 左对齐浮动【元素一旦浮动就会脱离文档流(不占位,漂浮)】 */
float: left;
/* 鼠标放在label元素上时变成小小手 */
cursor: pointer;
}
li label input {
/* 垂直居中 */
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代button */
li button {
/* 向右浮动 */
float: right;
/* 不为被隐藏的对象保留其物理空间,即该对象在页面上彻底消失,通俗来说就是看不见也摸不到。 */
display: none;
/* 上边距为3px */
margin-top: 3px;
}
li:before {
/* initial:它将属性设置为其默认值。 */
content: initial;
}
/* 结构伪类选择器 选择最后一个li元素 */
li:last-child {
/* 边框底部没有线 */
border-bottom: none;
}
li:hover{
background-color: #ddd;
}
/* 鼠标移动到该元素上时,将button按钮显示出来 */
li:hover button{
/* display:block将元素显示为块级元素 */
display: block;
}
style>
TodoFooter.vue
<template>
<div class="todo-footer" v-show="total">
<label>
<input type="checkbox" v-model="isAll"/>
label>
<span>
<span>已完成{{ doneTotal }}span> / 全部{{ total }}
span>
<button class="btn btn-danger" @click="clearAllDone">清除已完成任务button>
div>
template>
<script>
export default {
name: 'TodoFooter',
props: ['todos','checkAllTodo','clearAllTodo'],
computed:{
//总数
total(){
return this.todos.length
},
// 已完成数
doneTotal(){
//此处使用reduce方法做条件统计
/* return this.todos.reduce(
// todo:遍历数组todos中的每一个元素
// 比如:数组遍历时,把数组todos中的每一个元素分别赋值给todo【包含id='001'】
(pre,todo)=>{
// console.log('@',pre,todo)
return pre + (todo.done ? 1 : 0)
}
,0) */
//简写
return this.todos.reduce((pre,todo)=>pre + (todo.done ? 1 : 0),0)
},
//控制全选框
/* isAll(){
//计算属性简写:isAll属性,只能被读取,不能被修改
return this.total === this.doneTotal && this.total>0
} */
isAll:{
//get有什么作用?当有人读取isAll时,get就会被调用,且返回值就作为isAll的值
//get什么时候调用?1.初次读取isAll时。2.所依赖的数据发生变化时。
get(){
//全选框是否勾选 【&&:且】
return this.total === this.doneTotal && this.total>0
},
//set什么时候调用? 当isAll被修改时。
// value就是:v-model绑定的值false【未勾选】 or true【勾选】
set(value){
console.log(value)
this.checkAllTodo(value)
}
},
},
methods: {
/* checkAll(e){
console.log(e.target.checked);
// 拿到的是全选或者全不选的布尔值
this.checkAllTodo(e.target.checked)
} */
// 清空所有已完成
clearAllDone(){
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>