React教程 - 9. 表单

本文译自React官方文档
全文翻译及相关代码,请参看我的Github

在React中,HTML表单元素与其他DOM元素略有不同-表单元素具有一些内部状态。例如,以下HTML表单接收一个姓名:

<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>

用户提交表单时,该表单会执行HTML表单的默认行为。在React中,如果这就是我们想要的效果,那么不需要任何额外操作。
但大多情况下,会需要使用JS函数处理用户输入的数据并执行提交动作。实现这一点的标准方式是使用一种称为“受控组件”的技术。

1.受控组件

在HTML中, <input><textarea><select>等表单元素均有自己的状态,并会根据用户的输入进行更新。
在React中,可变状态通常存放在组件的state属性中,并通过setState()进行更新。
我们可以结合以上两种方式,采用React的状态作为“单一数据源(single source of truth)”。
渲染表单的React组件在用户输入时进行相应变化,控制组件的显示。采用这种方式,值由React控制的表单元素就叫做“受控组件”。
例如,在上例中,如果想要在用户提交时打印其提交的内容,可将form写成受控组件:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

在CodePen中尝试
由于在表单元素中设置了value属性的赋值方式-value={this.state.value},表单元素显示的值是this.state.value,使用了React的状态作为单一数据源。
由于每次用户输入时都会执行handleChange更新React的state,元素显示的值也将随着用户的输入而更新。
在受控组件中,状态每次的变化都会与处理函数(handleChange)相关联,这让我们有机会直接对用户的输入做一些操作,如直接将用户的输入自动转换为大写:

handleChange(event) {
  this.setState({value: event.target.value.toUpperCase()});
}

2.textarea标签

在HTML中,<textarea>元素通过子节点定义其文本:

<textarea>
  Hello there, this is some text in a text area
</textarea>

React中采用value属性进行替代。这种方式使得使用<textarea>标签与单行输入的<input>标签一样方便:

class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 'Please write an essay about your favorite DOM element.'
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('An essay was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <textarea value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

注意,this.state.value在构造函数中初始化,因此textarea在一开始就会有一些文本。

3.select标签

在HTML中,<select>创建一个下拉列表。下列HTML创建了一个口味下拉列表:

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

这里Coconut是默认被选中的-该option中添加了selected属性。
React中并不使用selected属性,而是仍采用为value赋值的方式,并将该属性置于select根标签中。这种方式在受控组件中更为方便,只需要在一个地方更新该值即可:

class FlavorForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: 'coconut'};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('Your favorite flavor is: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Pick your favorite La Croix flavor:
          <select value={this.state.value} onChange={this.handleChange}>
            <option value="grapefruit">Grapefruit</option>
            <option value="lime">Lime</option>
            <option value="coconut">Coconut</option>
            <option value="mango">Mango</option>
          </select>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

在CodePen中尝试
总而言之,采用这种方式使得使用<input type-"text">, <textarea><select>都非常类似-它们均接受一个value属性,我们通过该属性实现一个受控组件。

4.处理多个输入

当需要处理多个受控input元素时,可以为每个元素增加一个name属性,在处理函数中根据event.target.name的值进行相应操作:

class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isGoing: true,
      numberOfGuests: 2
    };

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Number of guests:
          <input
            name="numberOfGuests"
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}

在CodePen中尝试
注意,这里使用了ES6中的计算属性名表达式根据输入更新state中对应key的value:

this.setState({
  [name]: value
});

这等同与ES5中的:

var partialState = {};
partialState[name] = value;
this.setState(partialState);

另外,由于setState()是合并更新(可参看状态与生命周期-“状态更新是合并更新”这一小节),我们只要更新我们需要更新的部分就可以了。

5.受控组件的替代方案

有时大量使用受控组件会很麻烦-对每种数据的更新都要写好相应的处理函数,并通过React组件管理所有输入状态。
在这种情况下,或许我们可以尝试它的替代方案-非受控组件