React系列:实现TodoList

410 阅读3分钟

一、功能实现

  1. 添加任务
  2. 删除任务
  3. 更新任务状态
  4. 全选/全不选
  5. 清除已完成的任务

1.效果截图

image.png

二、组件的划分

  • TodoList整体作为一个大组件;
  • Header:input添加;
  • List:列表作为一个组件;
  • Item: 列表中的每个列表项(ListItem)作为一个组件;
  • Footer:底部展示和功能作为一个组件;

1.代码编写

App.js 父组件

import './App.css'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import { useState } from 'react'

function App() {
  const todoListData = [
    { id: '001', name: 'vue', check: true },
    { id: '002', name: 'react', check: false },
    { id: '003', name: 'node.js', check: true },
  ];

  const [todoList, setTodoList] = useState(todoListData);

  // 添加todo
  const addTodo = (todoObj) => {
    const newTodos = [todoObj, ...todoList]
    setTodoList(newTodos)
  };

  // 更新todolist
  const updataTodo = (id, check) => {
    const newTodos = todoList.map((item) => {
      if (item.id === id) {
        return { ...item, check: check }
      } else {
        return item
      }
    })
    setTodoList(newTodos)
  };

  // 删除某项todolist
  const delTodo = (id) => {
    const newTodos = todoList.filter((item) => {
      return item.id !== id
    })
    setTodoList(newTodos)
  };

  // 全选/全不选
  const allChecked = (check) => {
      const newTodos = todoList.map((item) => { 
        return { ...item, check: check } 
      })
      setTodoList(newTodos)
  };

    // 清除所以已经完成的任务
  const clearAllDone = () => {
      const newTodos = todoList.filter((item) => { 
        return !item.check  
      })
      setTodoList(newTodos)
  }

  return (
    <div style={{display: 'flex', justifyContent: 'center', alignItems: 'center',marginTop: '25px',}}>
      <div className="todo-wrap">
        <Header addTodo={addTodo}/>
        <List todoList={todoList} updataTodo={updataTodo} delTodo={delTodo} />
        <Footer  todoList={todoList} allChecked={allChecked} clearAllDone={clearAllDone}/>
      </div>
    </div>
  );
}
export default App;

Header 组件

import React from 'react';
import { message } from 'antd';
import './index.css';

const Header = (props) => {
    const {
        addTodo
    } = props;

    // 处理input enter事件
    const handleKeyUp = (e) => {
        if (e.keyCode !== 13) return
        if (e.target.value.trim() === '') {
            return message.warning('请输入代办事项!');
        }
        const todoObj = {
            id: Math.floor(10000000 * Math.random()),
            name: e.target.value,
            check: false
        }
        addTodo(todoObj)
        e.target.value = ""
    }
    return (
        <div className="todo-header">
            <input type="text" placeholder="请输入你的任务名称,按回车键确认" onKeyUp={(e) => handleKeyUp(e)} />
        </div>
    )
}
export default Header;

Header.css样式

/*header*/
.todo-header {
  margin-bottom: 10px;
}

.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);
  }

List 组件

import React from 'react'
import Item from '../Item'
import './index.css'

export default function index(props) {
    const {
        todoList,
        ...restProps
    } = props;
    return (
        <ul className="todo-main">
            {
                todoList.map((item) => <Item item={item} key={item.id} {...restProps}/>)
            }
        </ul>
    )
}

List.css样式

/*list*/
.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 组件

import React, { useState } from 'react';
import { Button } from 'antd';
import './index.css'

const Item = (props) => {
    const {
        item,
        updataTodo,
        delTodo
    } = props
    // 是否展现删除键
    const [mouse, setMouse] = useState(false)
    // 处理鼠标的移入移除事件
    const handleMouse = (mouse) => {
        setMouse(mouse)
    }
    return (
        <li onMouseLeave={() => handleMouse(false)} onMouseEnter={() => handleMouse(true)}>
            <label>
                <input type="checkbox" checked={item.check} onChange={(e) => updataTodo(item.id, e.target.checked)} />
                <span>{item.name}</span>
            </label>
            <Button type="primary" size="small" danger ghost style={{ display: mouse ? 'block' : 'none' }} onClick={()=> delTodo(item.id)}>删除</Button>
        </li>
    )
}
export default Item;

Item.css样式

/*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: 5px;
  }
  
  li:before {
    content: initial;
  }
  
  li:last-child {
    border-bottom: none;
  }

Footer 组件

import React from 'react';
import { Button } from 'antd';
import './index.css';

const Footer = (props)=> {
    const {
        todoList,
        allChecked,
        clearAllDone,
    } = props;
    const doneCount = todoList.reduce((pre,todo)=> pre + (todo.check ? 1 : 0),0);
    const allCount = todoList.length;
    return (
        <div className="todo-footer">
            <label>
                <input type="checkbox" onChange={(e)=>allChecked(e.target.checked)} checked={ doneCount === allCount && allCount !== 0 ? true : false }/>
            </label>
            <span>
                <span>已完成</span> / 全部{todoList.length}
            </span>
            <Button type="primary" ghost onClick={()=>clearAllDone()}>清除已完成任务</Button>
        </div>
    )
}

export default Footer;

Footer.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;
  }

三、案例总结

1. 动态初始化列表,如何确定将数据放在哪个组件的state中?

  • 某个组件使用:放在其自身的state中
  • 某些组件使用:放在他们共同的父组件state中(状态提升)

2.父子组件间如何通信

在父子组件中定义改变state数据的方法,将方法以props的形式传递给子组件,在子组件中触发事件处理程序,然后满足某种条件的话就执行父组件传来的函数。

  • 【父组件】给【子组件】传递数据:通过props传递
  • 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数

3.注意defaultChecked 和 checked的区别,类似的还有:defaultValue 和 value

defaultChecked 默认属性 只在初始化数据的时候赋值

4.状态在哪里,操作状态的方法就在哪里