0302todolist案例-react应用-爱代码爱编程
文章目录
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.state
return (
<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.state
return (
<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.props
return (
<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.props
return (
<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.value if (!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.props updateTodo(id, done) } } /** * 删除一个todu回调 * @param {string} id */ handleDelete = (id) => { // 校验 // 原生方法需要指定window前缀 if (window.confirm("您确定要删除吗?")) { // 向上传递id this.props.deleteTodo(id) } } render() { const { id, name, done } = this.props const { mouse } = this.state return ( <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 为true const 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.props updateTodo(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.length return ( <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 为true const 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].