React Todolist案例
效果展示
拆分组件
拆分为四个组件
header:负责输入新的任务。
main:任务展示区,负责列表展示每个任务。
item:任务展示区具体的每个任务,可以对单个任务进行操作。
footer:底部任务处理区,可以同时对多个任务进行处理。
具体实现
初学React,所以考虑到组件之间需要互相通信,所以选择将各组件都需要使用的信息放在各组件共同的父组件App组件上,以及将操作数据的方法都定义在App组件上。那么子组件怎么修改数据呢?且看下例:
当按下回车键,子组件header需要将输入的任务加入到任务列表中,因为添加任务到任务列表的事件在App组件定义,所以在使用Header组件时需要将该事件传给header组件
<Header addTodo={this.addTodo}/>
然后header组件就通过props拿到该事件,当按下回车键后先将拿到的任务包装成任务对象,再传递给拿到的添加任务函数,就可以完成添加任务。然后main组件拿到的任务列表数据改变就会重新渲染,将任务渲染到任务列表上。所以这里兄弟组件间的通信是由父组件App作为桥梁的。
// 键盘抬起事件
handleKeyUp = (event) => {
const { keyCode, target } = event;
if (keyCode !== 13) return;
if (target.value.trim() === '') {
alert('输入不能为空!')
return;
}
// 封装任务对象
const todo = { id: nanoid(), name: target.value, done: false }
// 调用父组件App中的方法,添加任务
this.props.addTodo(todo);
target.value = '';
}
App组件
App.jsx
import React, { Component } from 'react'
import Header from './components/Header';
import Footer from './components/Footer';
import List from './components/List';
import './App.css';
export default class App extends Component {
state = {
// 任务列表
todos: [
{ id: '001', name: '吃饭', done: true },
{ id: '002', name: '睡觉', done: true },
{ id: '003', name: '打代码', done: false },
{ id: '004', name: '逛街', done: false }
]
}
// 增加任务
addTodo = (todoObj) => {
const { todos } = this.state;
const newTodos = [todoObj, ...todos];
this.setState({ todos: newTodos });
}
// 更新任务选中状态
updateTodo = (id, checked) => {
const { todos } = this.state;
const newTodos = todos.map(item => {
if (item.id === id) {
item.done = checked;
}
return item;
})
this.setState({ todos: newTodos });
}
// 删除任务
deleteTodo = (id) => {
const { todos } = this.state;
const newTodos = todos.filter(item => {
return item.id !== id
})
this.setState({ todos: newTodos});
}
// 清除全部已选
deleteDone = () => {
const { todos } = this.state;
const newTodos = todos.filter(item => {
return item.done !== true;
})
this.setState({ todos: newTodos });
}
// 全选
ChooseDone = (done) => {
const { todos } = this.state;
const newTodos = todos.map(item => {
return { ...item, done};
})
this.setState({ todos: newTodos });
}
render() {
const { todos } = this.state;
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer todos={todos} ChooseDone={ this.ChooseDone } deleteDone={ this.deleteDone }/>
</div>
</div>
);
}
}
App.css
/*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组件
index.jsx
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { nanoid } from 'nanoid'
import './index.css'
export default class Header extends Component {
// 对接收的props进行:类型、必要性的限制
static propTypes = {
addTodo: PropTypes.func.isRequired
}
// 键盘抬起事件
handleKeyUp = (event) => {
const { keyCode, target } = event;
// 判断是不是回车键
if (keyCode !== 13) return;
// 输入为空提示
if (target.value.trim() === '') {
alert('输入不能为空!')
return;
}
// 封装任务对象
const todo = { id: nanoid(), name: target.value, done: false }
// 调用父组件App中的方法,添加任务
this.props.addTodo(todo);
// 重置输入框内容
target.value = '';
}
render() {
return (
<div className="todo-header">
<input onKeyUp={ this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认" />
</div>
)
}
}
index.css
/*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组件
index.jsx
import React, { Component } from 'react'
import Item from '../Item'
import PropTypes from 'prop-types'
import './index.css'
export default class List extends Component {
//对接收的props进行:类型、必要性的限制
static propTypes = {
todos:PropTypes.array.isRequired,
updateTodo:PropTypes.func.isRequired,
deleteTodo:PropTypes.func.isRequired,
}
render() {
const { todos, updateTodo, deleteTodo } = this.props;
return (
<ul className="todo-main">
{
todos.map(todo => {
return <Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
})
}
</ul>
)
}
}
index.css
/*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组件
index.jsx
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
state = {
// 鼠标移入移出标志
mouse: false
}
// 改变单个任务的选中条件
handleChangeCheck = (id) => {
return (event) => {
this.props.updateTodo(id, event.target.checked);
}
}
// 鼠标移入高亮
mouseChange(flag) {
return () => {
this.setState({ mouse: flag })
}
}
// 删除
handleDelete(id) {
return () => {
this.props.deleteTodo(id);
}
}
render() {
const { id, name, done } = this.props;
const { mouse } = this.state;
return (
<li style={{ backgroundColor: mouse ? '#ddd' : 'white' }} onMouseEnter={this.mouseChange(true)} onMouseLeave={this.mouseChange(false)}>
<label>
<input type="checkbox" checked={done} onChange={this.handleChangeCheck(id)}/>
<span>{name}</span>
</label>
<button className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }} onClick={this.handleDelete(id)}>删除</button>
</li>
)
}
}
/*item*/
li {
list-style: none;
height: 36px;
line-height: 31px;
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组件
index.jsx
import React from "react";
import './index.css'
// style={{display:mouse ? 'block': 'none'}}
export default class Footer extends React.Component{
// 清除全部已完成
handleDelDone = () => {
this.props.deleteDone();
}
// 全选
handleChooseDone = (event) => {
this.props.ChooseDone(event.target.checked);
}
render() {
const { todos } = this.props;
const doneNum = todos.reduce((pre, cur) => pre + (cur.done ? 1 : 0), 0)
const total = todos.length;
return (
<div className="todo-footer">
<label>
<input type="checkbox" onChange={this.handleChooseDone} checked={doneNum === total && total ? true : false}/>
</label>
<span>
<span>已完成{ doneNum }</span> / 全部{total}
</span>
<button className="btn btn-danger" onClick={this.handleDelDone} >清除已完成任务</button>
</div>
)
}
}
index.css
/*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;
}
总结
-
拆分组件、实现静态组件,注意:className、style的写法
-
动态初始化列表,如何确定将数据放在哪个组件的state中?
- 某个组件使用:放在其自身的state中
- 某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)
-
关于父子之间通信:
- 【父组件】给【子组件】传递数据:通过props传递
- 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
-
状态在哪里,操作状态的方法就在哪里
最近学React,发现它和Vue的很多地方都很相似,但也有很多区别,特别是JSX语法,现在我还不是很习惯OVO,还要继续努力啊。