一个简单的待办事项列表-React实现 | 青训营

132 阅读4分钟

青训营学习笔记

01 代办事项(TodoList)简述

TodoList案例是前端经典案例之一,代办事项(TodoList)案例通常由一个输入框和一个列表组成。主要的功能是用户可以在输入框中输入待办事项,并点击提交按钮将其添加到列表中。用户还可以标记已完成的任务、删除任务以及对任务进行编辑等操作。这个案例可以帮助开发者巩固对HTML、CSS和JavaScript的理解,并学习如何操作DOM元素、处理用户输入以及进行状态管理等。在React的学习中,通过TodoList案例,我学到了很多React设计思路,在此记录一下React的待办事项案例学习过程。

02 实现静态页面

create-react-app中编写代码和在vue-cli中是很不相同的,对于页面原型的style样式的CSS代码便不再着重记录。

首先,App.js中有整体组件代码布局:

import React, { Component } from 'react'
import './App.css'
import Header from './component/Header'
import List from './component/List'
import Footer from './component/Footer'

export default class App extends Component {
  render() {
    return (
      <div className='todo-container'>
        <div className='todo-wrap'>

            <Header/>

            <List/>

            <Footer/>

        </div>
      </div>
    )
  }
}

在代码中,也可以看出组件的拆分思路,本次案例代码中,组件拆分情况如下,可供参考:

在App中初始化状态(state):

  state = {
    todos:[
      {id:'001', name:'吃饭', done:false},
      {id:'002', name:'睡觉', done:true},
      {id:'003', name:'打豆豆', done:true},
      {id:'004', name:'敲代码', done:false},
      {id:'005', name:'写论文', done:true},
    ]
  }

03 Header组件功能实现

import React, { Component } from 'react'
import  PropTypes  from 'prop-types';
import { nanoid } from 'nanoid';

export default class Header extends Component {

  static propTypes = {
    addTodo: PropTypes.func.isRequired
  }

  handleKeyUp = (event) => {
    const {key, target} = event;
    if(key !== 'Enter') return
    if(target.value.trim() === ''){
      alert('输入不能为空')
      return
    }
    //准备好一个要添加的todo对象
    const todoObj = {id:nanoid(), name:target.value, done:false};
    //向父组件App传递todoObj
    this.props.addTodo(todoObj);
    target.value = '';
  }

  render() {
    return (
        <div className='todo-header'>
            <input onKeyUp={this.handleKeyUp} type="text" placeholder='请输入任务名称,按回车键确认' />
        </div>
    )
  }
}
  • 在此组件中,由于creat-react-app没有进行props限制的前置规定,所以在对传入的prop进行限制的时候,需引入名为props-type的库,执行命令:yarn add prop-types

  • 在Header组件中,需要实现添加一个todo的功能,但受限于在书写案例时,没有服务器来为我们提供新的todo的ID,因此,使用nanoid进行ID的生成。

  • 同时,准备好一个要添加的todo对象,并向父组件传递一个todo对象,通过App父组件addTodo()函数定义来实现添加一个代办事项:

      addTodo = (todoObj) => {
        //获取原todos
        const {todos} = this.state;
        //追加一个todo
        const newTodos = [todoObj,...todos]
        //更新状态
        this.setState({todos:newTodos})  
      }
    

04 List组件功能实现

import React, { Component } from 'react'
import PropTypes  from 'prop-types';
import Item from '../Item'

export default class List extends Component {

    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>
    )
  }
}
  • 在List组件中,同样对传入的props进行了类型和必要性的限制:
    • .array.isRequired就是要求传入的类型为数组,且必须传入值,不能为空
    • .func即传入的数据类型为函数
  • 在对Item组件进行展示时,使用了...JS中的展开运算符

05 Item组件的功能实现

import React, { Component } from 'react'

export default class Item extends Component {

  state = {
    mouse:false
  }

  //鼠标移入移出的回调
  handleMouse = (flag) => {
    return () => {
      this.setState({mouse:flag});
    }
  }

  handleCheck = (id) => {
    return (event) => {
      this.props.updateTodo(id,event.target.checked);
    }
  }

  //删除一个todo的回调
  handleDelete = (id) => {
    if(window.confirm('确定删除吗?')){
      this.props.deleteTodo(id);
    }
  }

  render() {
    const {id, name, done} = this.props;
    const {mouse} = this.state;
    return (
      <li style={{backgroundColor:mouse ? '#ddd' : 'white'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
        <label>
            <input type='checkbox' checked={done} onChange={this.handleCheck(id)}/>
            <span>{name}</span>
        </label>
        <button onClick={ ()=>{this.handleDelete(id)} } className='btn btn-danger' style={{display:mouse ? 'block' : 'none'}}>删除</button>
      </li>

    )
  }
}
  • 在此组件中,定义了一个鼠标移入移出的回调来处理每个代办的样式

  • 在每一个代办事项中,应有勾选复选框时,对应的代办事项的done值也发生改变,因此定义了handleCheck()函数。

  • 同时,在Item组件中,也应实现删除对应的代办事项功能,在此定义了删除一个todo的回调函数,在父组件App中进行数据处理并进行回调:

      deleteTodo = (id) => {
        const {todos} = this.state;
        //删除指定id的todo对象
        const newTodos = todos.filter((todoObj) => {
          return todoObj.id !== id
        })
        this.setState({todos:newTodos})
      }
    

06 Footer组件的功能实现

import React, { Component } from 'react'

export default class Footer extends Component {

  handleCheckAll = (event) => {
    this.props.checkAllTodo(event.target.checked)
  }

  handleClearAllDone = () => {
    this.props.clearAllDone()
  }

  render() {
    const {todos} = this.props;
    const doneCount = todos.reduce((pre,todo) => {return pre + (todo.done ? 1 : 0)},0);
    const total = todos.length;

    return (
        <div className='todo-footer'>
            <label>
                <input type='checkbox' checked={doneCount === total && total ? true : false} onChange={this.handleCheckAll}/>
            </label>
            <span>
                <span>已完成{doneCount}</span> / 全部{total}
            </span>
            <button onClick={this.handleClearAllDone} className='btn btn-danger' >清除已完成任务</button>
        </div>
    )
  }
}
  • 在Footer组件中,首先要实现的功能是统计已完成的待办数和总的待办数,并进行展示。因此,在代码中,定义了doneCounttotal分别代表二者,并通过数组的reduce方法进行已完成个数的统计

  • 对于清楚所有已完成代办事项的功能实现,我们定义了一个名为handleClearAllDone()的函数来处理clearAllDone()的函数回调。并在App组件里定义好回调的函数:

      clearAllDone = () => {
        const {todos} = this.state;
        const newTodos = todos.filter((todoObj) => {
          return !todoObj.done
        })
        this.setState(
          {todos:newTodos}
        )
      }
    
  • 同时,对于全选代办事项的功能,我们定义了名为handleCheckAll的回调,在App组件定义好函数实现功能:

      checkAllTodo = (done) => {
        const {todos} =this.state
        const newTodos = todos.map((todoObj) => {
          return {...todoObj, done}
        })
        this.setState(
          {todos:newTodos}
        )
      }