上节课我们学习了然后初始化加载展示一个动态的列表,在学习的过程中留下了一个问题不知道大家还记不记得,就是我们的子组件能修改父组件的state
吗?因为上节课我们把数据都存储在了App
组件的state
中了。如果我们想通过Add
组件来添加新的todo
的话是不是就要把todo
加到App
的state
中去?那么我们能不能完成这步操作呢?这节课我们就来研究一下。
回顾
在开始研究如何在子组件中修改父组件的state
之前,我们来对上节课的内容做一个简单的回顾。
- 目前我们无法在平级组件之间相互传递数据
- 在我们需要动态展示数据时一定要却定数据应该存储在哪个组件中
- 父组件可以给任意子组件传递数据
checkbox
如果写了checked
属性,如果不写onChange
事件就会变成只读状态- 遍历渲染一定要加唯一性的
key
以上便是上节课的主要内容,接下来我们来研究能不能以及如何在子组件中修改父组件的state
。
添加新todo
我们先来看一下,我们既然要添加新todo
,那么是一个什么流程?
- 输入
todo
- 按下回车键
- 获取到输入的值
- 添加到
App
组件的state
中 - 重新加载页面
那么我们从简单的开始,首先第 5 步是不是根本不需要我来做?只要成功修改了App
组件的state
,是不是就自动重新加载页面了?第 1 ~ 2 步是不是也没有什么多难的地方?有手就会啊,那么问题重点是不是在第 3 ~ 4 步?
获取输入的值
先看第 3 步,获取输入的值,怎么获取?什么时候获取?我们说了是按下回车之后更新,那么是不是在按下回车之后我们去获取数据然后更新到App
组件的state
中?那么我们来想想在按下回车后取值,这是不是一个键盘事件?是不是用onKeyUp
?为什么是onKeyUp
?因为onKeyUP
是按键按完在弹起,这代表正式按完了。
那么怎么获取呢?是不是下意识就想到了refs
?但是我们这里是给谁绑定的事件?是不是<input>
?而我们要获取谁的值?是不是也是<input>
?那么我们还需要用refs
吗?是不是就不用了直接用event.target
就行了啊。那么好我们来编写一下代码:
export default class Add extends Component { render() { return ( <div className="todo-add"> <input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认" /> </div> ); } handleKeyUp = e => { console.log(e.target.value); } }
我们来看一下,我们给<input>
绑定了onKeyUp
事件给了一个handleKeyUp
方法做回调,用e
来接收event
,然后我们打印了e.target.value
,为什么我们打印呢?因为什么现在还不知道怎么把获取到的值存到App
组件的state
中。我们先看一下效果。看看能不能拿到值:
确实是拿到了值,但是好像我们输入的每一个字符都被获取到了。这并不是我们想要的功能,那么我们是不是要在回调方法中来做一个判断了?那么根据什么判断?是不是我们原生js
里面就提到过得keyCode
?那回车键的keyCode
是多少?是不是13
?那么我们来改一下代码
export default class Add extends Component { render() {...} handleKeyUp = e => { const {target,keyCode} = e; if (keyCode !== 13) return; console.log(target.value); } }
现在我们做了判断是只当我们敲下回车之后才输出,那我们看一下效果吧:
这次就只输出了我们敲下回车之后输入框中的值。
更新App
组件的State
现在我们已经可以一按回车就获取到我们输入的值了,那么现在我们是不是就差一步,就是把值更新到App
组件的state
中就可以了?那么怎么更新进去呢?我们来分析一下:
我们的App
组件中是不是有一个Add
组件?这两个组件之间是什么关系?父子关系对不对?父组件给子组件传东西是不是很简单?直接传,然后子组件就会自动给收集到props
中去,那么父组件怎么拿到子组件中的值呢?其实也很简单,来看看我们这段代码
export default class App extends Component { state = {...}; add = data => { const {todos} = this.state; const todo = {id: todos.length + 1, name:data, done: false} this.setState({todos:[todo,...todos]}); }; render() { const {todos} = this.state; return ( <div className="todo-container"> <div className="todo-wrap"> <Add add={this.add} /> <List todos={todos} /> <Footer /> </div> </div> ) } }
这是什么意思?我是不是定义了一个方法?然后把方法传给了Add
组件?可能有人要说这个没明白,大家看,我是不是在App
组件中写了一个方法?这个方法中的this
指向是谁?是不是App
组件的实例对象啊?那我把这个函数传给Add
组件的换this
指向会丢吗?当然不会。
那么我在Add
组件中调用我们传进去的add
方法,那是不是就直接操作了App
组件中的state
?那么我们再让add
方法来接收一个参数,那么我们是不是就可以在Add
组件中把值传给add
方法,由add
方法来修改App
的state
了?
再来看看add
方法中,我们添加新的todo
,旧todo
是不是还要留着啊?而且新todo
要在最前面,那么我们就用展开运算符来拼接一下。接下来我们来修改一下Add
组件
export default class Add extends Component { render() {...} handleKeyUp = e => { const {target,keyCode} = e; if (keyCode !== 13) return; this.props.add(target.value); } }
现在我们就不需要打印target.value
了,我们直接修改到了state
中去,那么我们来看一下效果:
这次我们成功添加了todo
,而且正如需求中所说的那样,新todo
在最上面显示。
但是我们这么写其实是不规范的,为什么呢?如果说我们的这个state
中的todo
对象结构变了的话我们是不是还要去改这个方法啊?那么我们怎么改一下?
export default class App extends Component { state = {...}; add = dataObj => { const { todos } = this.state; this.setState({ todos: [dataObj, ...todos] }); }; render() {...} }
我们这样,我们让add
方法直接接收一个对象,我不管你结构变成什么样我就要求这是你在组件里面处理好的一个对象就行了。那么好,这里改了,我们是不是还要去改Add
组件啊?那么我们又有一个问题,我们这里的id
用什么呢?我们之前那种写法可以用state
的长度,现在我虽然能改state
但是我并不能拿到啊。这里我们来推荐一个库叫nanoid
,这是一个很小而且能够生成全球唯一的id
的一个库,直接在命令行中通过npm install nanoid
就可以安装了,那么我们来改一下代码
import React, { Component } from 'react'; import { nanoid } from 'nanoid'; import './Add.css'; export default class Add extends Component { render() {...} handleKeyUp = e => { const { target, keyCode } = e; if (keyCode !== 13) return; const todo = { id: nanoid(), name: target.value, done: false }; this.props.add(todo); } }
首先我们来看,我们导入了nanoid
然后在handleKeyUp
方法中我们定义了一个新的todo
对象,他的id
直接调用了nanoid
方法来生成,name
就是我们的target.value
,那么done
呢?我们往todolist
里面添加是不是肯定都还没完成?如果完成了也不会往里面添加啊。所以说这都是没完成的,所以说,那就默认false
。然后我们再把这个对象传给add
方法就可以完成state
的更新了,我们再看一下效果:
这次我也成功地更新了页面。但是我们有一个细节上的问题大家注意到没有?我们添加新todo
之后输入框却没有清空,而且还有一个问题:
像图上这样,我们什么都没用输入,但是也成功添加了,那么我们应该怎么处理?
export default class Add extends Component { render() {...} handleKeyUp = e => { const { target, keyCode } = e; if (keyCode !== 13) return; if (target.value.trim() ===""){ alert("输入不能为空!"); return; } const todo = { id: nanoid(), name: target.value, done: false }; this.props.add(todo); } }
我们先给handleKeyUp
方法加了一个判断。这是什么意思?就是判断输入的值是不是为空,但是为了防止有人输入的就是一串空格,那么我们先给去一下空格在来检查输入是否为空,如果为空,我们先提示一下,然后千万不能忘了,一定要return
来终止函数。那我们再来看一下效果:
这一次我们这种不符合规则的内容将无法添加了。那么另一个问题,清空,那简单啊,最后我们让输入框的值为空字符串不就行了嘛
export default class Add extends Component { render() {...} handleKeyUp = e => { const { target, keyCode } = e; if (keyCode !== 13) return; if (target.value.trim() ===""){ alert("输入不能为空!"); return; } const todo = { id: nanoid(), name: target.value, done: false }; this.props.add(todo); target.value = ""; } }
我们在最后把输入栏清空,我们看一下效果是不是有效:
添加todo
之后输入框也清空了,所以说这样是没有问题的,以上便是我们添加的功能
总结
- 添加
todo
分为 5 步 - 输入
todo
- 按下回车键
- 获取到输入的值
- 添加到
App
组件的state
中 - 重新加载页面
- 通过
onKeyUp
来监听键盘事件 - 通过
keyCode
来判断是否是回车键 - 通过父组件传入子组件一个函数来提供一个修改父组件
state
的入口给子组件
Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.
Link of this article:https://work.lynchow.com/article/lesson059_todolist_add_todo/