React 表单
这一节,我们将创建一个添加用户的功能组件。在client/src/components
目录下面添加两个新文件:
- AddUser.jsx
- AddUser.test.js
添加测试代码:
import React from 'react'; import { shallow } from 'enzyme'; import renderer from 'react-test-renderer'; import AddUser from './AddUser'; test('AddUser renders properly', () => { const wrapper = shallow(<AddUser />); const element = wrapper.find('form'); expect(element.find('input').length).toBe(3); expect(element.find('input').get(0).props.name).toBe('username'); expect(element.find('input').get(1).props.name).toBe('email'); expect(element.find('input').get(2).props.name).toBe('submit'); });
现在运行测试代码肯定会报错的,然后根据上面我们的测试代码,来编写AddUser
组件代码:
import React from 'react'; const AddUser = (props) => { return ( <form> <div className="form-group"> <input type="text" name="username" className="form-control input-lg" placeholder="Enter a username" required /> </div> <div className="form-group"> <input name="email" className="form-control input-lg" type="email" placeholder="Enter an email address" required /> </div> <input type="submit" className="btn btn-primary btn-lg btn-block" value="Submit" /> </form> ); }; export default AddUser;
然后我们在index.js
文件中引入AddUser
组件:
import AddUser from './components/AddUser';
然后在render()方法中增加组件:
render() { return ( <div className="container"> <div className="row"> <div className="col-md-6"> <br/> <h1>All Users</h1> <hr/><br/> <AddUser/> <br/> <UsersList users={this.state.users}/> </div> </div> </div> ) };
确保我们之前的后端服务是启动的状态:
$ docker-compose -f docker-compose.yml up -d --build
然后定位到 client 目录下面,先导入我们的环境变量:
$ export REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001 $ npm run start
然后在自动打开的浏览器中http://localhost:3000/
应该能看到下面的界面: 然后在AddUser.test.js
文件中新增一个测试:
test('AddUser renders a snapshot properly', () => { const tree = renderer.create(<AddUser/>).toJSON(); expect(tree).toMatchSnapshot(); });
然后我们执行测试,确保测试能够正常通过
$ npm run test
由于这是一个单页面应用程序,接下来我们希望在提交表单的时候阻止浏览器刷新页面,这样的用户体验会好很多的。要完成该功能需要以下4步:
- 处理表单提交事件
- 获取用户输入
- 发送
AJAX
请求 - 更新页面
处理表单提交事件¶
为了让我们自己能够处理submit
提交事件,只需要在AddUser.jsx
文件中更新下form
表单元素即可:
<form onSubmit={(event) => event.preventDefault()}>
大家记住event.preventDefault()
这个方法,是用来阻止元素的默认处理事件的,以后肯定会用到的,然后我们随意输入一个用户名或邮件地址,然后尝试提交一次表单,我们可以看到页面没有任何反应,这正是我们所希望的,因为我们阻止了正常的事件行为。
然后我们在client/src/index.js
文件中,为App
组件添加一个新的方法:
addUser(event) { event.preventDefault(); console.log('sanity check!'); };
由于 AddUser 是一个函数组件,所以我们需要通过props
来传递上面的添加方法,更新 render 方法下面的 AddUser 元素,如下所示:
<AddUser addUser={this.addUser} />
然后我们需要更新 App 组件的构造函数:
constructor() { super(); this.state = { users: [] }; this.addUser = this.addUser.bind(this); }
注意上面的bind方法,我们通过 bind 方法来手动绑定 this 的上下文。如果没有绑定的话,方法内的上下文是不正确的。
关事件处理的更多信息,可以查看 React 关于事件处理的官方文档。
没有它,这个方法内的上下文将不会有正确的上下文。 想要测试这个? 只需将console.log(this)添加到addUser(),然后提交表单即可。 什么是上下文? 删除绑定并再次测试。 现在的情况是什么?
然后在更新AddUser
组件中的 form 元素:
<form onSubmit={(event) => props.addUser(event)}>
然后切换到浏览器中,随便输入用户名和邮件,点击提交按钮,在JavaScript Console
终端(还记得怎么查看吗?)中可以看到sanity check!
打印出来。
获取用户输入¶
我们将使用受控组件来获取用户提交的数据。先增加两个新的属性在 App 组件的 state 对象上:
this.state = { users: [], username: '', email: '' };
然后通过属性传递给 AddUser 组件:
<AddUser username={this.state.username} email={this.state.email} addUser={this.addUser} />
现在我们在 AddUser 组件中就可以通过props
来访问 username 和 email 了,更新 AddUser 组件:
import React from 'react'; const AddUser = (props) => { return ( <form onSubmit={(event) => props.addUser(event)}> <div className="form-group"> <input type="text" name="username" className="form-control input-lg" placeholder="Enter a username" required value={props.username} /> </div> <div className="form-group"> <input name="email" className="form-control input-lg" type="email" placeholder="Enter an email address" required value={props.email} /> </div> <input type="submit" className="btn btn-primary btn-lg btn-block" value="Submit" /> </form> ); }; export default AddUser;
然后我们可以到浏览器中测试下,随意输入一些值,你会发现什么都不能输入,这是为什么呢?仔细看上面的 input 组件的 value 值,是通过props
来获取的,而传递给该组件的值是通过父组件的 state 传递过来的,因为父组件的 state 对象中的 username 和 email 一直都是空字符串,所以我们这里的输入值没有任何效果。
将父组件中的 state 对象中的 username 默认值更改成其他字符串,再看看效果呢?是不是和我们的分析是一致的。
那么我们怎么来更新父组件中的状态值呢,这样当我们在输入框中输入文本的时候就可以看到变化了。
首先,在 App 组件中新增一个handleChange
的方法:
handleChange(event){ const obj = {}; obj[event.target.name] = event.target.value; this.setState(obj); }
然后同样的,在构造函数中添加一个 bind:
this.handleChange = this.handleChange.bind(this);
然后通过props
将该方法传递给AddUser
组件:
<AddUser username={this.state.username} email={this.state.email} handleChange={this.handleChange} addUser={this.addUser} />
然后,我们就需要更新 AddUser 组件了,我们监听 input 元素的 onChange
方法,当 input 元素发送改变的时候,就会触发该调用:
import React from 'react'; const AddUser = (props) => { return ( <form onSubmit={(event) => props.addUser(event)}> <div className="form-group"> <input type="text" name="username" className="form-control input-lg" placeholder="Enter a username" required value={props.username} onChange={props.handleChange} /> </div> <div className="form-group"> <input name="email" className="form-control input-lg" type="email" placeholder="Enter an email address" required value={props.email} onChange={props.handleChange} /> </div> <input type="submit" className="btn btn-primary btn-lg btn-block" value="Submit" /> </form> ); }; export default AddUser;
现在我能在去浏览器中测试下,已经正常工作了。我们可以在 addUser 方法中打印下 state 对象的值,方便我们来了解 state 对象:
addUser(event) { event.preventDefault(); console.log('sanity check!'); console.log(this.state); };
然后我们可以到浏览器中输入用户名和邮件,点击提交按钮,我们可以在浏览器的 Console 终端中看到如下的信息了:
现在我们获得了输入的值,接下来我们就可以发送一个AJAX
请求,将我们的输入添加到数据库中,然后更新 DOM 树...
发送 AJAX 请求¶
还记得我们前面的 users-service 服务中的add_user
方法吗(project/api/views.py
文件下)?我们添加一个新的用户需要发送 username 和 email 两项数据:
db.session.add(User(username=username, email=email))
现在我们在 React 代码中用Axios
来发送添加用户的 POST 请求,修改client/src/index.js
中 App 组件的addUser
方法:
addUser(event) { event.preventDefault(); console.log('sanity check!'); const data = { username: this.state.username, email: this.state.email }; axios.post(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`, data).then(res => { console.log(res); }).catch(err => { console.log(err); }); }
然后我们切换到浏览器中添加一条数据,只要输入的 email 地址是唯一的,测试应该都会通过的。如果我们连续点击提交两次,我们就可以看到在 Chrome 浏览器的终端中看到有一个400的错误码打印出来,因为我们第二次提交的 email 邮箱已经存在了,而我们前面的users-service
服务在 email 存在的情况下会返回 400 状态码。
更新页面¶
最后当添加用户的表单提交成功后我们来更新用户列表,并且清空表单,同样更新addUser
方法:
addUser(event) { event.preventDefault(); console.log('sanity check!'); const data = { username: this.state.username, email: this.state.email }; axios.post(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`, data).then(res => { this.getUsers(); this.setState({ username: '', email: '' }); }).catch(err => { console.log(err); }); }
我们前面知道获取用户列表是通过getUsers()
方法的,所以当我们添加用户的请求发送成功后,是不是重新请求下getUsrs()
方法,是不是就能够把数据库中的所有用户获取到了啊?要清空表单该怎么做呢?当然是操作状态了,我们知道只需要更改 state 对象下的 username 和 email,对应的元素就会发送变化,我们不需要自己手动去更新 DOM 元素,只需要操作 state 就行,是不是很方便了啊?
然后我们切换到浏览器中测试一下,添加一个唯一的邮箱,是不是提交完成后下面的列表也更新了,表单也清空了。然后我们可以运行下我们的测试代码:
$ npm run test
我们会发现测试是没有通过的,出现下面的错误提示,这是因为我们的 UI 界面已经发生了改变,和之前我们的快照是不一样的,所以这个时候我们只需要输入u
更新下快照即可。
Snapshot Summary › 1 snapshot test failed in 1 test suite. Inspect your code changes or press `u` to update them.
快照更新后,正常情况下测试代码都会通过的。如果有其他问题,请仔细查看下错误日志,仔细排查,然后提交代码到github
。