跳转至

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/应该能看到下面的界面: index 然后在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 终端中看到如下的信息了: index

现在我们获得了输入的值,接下来我们就可以发送一个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 状态码。 status400

更新页面

最后当添加用户的表单提交成功后我们来更新用户列表,并且清空表单,同样更新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