通过该项目熟悉,react的事件绑定、父子之间的数据传递,和分模块设计的理念,没有涉及到太多的东西,之前也写过一个todoLIst的项目相比上次知识点用的比较少,但是更加注重细节和规范,显示效果如下:
项目结构如下:
项目划分一个入口,四大组件分别:顶部的输入框、中间的list,list显示的item以及底部的footer,下面分别介绍。
一、package.json
{
"name": "my-app2",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
"nanoid": "^3.3.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
nanoid:用于生成唯一标识的id,相比UUID库更加轻量
二、App
1、App.js
App.js作为所以组件的父组件,所以将所需要的数组都定义在他里面,以便于子组件之间的相互交互。父组件向子组件传递数据使用props,子组件向父组件传递,使用方法绑定,下面的内容都会涉及到。
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 = {
todoList: [
{
id: '001',
name: '吃饭',
done: false
},
{
id: '002',
name: '睡觉',
done: true
}
]
}
render() {
const {todoList} = this.state
return (
<div className={'todo-wrapper'}>
{/*子组件Header向父组件传递数据,定义addTodo方法,子类this.props.addTodo()即可以调用父类的方法传递*/}
<Header addTodo={this.addTodo}/>
<List todoList={todoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer todoList={todoList} onFooterCheckAll={this.onFooterCheckAll} clearClick={this.clearClickAll}/>
</div>
);
}
/**
* 用户添加一个todo添加的对象是一个todo的对象
* @param todoObj
*/
addTodo = (todoObj) => {
//获取源todo
const {todoList} = this.state
//追加一个todo
const newTodo = [todoObj, ...todoList]
//更新状态
this.setState({
todoList: newTodo
})
}
/**
* 用于更新一个todo
* @param id
* @param done
*/
updateTodo = (id, done) => {
const {todoList} = this.state
let list = todoList.map((item) => {
if (item.id === id) {
return {...item, done}
} else {
return item
}
})
this.setState({
todoList: list
})
}
/**
* 删除按钮事件
*/
deleteTodo = (id) => {
const {todoList} = this.state
const list = todoList.filter((item) => {
return item.id !== id
})
this.setState({
todoList: list
})
}
/**
* 全部选中
*/
onFooterCheckAll = (done) => {
const {todoList} = this.state
const list = todoList.map(item => {
return {...item, done}
})
this.setState({
todoList: list
})
}
/**
* 清空
*/
clearClickAll = () => {
const {todoList} = this.state
const list = todoList.filter(item => {
return !item.done
})
this.setState({
todoList: list
})
}
}
2、App.css
.todo-wrapper {
margin-left: 30px;
}
ul, li {
padding: 0;
margin: 0;
list-style: none
}
三、Header
1、header/index.js
import React, {Component} from 'react';
import {nanoid} from "nanoid";
import './index.css'
export default class Header extends Component {
render() {
return (
<div className={'header-wrapper'}>
<input placeholder={'请输入任务名称,按回车键确认'}
type={'text'} onKeyDown={this.onKeyDown}/>
</div>
);
}
/**
* 监听键盘事件,当为13的时候是回车键
*/
onKeyDown = (event) => {
if (event.keyCode === 13) {
//过滤空数据
if (event.target.value.trim() === '') {
return
}
//生成对象,调用nanoid()生成ID
let list = {
id: nanoid(),
name: event.target.value,
done: false
}
this.props.addTodo(list)
//输入框置空
event.target.value = ''
}
}
}
2、header/index.css
.header-wrapper input {
margin-top: 30px;
width: 300px;
height: 32px;
font-size:14px;
text-indent: 10px;
}
四、List
1、list/index.js
import React, {Component} from 'react';
import Item from "../item";
export default class List extends Component {
constructor(props) {
super(props);
}
render() {
//不能这么写,错误的
//const {todoList} = this.props.todoList
const {todoList, updateTodo, deleteTodo} = this.props
return (
<ul>
{
todoList.map(item => {
return (
//{...item}表示解构数据,即将item的内容挂载在子类Item的props节点上
<Item key={item.id} {...item} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
)
})
}
</ul>
);
}
}
2、index.css
无
五、Item
1、item/index.js
import React, {Component} from 'react';
import './index.css'
export default class Item extends Component {
constructor(props) {
super(props);
this.state = {
mouse: false
}
}
render() {
//解构数据
const {mouse} = this.state
const {id, name, done} = this.props
return (
//onMouseEnter、onMouseLeave监听鼠标点击的事件,label表示扩大checkbox的点击范围
<li className={'item'}
style={{background: mouse ? '#ddd' : 'white'}}
onMouseEnter={() => this.onMouseEvent(true)}
onMouseLeave={() => this.onMouseEvent(false)}>
<label>
<input type={'checkbox'} checked={done}
onChange={(event) => this.onCheckChange(event, id)}/>
<span>{name}</span>
</label>
<button className={'item-button'} style={{display: mouse ? 'block' : 'none'}}
onClick={() => this.onDeleteClick(id)}>删除
</button>
</li>
);
}
/**
* 鼠标移动事件
* @param flag
*/
onMouseEvent = (flag) => {
this.setState({
mouse: flag
})
}
/**
* 选择框点击时间
*
* @param event
* @param id
*/
onCheckChange = (event, id) => {
this.props.updateTodo(id, event.target.checked)
}
/**
* 删除按钮事件
*/
onDeleteClick = (id) => {
//注意这里有个window,不写会报错
if (window.confirm('确定删除吗?')) {
this.props.deleteTodo(id)
}
}
}
2、item/index.css
.item {
width: 500px;
height: 100%;
display: block;
border: 1px solid gray;
padding: 5px;
border-radius: 5px;
background: white;
margin: 20px 0 0 0;
}
.item-button {
float: right;
}
.item :first-child {
margin-right: 10px;
}
六、Footer
1、footer/index.js
import React, {Component} from 'react';
import './index.css'
export default class Footer extends Component {
render() {
const {todoList} = this.props
//注意reduce的如何使用,怎么过滤数据
const doneCount = todoList.reduce((previousValue, currentValue) => {
return previousValue + (currentValue.done ? 1 : 0)
}, 0)
const total = todoList.length
return (
<div className={'footer-wrapper'}>
<label>
<input type='checkbox' className={'footer-input'}
checked={doneCount === total && total !== 0}
onChange={this.onFooterCheckAll}/>
<span>已完成{doneCount}/全部{total}</span>
<button style={{float: 'right'}} onClick={this.clearClickAll}>清除已完成任务</button>
</label>
</div>
);
}
/**
* 调用父类的方法传递数据
* @param event
*/
onFooterCheckAll = (event) => {
this.props.onFooterCheckAll(event.target.checked)
}
/**
* 调用父类的方法传递数据
* @param event
*/
clearClickAll = () => {
this.props.clearClick()
}
}
2、footer/index.css
.footer-input {
margin-right: 10px;
}
.footer-wrapper {
margin-top: 30px;
width: 500px;
}