React _ 通过 class 类组件 实现前端基础应用 todolist
前言
TodoList 案例在前端学习中挺重要的,从原生 JavaScript 的增删查改,到现在 React 的组件通信,都是一个不错的案例,这篇文章主要还原一下通过 React 实现 TodoList 的全部实际业务需求(增删改查)的实现步骤及组件通信等内容。
拆分组件
首先第一步需要做的是将这个页面拆分成几个组件
-
首先顶部的输入框,可以完成添加项目的功能,可以拆分成一个 Header 组件
-
中间部分可以实现一个渲染列表的功能,可以拆分成一个 List 组件
-
在这部分里面,每一个待办事项都可以拆分成一个 Item 组件
-
最后底部显示当前完成状态的部分,可以拆分成一个 Footer 组件
项目目录
todolist
├─ package.json
├─ public
│ ├─ favicon.ico
│ └─ index.html
├─ src
│ ├─ App.css
│ ├─ App.jsx
│ ├─ Components
│ │ ├─ Footer
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ ├─ Header
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ ├─ item
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ └─ List
│ │ ├─ index.css
│ │ └─ index.jsx
│ └─ index.js
└─ yarn.lock
实现动态组件
-
动态展示列表 我们目前实现的列表项是固定的,我们需要它通过状态来维护,而不是通过组件标签来维护 首先我们知道,父子之间传递参数,可以通过
state和props实现 我们通过在父组件也就是App.jsx中设置状态//初始化状态 state = { todos:[ {id:'001',name:'吃饭',done:true}, {id:'002',name:'睡觉',done:true}, {id:'003',name:'打代码',done:false}, {id:'004',name:'逛街',done:false} ] }我们将 state 传递给 list 组件
<List todos={todos}/>在
List组件中就能通过props来获取到todos我们通过解构取出
todos,再通过map遍历渲染Item数量const { todos } = this.props; render() { const { todos} = this.props; return ( <ul className="todo-main"> {todos.map((todo) => { return ( <Item key={todo.id} {...todo} /> ); })} </ul> ); }
同时由于我们的数据渲染最终是在 Item 组件中完成的,所以我们需要将数据传递给 Item 组件 这里有两个注意点
-
关于
key的作用在 diff 算法的文章中已经有讲过了,需要满足唯一性 -
这里采用了简写形式
{...todo},这使得代码更加简洁,它代表的意思是
id = {todo.id} name = {todo.name} done = {todo.done}
在Item组件中取出props即可使用
const { id, name, done } = this.props
这样我们更改APP.jsx文件中的state就能驱动着Item组件的更新,
每个 Item 就是一个li
包含 input 为 chexkbox 的所选框 + todo 事项 + 删除按钮
return (
<li
style={{ backgroundColor: mouse ? "#ddd" : "white" }}
onMouseEnter={this.handleMouse(true)}
onMouseLeave={this.handleMouse(false)}
>
<label>
<input
type="checkbox"
checked={done}
/>
<span>{name}</span>
</label>
<button
className="btn btn-danger"
style={{ display: mouse ? "block" : "none" }}
>
删除
</button>
</li>
);
添加功能
首先我们需要在 Header 组件中,绑定键盘事件,判断按下的是否为回车,如果为回车,则将当前输入框中的内容传递给 APP 组件
因为,在目前的学习知识中,Header 组件和渲染组件 List 属于兄弟组件,没有办法进行直接的数据传递,因此可以将数据传递给 APP 再由 APP 转发给 List。
// Header/index.jsx
handleKeyUp = (event) => {
// 结构赋值获取 keyCode,target
const { keyCode, target } = event
// 判断是不是回车
if (keyCode !== 13) return
if(target.value.trim() === '') {
alert('输入不能为空')
}
// 准备一个todo对象
const todoObj = { id: nanoid(), name: target.value, done: false }
// 传递给 app
this.props.addTodo(todoObj)
// 清空
target.value = ''
}
Header 怎么传递给 App组件 ,App组件然后怎么传递 List 组件呢
// App 组件中 <Header> 标签传递给List 组件一个方法 addTodo
<Header addTodo={this.addTodo}/>
// Header 组件中 就可以使用 props 传递过来的 addTodo方法 将数据 todoObj 以参数形式传递给父组件
this.props.addTodo(todoObj);
// 父组件 App 组件就可以 拿到传递过来的 todoObj
//addTodo用于添加一个todo,接收的参数是 todo 对象
addTodo = (todoObj)=>{
// 获取原 todos
const {todos} = this.state
//追加一个todo
const newTodos = [todoObj,...todos]
//更新状态
this.setState({todos:newTodos})
}
实现鼠标悬浮效果
首先是鼠标移入时的变色效果 我的逻辑是,通过一个状态来维护是否鼠标移入,比如用一个 mouse 变量,值给 false 当鼠标移入时,重新设定状态为 true 当鼠标移出时设为 false ,然后我们只需要在 style 中用mouse 去设定样式即可
state = { mouse: false } // 标识鼠标移入,移出
给元素绑定上鼠标移入,移出事件
<li
onMouseEnter={this.handleMouse(true)}
onMouseLeave={this.handleMouse(false)} >
<li/>
当鼠标移入时,会触发onMouseEnter事件,调用handleMouse事件传入参数true表示鼠标进入,更新组件状态
handleMouse = flag => {
return () => {
this.setState({ mouse: flag })
}
}
再在li身上添加由mouse控制的背景颜色
style={{ backgroundColor: this.state.mouse ? '#ddd' : 'white' }}
同时通过mouse来控制删除按钮的显示和隐藏,做法和上面一样
<button
className="btn btn-danger"
style={{ display: mouse ? "block" : "none" }}
>
删除
</button>
复选框状态维护
我们需要将当前复选框的状态,维护到 state 当中 我们的思路是 在复选框中添加一个 onChange 事件来进行数据的传递,当事件触发时我们执行 handleCheck 函数,这个函数可以向 App 组件中传递参数,这样再在 App 中改变状态即可 首先绑定事件
// Item/index.jsx
<input
type="checkbox"
defaultChecked={done}
onChange={this.handleCheck(id)}
/>
事件回调 就是调用父组件的方法 updateTodo , 并将 id 和 复选框的值 event.target.checked 传递给父组件的父组件 App
handleCheck = (id) => {
return (event) => {
this.props.updateTodo(id, event.target.checked)
}
}
父组件 List 再传递给 App 组件
<Item
key={todo.id}
{...todo}
updateTodo={updateTodo}
/>
然后 App 组件中
<List
todos={todos}
updateTodo={this.updateTodo}/>
父组件中 拿取 传递过来的数据 进行状态更新及页面 展示
//updateTodo用于更新一个todo对象
updateTodo = (id,done)=>{
//获取状态中的todos
const {todos} = this.state
//匹配处理数据
const newTodos = todos.map((todoObj)=>{
if(todoObj.id === id) return {...todoObj,done}
else return todoObj
})
this.setState({todos:newTodos})
}
这里更改的方式是 { ...todoObj, done },首先会展开 todoObj 的每一项,再对 done 属性做覆盖
删除
在 Item 组件上的按钮绑定点击事件,然后传入被点击事项的 id 值,通过 props 将它传递给父元素 List ,再通过在 List 中绑定一个 App 组件中的删除回调,将 id 传递给 App 来改变 state 首先我们先编写 点击事件 调用 props 传递的事件 deleteTodo 将 id 传递给父组件 List
// Item/index.jsx
handleDelete = (id) => {
this.props.deleteTodo(id)
}
删除按钮添加事件
<button onClick={() => this.handleDelete(id)} className="btn btn-danger" style={{ display: mouse ? "block" : "none" }} > 删除 </button>
List 组件
<Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo} />
App 组件 给子组件传递 事件 deleteTodo
<List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
App 组件 deleteTodo 拿取 List 组件传递过来的数据 id 进行删除 及展示
//deleteTodo用于删除一个todo对象 deleteTodo = (id)=>{ //获取原来的todos const {todos} = this.state //删除指定id的todo对象 const newTodos = todos.filter((todoObj)=>{ return todoObj.id !== id }) //更新状态 this.setState({todos:newTodos}) }
获取已完成数量
我们在 App 中向 Footer 组件传递 todos 数据,再去统计数据
<Footer todos={todos} />
Footer 组件中解构todos
const { todos } = this.props;
统计 done 为 true 的个数
//已完成的个数 const doneCount = todos.reduce((pre, todo) => { return pre + (todo.done ? 1 : 0) }, 0) //总数 const total = todos.length;
再渲染数据即可
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
全选
首先我们需要在按钮上绑定事件,由于子组件需要改变父组件的状态,所以我们的操作和之前的一样,先绑定事件,再在 App 中传一个函数个 Footer ,再在 Footer 中调用这个函数并传入参数即可
这里需要特别注意的是 defaulChecked 只有第一次会起作用,所以我们需要将前面写的改成 checked 添加 onChange 事件即可 首先我们先在 App 中给 Footer 传入一个函数 checkAllTodo
//App 组件 <Footer todos={todos} checkAllTodo={this.checkAllTodo} />
事件 checkAllTodo 将所有的父组件传递过来的数据 done 全部更新
//checkAllTodo用于全选 checkAllTodo = (done)=>{ //获取原来的todos const {todos} = this.state //加工数据 const newTodos = todos.map((todoObj)=>{ return {...todoObj,done} }) //更新状态 this.setState({todos:newTodos}) }
Footer 组件 需要排除总数为0 时的干扰
<input type="checkbox" onChange={this.handleCheckAll} checked={doneCount === total && total !== 0 ? true : false} />
事件 handleCheckAl 执行父组件传递过来的方法,并传参
//全选checkbox的回调 handleCheckAll = (event) => { this.props.checkAllTodo(event.target.checked); };
删除全部
首先在 Footer 组件中调用传来的函数,在 App 中定义函数,过滤掉 done 为 true 的,再更新状态即可
// App.jsx clearAllDone = () => { const { todos } = this.state const newTodos = todos.filter((todoObj) => { return todoObj.done !== true }) this.setState({ todos: newTodos }) }
App 组件中 传递 事件 给子组件 Footer
<Footer todos={todos} checkAllTodo={this.checkAllTodo} clearAllDone={this.clearAllDone} />
<button onClick={this.handleClearAllDone} className="btn btn-danger"> 清除已完成任务 </button>
Footer 组件调用 props 传递过来的事件clearAllDone
//清除已完成任务的回调 handleClearAllDone = () => { this.props.clearAllDone(); };

浙公网安备 33010602011771号