一、效果展示
从界面上看分"上、下"两个部分:
- 上:新增待办
- 下:待办列表
二、数据分析
当"上"部分添加数据后,"下"部分会显示新增的数据,因此考虑使用数组list 保存待办事项数据,具体的每一项是个JS对象。
ini
let list = [
{
// 待办事项JS对象
},
{},
...];
"下"部分会切换显示相应类型的待办事项,因此考虑JS对象 具有completed 字段保存状态,且具有name 字段保存事项名称,以及id作为唯一标识。
csharp
let plan = {
id: 0, // 唯一标识
name: 'xxx', // 待办事项名称
completed: false // 待办事项完成状态
}
三、组件拆分
最简单的拆分方式,按照界面可以粗粒度地拆为两部分:
- AddTodo组件:对应界面"上"部分
- TodoList组件:对应界面"下"部分
深入一点思考,细化组件的功能:
AddTodo组件,可以改变 列表list,因此有个方法去执行这个改变;
TodoList组件,可以显示 list,因此有个状态 list保存数据;可以改变 列表list,因此有个方法去执行这个改变;
更进一步,AddTodo组件和TodoList组件关联度很高,可以考虑都放在Todos组件下;TodoList组件中的每行看起来都是差不多,可以考虑复用成TodoItem组件;
四、组件实现
AddTodo组件
前面分析过,AddTodo组件具有改变list功能,我们需要创建一个组件内部的方法clickHandler,其中能获取input的值,并执行父组件的方法,用于改变父组件中的list。
javascript
import React, { createRef } from 'react';
const inputRef = createRef(); // 使用hook
export default function AddTodo({ onAdd }) {
const clickHandler = () => {
let value = inputRef.current.value.trim();
if (value) {
// 执行父组件的方法,修改list数据
onAdd(value);
// 添加后将input框置空,提高用户体验
inputRef.current.value = "";
}
}
return (
<React.Fragment>
<input ref={inputRef} />
<button onClick={clickHandler}>添加</button>
</React.Fragment>
)
}
使用createRef()方法来创建一个ref对象inputRef。组件接受一个名为onAdd的props,用于添加一个新的待办事项。
点击按钮时,clickHandler函数会被调用。它首先从inputRef中获取输入框的值并进行了去除首尾空格的处理。然后判断值是否存在,如果存在则调用传入的onAdd函数,并将值作为参数传递给该函数。同时,将输入框的值置为空字符串,以提高用户体验。
最后,组件返回一个包含一个input和一个按钮的React.Fragment元素。input元素使用了ref属性将inputRef与之关联起来,以便能够在clickHandler函数中访问到input的值。按钮元素的onClick事件绑定了clickHandler函数。
TodoList组件
TodoList组件具有显示特定类型list和改变list功能,我们可以在Todos组件创建方法clickHandler,将其作为props传递给TodoList组件,TodoList以及子组件均为"受控组件"。显然TodoList依赖list做渲染,也能通过props从父组件获取。
javascript
import React from 'react';
import TodoItem from './todo-item';
export default function TodoList({ list, onClick }) {
return (
<ul>
{
list.map(item => <TodoItem // 渲染TodoItem
key={item.id}
text={item.text}
onClick={() => onClick(item.id)}
completed={item.completed} />)
}
</ul>
)
}
组件接受两个个props:list、onClick。list表示待办事项的数组,onClick表示点击待办事项时要执行的函数。
在返回的JSX代码中,map()方法将每个待办事项映射为一个TodoItem组件。
每个TodoItem组件都有一个唯一的key属性,使用item.id作为key,text属性表示待办事项的文本内容,onClick属性表示点击待办事项时要执行的函数,completed属性表示待办事项是否已完成。
最后,组件返回一个ul元素,其中包含了通过过滤和映射得到的TodoItem组件列表。
TodoItem组件
TodoItem组件是完全受控的组件,它的显示内容完全由props决定,根据UI图我们可以知道它需要text、completed和onClick属性。
javascript
import React from 'react';
export default function TodoItem({ text, onClick, completed }) {
return (
<div onClick={onClick}>
<span>{text} </span>
<span>
完成:
<input type="checkbox" readOnly checked={completed} />
</span>
</div>
)
}
组件接受三个props:text、onClick和completed。
在返回的JSX代码中,组件被包裹在一个div元素中,并绑定了onClick事件。当用户点击这个div时,会调用传入的onClick函数。
div元素内部包含两个span元素,第一个span元素用于显示待办事项的文本内容,其中 用于插入多个空格以增加文本的间距。
第二个span元素用于表示待办事项的完成状态,包含了一个只读的复选框input元素。复选框的checked属性根据completed的值决定是否被选中。
最后,组件返回这个包含了文本和完成状态的div元素。当用户点击这个div时,会调用传入的onClick函数。
Todos组件
Todos组件相对复杂点,因为list和clickHandler方法都放在里面,它将AddTodo组件和TodoList组件关联了起来。
javascript
import React, { useState } from 'react';
import deepcopy from 'deepcopy';
import AddTodo from './add-todo';
import TodoList from './todo-list';
export default function () {
// 整个todos组件中的状态list保存在这里
const [list, setList] = useState([]);
// 传递给AddTodo组件
const addHandler = (text) => {
let item = {
id: (Math.random() * 10000).toFixed(0),
text: text,
completed: false
}
setList([...list, item]);
}
// 传递给能TodoList组件
const clickHandler = (id) => {
let newList = deepcopy(list);
newList.forEach(item => {
if (item.id === id) {
item.completed = !item.completed;
}
});
setList(newList);
}
return (
<div className="todos">
{/* 渲染AddTodo组件 */}
<AddTodo onAdd={addHandler} />
{/* 渲染TodoList组件 */}
<TodoList list={list} onClick={clickHandler} />
</div>
)
}
使用了React的useState()钩子,以及导入了deepcopy库。
在组件内部,使用useState()定义了一个名为list的状态变量以及一个名为setList的更新状态的函数。初始状态为空数组。
在组件返回的JSX代码中,渲染了AddTodo组件和TodoList组件。
AddTodo组件中的onAdd属性绑定了addHandler函数,用于添加待办事项。addHandler函数接受一个参数text,根据text创建一个新的待办事项对象item,并将其添加到list状态中。需要通过使用扩展运算符(...)将原来的list数组解构出来,再添加新的item,以确保React能够正确地触发重新渲染。
TodoList组件中的list属性绑定了list状态,onClick属性绑定了clickHandler函数。clickHandler函数用于处理待办事项的点击事件。通过使用deepcopy库创建列表的深拷贝newList,遍历该列表并根据待办事项的id找到对应的项,并将其completed属性取反。最后,使用setList函数更新list状态为新的列表newList。
最后,根组件返回一个包含AddTodo组件和TodoList组件的div元素。
五、总结
总的来说,实现这个业务逻辑不难,但是在实操中就会容易出错。