文章目录
- 1 效果
- 2 功能拆分和静态组件
- 3 动态初始化
- 4 功能实现
- 4.1 添加todo
- 4.2 鼠标移入效果和删除todo
- 4.3 todo选中和取消选中
- 4.4 底部统计和删除已完成
- 5 TodoList案例总结
- 结语
1 效果
通过前面学习React基础和create-react-app脚手架,下面我们做一个经典的入门案例TodoList。效果如下图1-1所示:
2 功能拆分和静态组件
首先对页面进行拆分,根据功能相关拆分如下图2-1所示组件:
拆分为4个组件如下:
- Header组件:对应输入框部分
- List:对应待办事项列表
- Item:对应每一个待办事项
- 包括复选框,文本,鼠标悬浮效果,删除操作等
- Item:对应每一个待办事项
- Footer:底部,包括复选框,因完成和总数统计即清楚已完成功能
静态页面如下:
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>React App</title><link rel="stylesheet" href="index.css">
</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>xxxxx</span></label><button class="btn btn-danger" style="display:none">删除</button></li><li><label><input type="checkbox"/><span>yyyy</span></label><button class="btn btn-danger" style="display:none">删除</button></li></ul><div class="todo-footer"><label><input type="checkbox"/></label><span><span>已完成0</span> / 全部2</span><button class="btn btn-danger">清除已完成任务</button></div></div></div>
</div></body>
</html>
静态样式如下:
/*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;
}/*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);
}/*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;
}
/*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;
}/*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;
}
- 静态组件
create-react-app脚手架初始化项目,把不需要的去除,建立相应的组件,项目文件结构如下图3-1所示:
静态组件构建步骤:
- 把div#root之下的所有结构放入App组件
- 修改类名和style
- class修改为className
- style=“”,修改为jsx语法
- 把样式放入App.css,查看页面效果。
- 拆分对应的4个组件和样式,在App中引入对应3个子组件,List组件引入Item子组件。
App.jsx代码2-1如下图所示,其他组件不做展示:
// 创建外壳组件App
import { Component } from 'react'
import Header from './components/Header';
import List from './components/List';
import Footer from './components/Footer';
import './App.css'class App extends Component {render() {const { todos } = this.statereturn (<div className="todo-container"><div className="todo-wrap"><Header /><List /><Footer /></div></div>)}}export default App
3 动态初始化
首先我们来看下该应用场景下数据如何传递,如下图3-1所示:
我们的数据todos待办事项列表为同一份数据,而这三个组件给兄弟组件,想要进行数据交互,目前为止我们可以把该数据放置在它们共同的父组件App的状态中。
App.jsx组件state初始化代码3-1如下所示:
/*** 待办事项列表*/state = {todos: [{ id: '001', name: '晨练', done: false },{ id: '002', name: '学习spring', done: true },{ id: '003', name: '逛街', done: true },{ id: '004', name: '晚饭', done: false },]}
第一步在List中展示待办事项,通过App组件props传递代码如下3-2所示:
render() {const { todos } = this.statereturn (<div className="todo-container"><div className="todo-wrap"><Header /><List todos={todos}/><Footer /></div></div>)}
第二步 List中循环展示Item,List.jsx代码3-3如下所示:
import React, { Component } from 'react'
import Item from '../Item';
import './index.css'export default class List extends Component {render() {const {todos, updateTodo, deleteTodo} = this.propsreturn (<ul className="todo-main">{ todos.map(todo => {return <Item {...todo} key={todo.id}/>})}</ul>)}
}
第三部 Item 展示具体的信息,Item.jsx代码3-4如下:
import React, { Component } from 'react'
import './index.css'export default class Item extends Component {render() {const { name, done } = this.propsreturn (<li><label><input type="checkbox" defaultChecked={done}/><span>{name}</span></label><button className="btn btn-danger" style={{ display: 'none' }}>删除</button></li>)}
}
4 功能实现
4.1 添加todo
Header.jsx初始代码如下4.1-1所示:
import React, { Component } from 'react'
import './index.css'export default class Header extends Component {render() {return (<div className="todo-header"><input type="text" placeholder="请输入你的任务名称,按回车键确认" /></div>)}
}
添加Todo待办事项,流程:
- 输入框输入待办事项名称
- 敲击键盘回车键,完成Todo添加
我们在Header组件需要做的事情:
- 输入框添加change事件监听;
- change事件监听回调执行接收输入框数据,判断如果是回车键,把数据传入App组件;
- App根据接收的数据,更新state,驱动页面更新。
具体实现:
-
输入框添加change事件回调,代码4.1-2:
<input type="text" placeholder="请输入你的任务名称,按回车键确认" onKeyUp={this.handleKeyUp} />
-
回调函数代码如下4.1-3:
/*** 键盘事件回调* @param {object} event * @returns */handleKeyUp = (event) => {const {keyCode, target} = event// 判断输入enter获取值if (keyCode !== 13) {return}// 校验数据,非空const val = target.value.trim() || target.valueif (!val) {alert('待做事项不能为空!')return}// 子组件给父组件传值,通过函数调用// 构建todo对象const todo = {id: nanoid(), name: val, done: false}this.props.addTodo(todo)// 清空输入框target.value = ''}
- 父子传值,父组件给子组件传值通过props传递;子组件给父组件传值,子组件通过调用调用props接收父组件传递的函数的参数,传递给父组件。
- 我们直接传递todo对象,待办事项id通过nanoid库生成。
-
App组件接收todo对象,更新state代码4.1-4:
/*** 添加待办事项* @param {*} todo 待办事项*/ addTodo = (todo) => {this.setState({ todos: [todo, ...this.state.todos] }) } <Header addTodo={this.addTodo} />
4.2 鼠标移入效果和删除todo
鼠标移入直观效果,Item高亮同时显示删除按钮,可以删除当前Item项。
流程如下:
- 鼠标移入,当前待办事项高亮,同时显示删除按钮;
- 点击删除,给出提示;点击”确定“,删除当前待办事项,取消啥也不做。
Item组件需要操作:
- li标签样式根据鼠标移入状态改变;
- li标签添加鼠标移入事件和鼠标移出事件,同一个回调函数;
- 通过回调函数接收的参数修改鼠标状态;
- 删除按钮监听鼠标点击事件;
- 回调函数获取todo的id向上传递父组件List,List组件向上传递App组件。App组件根据接收的todo的id删除相应的todos列表中的todo对象。
具体实现:
-
Item.jsx代码4.2-1:
import React, { Component } from 'react' import './index.css'export default class Item extends Component {/*** 鼠标是否悬浮*/state = { mouse: false // 鼠标进入true;鼠标离开false}/*** 鼠标进入离开标志* @param {boolean} flag 鼠标进入true;鼠标离开false*/handleMouse = (flag) => {return () => {this.setState({ mouse: flag })}}/*** 处理复选框选中状态* 状态改变,获取id和状态值向上传递* @param {strig} id * @returns 回调函数*/handleCheck = (id) => {return (e) => {// 获取checkbox选中状态const done = e.target.checked// id,done向上传递const {updateTodo} = this.propsupdateTodo(id, done)}}/*** 删除一个todu回调* @param {string} id */handleDelete = (id) => {// 校验// 原生方法需要指定window前缀if (window.confirm("您确定要删除吗?")) {// 向上传递idthis.props.deleteTodo(id)}}render() {const { id, name, done } = this.propsconst { mouse } = this.statereturn (<li style={{ backgroundColor: mouse ? '#ddd' : '#fff' }} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}><label><input type="checkbox" checked={done} onChange={this.handleCheck(id)}/><span>{name}</span></label><button className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }} onClick={() => this.handleDelete(id)}>删除</button></li>)} }
-
List组件代码4.2-2:
<ul className="todo-main">{ todos.map(todo => {return <Item {...todo} key={todo.id} deleteTodo={deleteTodo}/>})} </ul>
-
App组件代码4.2-3:
/*** 根据id删除todo* @param {string} id */deleteTodo = (id) => {// 获取todo列表const { todos } = this.state// 设置todo对象的done 为trueconst newTodos = todos.filter((todo) => {return todo.id !== id})// 更新状态this.setState({ todos: newTodos })}<List todos={todos} deleteTodo={this.deleteTodo} />
4.3 todo选中和取消选中
效果:
- 点击待办事项复选框选中;再次点击取消选中。
组件操作:
- Item组件复选框监听change事件;
- 回调函数向上传递todo对象id,done属性;
- App组件更加传递的id修改相应todo对象的done属性。
具体实现:
-
Item组件代码4.3-1:
/*** 处理复选框选中状态* 状态改变,获取id和状态值向上传递* @param {strig} id * @returns 回调函数*/handleCheck = (id) => {return (e) => {// 获取checkbox选中状态const done = e.target.checked// id,done向上传递const {updateTodo} = this.propsupdateTodo(id, done)}}<input type="checkbox" checked={done} onChange={this.handleCheck(id)}/>
- 这里我们输入框属性由defaultChecked改回checked,想想为什么?
-
List组件代码4.3-2:
<ul className="todo-main">{ todos.map(todo => {return <Item {...todo} key={todo.id} updateTodo={updateTodo} deleteTodo={deleteTodo}/>})} </ul>
-
App组件代码4.3-3:
/*** 更新待办事项* @param {string} id 待办事项唯一标识* @param {boolean} done 待办事项是否已完成*/updateTodo = (id, done) => {console.log(id, '==', done);// 获取待办事项列表const { todos } = this.state// 根据id查找待办事项,修改是否已完成const newTodos = todos.map(todo => {// 根据id判断是否目标待办事项if (todo.id === id) {return { ...todo, done }} else {return todo}})// 更新状态this.setState({ todos: newTodos })}<List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo} />
4.4 底部统计和删除已完成
效果:
- 底部显示已完成待办事项数量和总数量,更加上面选中或者取消实时改变;
- 当上面全部选中后,下面复选框也选中;否则不选中;
- 点击复选框,全选;再次点击全取消选中;
- 点击删除已完成,删除已完成待办事项;
组件操作:
- 已完成通过统计todos列表中done为true的项;总数量为数组长度;
- 复选框属性checked初始值通过判断已完成数量和总数量算法相等;复选框监听change事件,回调函数向上传递复选框的值;
- 复选框全选,遍历todos,修改done为true;复选框全不选,遍历todos,修改每个todo的done值为false;
- 删除按钮监听鼠标点击事件,回调函数向上传递;
具体实现:
-
Footer组件代码4.4-1:
import React, { Component } from 'react' import './index.css'export default class Footer extends Component {/*** 选中全部复选框回调*/handleAllCheck = (event) => {this.props.checkAllTodo(event.target.checked)}/*** 清除所有已完成回调*/handleAllDone = () => {this.props.deleteAllDone()}render() {const { todos } = this.props// 1 已完成总数const doneCount = todos.reduce((pre, current) => pre + (current.done ? 1 : 0), 0)// 2 总数const total = todos.lengthreturn (<div className="todo-footer"><label><input type="checkbox" onChange={this.handleAllCheck} checked={doneCount === total && total !== 0} /></label><span><span>已完成{doneCount}</span> / 全部{total}</span><button className="btn btn-danger" onClick={this.handleAllDone}>清除已完成任务</button></div>)} }
-
App组件代码4.4-2:
/*** 选中全部复选框*/checkAllTodo = (done) => {// 获取todo列表const { todos } = this.state// const newTodos = todos.map((todo) => {return { ...todo, done }})// 更新状态this.setState({ todos: newTodos })}/*** 删除已完成*/deleteAllDone = () => {// 获取todo列表const {todos} = this.state// 设置todo对象的done 为trueconst newTodos = todos.filter((todo) => {return !todo.done})// 更新状态this.setState({todos: newTodos})}<Footer todos={todos} checkAllTodo={this.checkAllTodo} deleteAllDone={this.deleteAllDone}/>
5 TodoList案例总结
- 拆分组件和实现静态组件
- 注意className和style些饭
- 动态初始化列表,如何确定数据放在那些组件的state中?
- 某个组件使用,放在其自身的state中
- 某些组件使用,放在其共同的父组件中
- 父子组件通信
- 父组件给子组件传值:通过props传递
- 子组件给父组件传值:父组件通过props给子组件传递一个函数,子组件执行该函数通过参数传递。
- 注意defaultChecked和checked区别
- defaultChecked:初始状态,初始生效;
- checked:非初始状态,如需修改,需要通过监听change事件修改。
- 状态在哪里,操作状态的操作放置在哪里。
关于对传递数据必要性和类型限制这里不在详述,完整的代码参考底部代码仓库。
结语
❓QQ:806797785
⭐️源代码仓库地址:https://github.com/gaogzhen/react-staging.git
参考:
[1]React视频教程[CP/OL].2020-12-15.p56-64.
[2]React官网[CP/OL].
[2]ChatGPT[CP/OL].