第一步:先创建一个组件,显示单个待办事项
组件的结构 TodoLists.jsx
// TodoLists.jsx
import './TodoLists.css';
// 这里添加props作为行参,将来要接收Parent传递过来的参数
const TodoLists = (props) => {
return (
<div className="todo-container">
<div className="todo-list">
<div>Date //这里是添加日期</div>
<div>Thing //这里是待办事项的名字</div>
<button className="btn">删除</button>
</div>
</div>
);
};
export default TodoLists;
组件的样式 TodoLists.css
/* TodoLists.css */
.todo-container {
font-size: 2rem;
width: 100%;
/* height: 10rem; */
color: white;
padding: 2rem;
}
.todo-list {
font-weight: 400;
background-color: rgb(46, 125, 67);
display: flex;
justify-content: space-between;
align-items: center;
padding: 2rem;
}
显示组件,一般默认是 App.jsx,它为所有组件的 Top Parent App.jsx
import TodoLists from './components/TodoLists/TodoLists';
function App() {
return (
<div className="App">
<TodoLists />
</div>
);
}
export default App;
下面是渲染后的样子
第二步:添加动态数据
数据应该储存在最上层组件里,因为需要渲染这些数据,而最上层组件 App.jsx 负责全部组件,所以在 App.jsx 里添加数据
App.jsx
// App.jsx
function App() {
const todoListItems = [
{ id: 0, title: '睡觉', date: new Date(2022, 2, 2) },
{ id: 1, title: '上学', date: new Date(2022, 1, 2) },
{ id: 2, title: '睡午觉', date: new Date(2021, 2, 2) },
{ id: 3, title: '吃饭', date: new Date(2022, 1, 5) },
];
return <div className="App">{mapTodoListItems(todoListItems)}</div>;
}
const mapTodoListItems = (items) => {
// 用map函数返回多个小组件,避免代码重复
return items.map((item) => {
return <TodoLists key={item.id} title={item.title} date={item.date} />;
});
};
TodoLists.jsx
// TodoLists.jsx
const TodoLists = (props) => {
// 将Date的实例转为字符串
const date = props.date.toLocaleString('zh-cn', {
month: 'long',
day: '2-digit',
hour12: false,
});
return (
<div className="todo-container">
<div className="todo-list">
<div>{date}</div>
<div>{props.title}</div>
<button className="btn">删除</button>
</div>
</div>
);
};
每个组件应该负责它们单独的任务,可能很多地方都需要这个功能。所以对日期各式转换的功能我想单独放在一个组件里。
创建一个文件 TodoListDate.jsx
现在把处理日期的代码移到 TodoListDate 组件里
//TodoListDate.jsx
const TodoListDate = (props) => {
const date = formatDate(props.date);
return <div>{date}</div>;
};
const formatDate = (date) => {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
};
export { TodoListDate, formatDate };
// TodoLists.jsx
import { TodoListDate } from './TodoListDate';
const TodoLists = (props) => {
return (
<div className="todo-container">
<div className="todo-list">
// 将date实参传给TodoListDate(子组件)
<TodoListDate date={props.date} />
<div>{props.title}</div>
<button className="btn">删除</button>
</div>
</div>
);
};
第三步:增加添加待做事项的功能
创建新的组件 NewTodoList 文件夹,里面包含功能的小组件
// NewTodoList.jsx
import { formatDate } from '../TodoLists/TodoListDate';
import './NewTodoList.css';
const NewTodoList = (props) => {
return (
<div className="new-todo-container">
<form onSubmit="" className="new-todo">
<div className="new-todo__inputs">
<div className="new-todo__input">
<label htmlFor="title">Title:</label>
<input type="text" name="title" value="" />
</div>
<div className="new-todo__input">
<label htmlFor="date">Date:</label>
<input type="date" name="date" min={formatDate(new Date())} max="2022-12-31" />
</div>
</div>
<button className="btn" type="submit">
添加
</button>
</form>
</div>
);
};
export default NewTodoList;
/* NewTodoList.css */
.new-todo-container {
width: 100%;
}
.new-todo {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 3rem;
font-size: 1.2rem;
color: rgb(255, 254, 254);
}
.new-todo__inputs {
display: flex;
}
.new-todo__input {
margin: 0 1rem;
}
label {
margin-right: 0.5rem;
font-weight: 800;
}
.btn {
background-color: white;
padding: 1rem 2rem;
}
.btn:hover {
background-color: white;
color: black;
}
添加到 App.jsx 里
// App.jsx
import NewTodoList from './components/NewTodoList/NewTodoList';
return (
<div className="App">
{mapTodoListItems(todoListItems)}
<NewTodoList />
</div>
);
React input 元素里有 onChange 的属性,可以实时监听输入的内容,我们需要获取输入的内容,然后储存起来,再传入到 App 父组件里进行渲染
// NewTodoList.jsx
const NewTodoList = (props) => {
const titleEnteredHandler = (event) => {
// onChange属性返回一个事件,先log一下事件
console.log(event);
};
return (
<div>
<input type="text" name="title" value="" onChange={titleEnteredHandler} />
</div>
);
};
这个事件是一个 object,它的 target 属性就是我们需要的,因为其中有用户输入的内容的数据。
// 可以通过下面的表达式获取用户输入的内容
const data = event.target.value;
如果需要 Title 和 Date,则需要对 2 个 input 元素同时用 onChange 的属性。 现在来定义监听事件的函数
首先我需要把这些数据储存起来。form 元素的 onSubmit 的属性也接受一个传入函数,它用来处理提交表单。我希望的是提交表单后 input 元素里的值为空,并且用来储存这些数据的参数传递给 App.js 后,也为空。
先单个处理 Title
// NewTodoList.jsx
const NewTodoList = (props) => {
const title = '';
const titleEnteredHandler = (event) => {
title = event.target.value;
console.log(title);
};
return (
<div>
{' '}
<input type="text" name="title" onChange={titleEnteredHandler} />{' '}
</div>
);
};
上面代码有问题,因为函数里的 title 的作用域和我们在组件函数里定义的 title 的作用域不一样,所以无法改变定义在外面的 title,一般解决办法就是将组件定义为一个类,然后用构造函数,使得 title 为类的全局属性,但是这样太累赘了。React 给我们提供了一个特别方便的函数useState()
。它接受一个参数:字符串,我们想传入的初始值;返回一个数组。数组中第一个是字符串,也就是更新后的状态值,第二个是函数,用来设置状态值。
现在来定义输入框()的 value 属性,这个 value 属性也就是event.target.value
// NewTodoList.jsx
const [enteredTitle, setEnteredTitle] = useState(''); // 将初始值设为空的字符串,因为输入框初始状态就是没有输入任何内容
const [enteredDate, setEnteredDate] = useState(''); // 同理设为空
//或者直接把title和date设置为一个初始状态,它们是一个object的属性。
const [enteredInput, setEnteredInput] = useState({ title: '', date: '' });
const titleEnteredHandler = (event) => {
// 这个函数可以接受一个函数参数,prevState是这个函数传入在的参数函数里的值,它表示之前的状态。通过用箭头函数可以实时更新状态,而不必等某一个输入框完成更新。
setEnteredInput((prevState) => {
return { ...prevState, title: event.target.value };
});
// log一下最新的状态
console.log(enteredInput);
};
const dateEnteredHandler = (event) => {
setEnteredInput((prevState) => {
return { ...prevState, date: event.target.value };
});
// log一下最新的状态
console.log(enteredInput);
};
// 在返回的 div元素里, 修改input里onChange接受的函数
<input type="text" name="title" onChange={titleEnteredHandler} />
<input
type="date"
name="date"
min={formatDate(new Date())}
max="2022-12-31"
onChange={dateEnteredHandler}
/>
当我输入标题或者修改日期后,控制台出现了最新的 state
现在有了数据了,那怎么才能传给 App 组件呢 child 组件可以通过 props 传递给 parent 组件。
先处理提交表单的函数
// NewTodoList.jsx
const formSubmitHandler = (event) => {
// 防止跳转
event.preventDefault();
const newTodo = {
title: enteredInput.title,
date: new Date(enteredInput.date),
};
props.onAddTodo(newTodo);
setEnteredInput((prevState) => {
return { title: '', date: '' };
});
};
return (
<div className="new-todo-container">
<form onSubmit={formSubmitHandler} className="new-todo">
<div className="new-todo__inputs">
<div className="new-todo__input">
<label htmlFor="title">Title:</label>
<input
type="text"
name="title"
onChange={titleEnteredHandler}
value={enteredInput.title}
/>
</div>
<div className="new-todo__input">
<label htmlFor="date">Date:</label>
<input
type="date"
name="date"
min={formatDate(new Date())}
max="2022-12-31"
onChange={dateEnteredHandler}
value={enteredInput.date}
/>
</div>
</div>
<button className="btn" type="submit">
添加
</button>
</form>
</div>
);
然后在 App 组件里更新储存待做事项的数据
// App.jsx
function App() {
const DEFAULT_TODO = [
{ id: 0, title: '睡觉', date: new Date(2022, 2, 2) },
{ id: 1, title: '上学', date: new Date(2022, 1, 2) },
{ id: 2, title: '睡午觉', date: new Date(2021, 2, 2) },
{ id: 3, title: '吃饭', date: new Date(2022, 1, 5) },
];
const [todoListItems, setTodoListItems] = useState(DEFAULT_TODO);
const addTodoHandler = (newTodo) => {
setTodoListItems((prevState) => {
newTodo = { id: prevState[prevState.length - 1].id + 1, ...newTodo };
return [newTodo, ...prevState];
});
};
return (
<div className="App">
{mapTodoListItems(todoListItems)}
<NewTodoList onAddTodo={addTodoHandler} />
</div>
);
}
第四步:增加移除待做事项的功能
我们需要监听删除按钮,当它被点击的时候,需要更新 App.js 里的数据。
// App,jsx
const deleteTodoHandler = (id) => {
// 我们需要获取按钮所处的位置,因为每个待做事项都有自己id,所以可以通过id来删除某个待做事项。
const newTodoListItems = todoListItems.filter((item) => item.id !== id);
setTodoListItems(newTodoListItems);
};
const mapTodoListItems = (items) => {
return items.map((item) => {
return (
<TodoLists
key={item.id}
id={item.id}
title={item.title}
date={item.date}
// 增加一个prop, 它用来处理button被点击的事件
onDeleteTodo={deleteTodoHandler}
/>
);
});
};
//TodoList.jsx
const TodoLists = (props) => {
const deleteTodoHandler = (event) => {
props.onDeleteTodo(props.id);
};
return (
<div className="todo-container">
<div className="todo-list">
<TodoListDate date={props.date} />
<div>{props.title}</div>
<button className="btn" onClick={deleteTodoHandler}>
删除
</button>
</div>
</div>
);
};
测试一下,先删除所有的列表
但是全部删除后再添加,浏览器报错
因为现在没有数据在数组里,长度为 0,减去 1 为负数,Python 里的话应该是 Index out of range。所以报错了。
// App.jsx
const addTodoHandler = (newTodo) => {
setTodoListItems((prevState) => {
// 这里出错了,因为现在没有数据在数组里,长度为0,减去1为负数,Python里的话应该是Index out of range。所以报错了。
// 解决办法就是加一个判断语句
newTodo = { id: prevState[prevState.length - 1].id + 1, ...newTodo };
return [newTodo, ...prevState];
});
};
修改后
const addTodoHandler = (newTodo) => {
setTodoListItems((prevState) => {
if (prevState.length > 0) {
newTodo = { id: prevState[prevState.length - 1].id + 1, ...newTodo };
} else {
newTodo = { id: 0, ...newTodo };
}
return [newTodo, ...prevState];
});
};
完成!