目标
- 顶部输入框中输入任务(字符串),敲击回车键后,中间新出现一个代办项
- 鼠标放在单个代办项,右侧出现删除按钮,点击删除代办项
- 选中多个代办项,点击右下角“清除已完成”按钮,删除所有被选中的待办项
组件设计
除整体App组件外,初步设计为4个组件:
- Header:顶部输入框
- List:中间所有的代办列表
- Item:待办列表中的一项
- Footer:底部显示统计状态信息和“删除所有”按钮所在栏
详细设计
肯定涉及到官方入门程序中“状态提升”的概念:兄弟组件将自己的状态交给父组件管理(自己就变成了一个“受控组件”)
- Header首先要有一个成员变量接收输入的字符串
- 其次要新建一个Item待办项,并将输入串交给它
- 我们还希望同时页面能自动重新渲染,这就涉及到了某个state。具体哪一个呢?
- Footer要同步更新总共的待办项数量
这么看来,应该在App中准备两个变量,嗯…突然想到了这一系列动作其实都是“回车键”触发的,所以其实暂时不用state
但是…手动重新渲染页面吗?…还是用state吧
仔细想,这个
敲下回车->Header获取输入框的内容,交给List(事实上应该是修改了App的一个属性),并清空输入框->List根据获取到的字符串,新建一个Item组件->修改Footer显示的Item数量->重新渲染页面
我该用怎样的数据结构来保存这些Item?需要能够被选中删除,而且不定长
答案是:一个对象数组,但是怎么获取任意对象?
实现
输入并按下回车键,新增一项到待办列表
- 首先(由Header)监听键盘事件,当回车键回弹后触发
<!--为输入框绑定键盘事件-->
<input type="text" placeholder="请输入你的任务名称,按回车键确认" onKeyUp={this.handleKeyUp}/>
// 事件处理函数中判断是否是“回车键”以及输入是否为空
handleKeyUp = (e) => {
if (e.key !== 'Enter' || e.target.value.trim() === '') return;
}
- 因为Header获取的输入需要传递给List,很明显兄弟组件之间的传值,状态提升将变量保存在父组件App中,至少是个数组
但是这里的每一项又是有状态的(比如默认是否选中(完成)),于是这里被定义为一个对象数组
// 为什么这里要放在state中?因为每次回车触发向数组中添加一个对象后,我都希望立即出现在下面的列表中,即页面的重新渲染,state可以方便地做到这一点
constructor(props) {
super(props);
this.state = {
items: [
{id: '1', label: '吃饭', done: true},
{id: '2', label: '睡觉', done: true},
{id: '3', label: '敲代码', done: false}
]
}
}
- 保存在App状态中的数据改变了还不够,还需要List获取并遍历,构造Item组件
父传子就很简单通过props
// 思考:页面重新渲染,传参也会重新传一遍吗?
<List items={items}/>
然后就是List遍历这个数组并构造Item组件,注意这里指定key
其实这里不是很明白怎么回事
return (
<ul className="todo-main">
{
items.map((item) => {
/*return <Item key={item.id} id={item.id} name={item.name} done={item.done}/>*/
return <Item key={item.id} {...item}/>
})
}
</ul>
)
Item组件需要把对象的信息展示出来
return (
<li className="item">
<label>
<input type="checkbox" defaultChecked={done}/>
<span>{label}</span>
</label>
</li>
)
- 子组件Header修改父组件state,是通过父组件向子组件传递一个可以修改自己state地函数来做到的
// 这个函数接收一个待办项对象为参数,把他添加到列表数组并更新state触发刷新
// 思考:为什么这里构造了一个新数组而不是直接修改?
addItem = (item) => {
const {items} = this.state;
const newItems = [item, ...items];
this.setState({items: newItems});
}
// 把这个函数交给子组件
<Header addItem={this.addItem}/>
- Header中直接获取的仅仅是一个字符串,我们需要额外的参数构造一个对象并交给父组件提供的方法
nanoid是一个库,提供一个UUID
const item = {id: nanoid(), label: e.target.value, done: false};
this.props.addItem(item);
e.target.value = "";
至此,功能点1完成(当然,Footer中的实时统计信息还没做)
鼠标放在单个代办项,右侧出现删除按钮,点击删除代办项
首先是鼠标移至待办项上,待办项的交互响应:
- 背景色改变:这个可以通过 伪类选择器
:hover
轻易实现 - “删除”按钮出现,这个只能绑定监听事件,监听鼠标移入和移出
<li className="item"
onm ouseEnter={this.handleMouse(true)} onm ouseLeave={this.handleMouse(false)}>
</li>
处理事件中仅仅是修改了保存在state中的标志位mouseEnter
,当然,这也是为了能够自动渲染
handleMouse = (flag) => {
return () => {
this.setState({mouseEnter: flag});
}
}
而这个标志位又决定了“删除按钮”是否显示
<button className="btn btn-danger" style={{display: mouseEnter ? 'block' : 'none'}}>删除</button>
至此,功能2完成
来看一眼目前的样子