赞
踩
文章来源 | Github
文章地址 | 翻译 | 玩转 React 表单 —— 受控组件详解
本文涵盖以下受控组件:
同时也包含:
点击这里直接查看示例代码。 查看示例。 请在运行示例时打开浏览器的控制台。
在学习 React.js 时我遇到了一个问题,那就是很难找到受控组件的真实示例。受控文本输入框的例子倒是很丰富,但复选框、单选框、下拉选择框的例子却不尽人意。
本文列举了真实的受控表单组件示例,要是我在学习 React 的时候早点发现这些示例就好了。除了日期和时间输入框需要另开篇幅详细讨论,文中列举了所有的表单元素。
有时候,为了减少开发时间,有时候人们很容易为了一些东西(譬如表单元素)引入一个库。而对于表单,我发现当需要添加自定义行为或表单校验时,使用库会让事情变得更复杂。不过一旦掌握合适的 React 模式,你会发现构建表单组件并非难事,并且有些东西完全可以自己动手,丰衣足食。请把本文的示例代码当作你创建表单组件的起点或灵感之源。
除了提供单独的组件代码,我还将这些组件放进表单中,方便你理解子组件如何更新父组件 state ,以及接下来父组件如何通过 props(单向数据流)更新子组件。
注意:本表单示例由很赞的create-react-app构建配置生成,如果你还没有安装该构建配置,我强烈推荐你安装一下(npm install -g create-react-app)。目前这是搭建 React 应用最简单的方式。
受控组件有两个特点:
这个单向循环 —— (数据)从(1)子组件输入到(2)父组件的 state,接着(3)通过 props 回到子组件,就是 React.js 应用架构中单向数据流的含义。
我们的顶级组件叫做App,这是它的代码:
- import React, { Component } from 'react';
- import '../node_modules/spectre.css/dist/spectre.min.css';
- import './styles.css';
- import FormContainer from './containers/FormContainer';
-
- class App extends Component {
- render() {
- return (
- <div className="container">
- <div className="columns">
- <div className="col-md-9 centered">
- <h3>React.js Controlled Form Components</h3>
- <FormContainer />
- </div>
- </div>
- </div>
- );
- }
- }
-
- export default App;
App只负责渲染index.html页面。整个App组件最有趣的部分是 13 行,FormContainer组件。
是时候提及一下容器(智能)组件和普通(木偶)组件了。容器组件包含业务逻辑,它会发起数据请求或进行其他业务操作。普通组件则从它的父(容器)组件接收数据。木偶组件有可能触发更新 state (译注:容器组件的 state)这类逻辑行为,但它仅通过从父(容器)组件传入的方法来达到该目的。
注意: 虽然在我们的表单应用里父组件就是容器组件,但我要强调,并非所有的父组件都是容器组件。木偶组件嵌套木偶组件也是可以的。
FormContainer组件包含了表单元素组件,它在生命周期钩子方法componentDidMount里请求数据,此外还包含更新表单应用 state 的逻辑行为。在下面的预览代码里,我移除了表单元素的 props 和 change 事件处理方法,这样看起来更简洁清晰(拉到文章底部,可以看到完整代码)。
- import React, {Component} from 'react';
- import CheckboxOrRadioGroup from '../components/CheckboxOrRadioGroup';
- import SingleInput from '../components/SingleInput';
- import TextArea from '../components/TextArea';
- import Select from '../components/Select';
-
- class FormContainer extends Component {
- constructor(props) {
- super(props);
- this.handleFormSubmit = this.handleFormSubmit.bind(this);
- this.handleClearForm = this.handleClearForm.bind(this);
- }
- componentDidMount() {
- fetch('./fake_db.json')
- .then(res => res.json())
- .then(data => {
- this.setState({
- ownerName: data.ownerName,
- petSelections: data.petSelections,
- selectedPets: data.selectedPets,
- ageOptions: data.ageOptions,
- ownerAgeRangeSelection: data.ownerAgeRangeSelection,
- siblingOptions: data.siblingOptions,
- siblingSelection: data.siblingSelection,
- currentPetCount: data.currentPetCount,
- description: data.description
- });
- });
- }
- handleFormSubmit() {
- // 提交逻辑写在这
- }
- handleClearForm() {
- // 清除表单逻辑写在这
- }
- render() {
- return (
- <form className="container" onSubmit={this.handleFormSubmit}>
- <h5>Pet Adoption Form</h5>
- <SingleInput /> {/* Full name text input */}
- <Select /> {/* Owner age range select */}
- <CheckboxOrRadioGroup /> {/* Pet type checkboxes */}
- <CheckboxOrRadioGroup /> {/* Will you adopt siblings? radios */}
- <SingleInput /> {/* Number of current pets number input */}
- <TextArea /> {/* Descriptions of current pets textarea */}
- <input
- type="submit"
- className="btn btn-primary float-right"
- value="Submit"/>
- <button
- className="btn btn-link float-left"
- onClick={this.handleClearForm}>Clear form</button>
- </form>
- );
- }
我们勾勒出了应用基础结构,接下来我们一起浏览下每个子组件的细节。
该组件可以是 text 或 number 输入框,这取决于传入的 props。通过 React 的 PropTypes,我们可以非常好地记录组件拿到的 props。如果漏传 props 或传入错误的数据类型, 浏览器的控制台中会出现警告信息。
下面列举 <SingleInput /> 组件的 PropTypes:
- SingleInput.propTypes = {
- inputType: React.PropTypes.oneOf(['text', 'number']).isRequired,
- title: React.PropTypes.string.isRequired,
- name: React.PropTypes.string.isRequired,
- controlFunc: React.PropTypes.func.isRequired,
- content: React.PropTypes.oneOfType([
- React.PropTypes.string,
- React.PropTypes.number,
- ]).isRequired,
- placeholder: React.PropTypes.string,
- };
PropTypes 声明了 prop 的类型(string、 number、 array、 object 等等),其中包括了必需(isRequired)和非必需的 prop,当然它还有更多的用途(欲知更多细节,请查看 React 文档)。
下面我们逐个讨论这些 PropType:
既然该组件不需要任何逻辑行为和内部 state,那我们可以将它写成纯函数组件(pure functional component)。我们将纯函数组件赋值给一个 const 常量上。下面是 <SingleInput /> 组件的所有代码。本文列举的所有表单元素组件都是纯函数组件。
- import React from 'react';
-
- const SingleInput = (props) => (
- <div className="form-group">
- <label className="form-label">{props.title}</label>
- <input
- className="form-input"
- name={props.name}
- type={props.inputType}
- value={props.content}
- onChange={props.controlFunc}
- placeholder={props.placeholder} />
- </div>
- );
-
- SingleInput.propTypes = {
- inputType: React.PropTypes.oneOf(['text', 'number']).isRequired,
- title: React.PropTypes.string.isRequired,
- name: React.PropTypes.string.isRequired,
- controlFunc: React.PropTypes.func.isRequired,
- content: React.PropTypes.oneOfType([
- React.PropTypes.string,
- React.PropTypes.number,
- ]).isRequired,
- placeholder: React.PropTypes.string,
- };
-
- export default SingleInput;
接着,我们用handleFullNameChange方法(它被传入到controlFuncprop 属性)来更新<FormContainer />容器组件的 state。
- // FormContainer.js
-
- handleFullNameChange(e) {
- this.setState({ ownerName: e.target.value });
- }
- // constructor 方法里别漏掉了这行:
- // this.handleFullNameChange = this.handleFullNameChange.bind(this);
随后我们将容器组件更新后的 state (译注:这里指 state 上挂载的 ownerName 属性)通过contentprop 传回<SingleInput />组件。
选择组件(就是下拉选择组件),接收以下 props:
- Select.propTypes = {
- name: React.PropTypes.string.isRequired,
- options: React.PropTypes.array.isRequired,
- selectedOption: React.PropTypes.string,
- controlFunc: React.PropTypes.func.isRequired,
- placeholder: React.PropTypes.string
- };
- import React from 'react';
-
- const Select = (props) => (
- <div className="form-group">
- <select
- name={props.name}
- value={props.selectedOption}
- onChange={props.controlFunc}
- className="form-select">
- <option value="">{props.placeholder}</option>
- {props.options.map(opt => {
- return (
- <option
- key={opt}
- value={opt}>{opt}</option>
- );
- })}
- </select>
- </div>
- );
-
- Select.propTypes = {
- name: React.PropTypes.string.isRequired,
- options: React.PropTypes.array.isRequired,
- selectedOption: React.PropTypes.string,
- controlFunc: React.PropTypes.func.isRequired,
- placeholder: React.PropTypes.string
- };
-
- export default Select;
请注意 option 标签中的 key 属性(第 14 行)。React 要求被重复操作渲染的每个元素必须拥有独一无二的 key 值,我们这里的 .map() 方法就是所谓的重复操作。既然选择项数组中的每个元素是独有的,我们就把它们当成 key prop。该 key 值协助 React 追踪 DOM 变化。虽然在循环操作或 mapping 时忘加 key 属性不会中断应用,但是浏览器的控制台里会出现警告,并且渲染性能将受到影响。
以下是控制选择框组件(记住,该组件存在于 <FormContainer /> 组件中)的处理方法(该方法从 <FormContainer /> 组件传入到子组件的 controlFun prop 中)
- // FormContainer.js
-
- handleAgeRangeSelect(e) {
- this.setState({ ownerAgeRangeSelection: e.target.value });
- }
- // constructor 方法里别漏掉了这行:
- // this.handleAgeRangeSelect = this.handleAgeRangeSelect.bind(this);
<CheckboxOrRadioGroup /> 与众不同, 它从 props 拿到传入的数组(像此前 <Select /> 组件的选项数组一样),通过遍历数组来渲染一组表单元素的集合 —— 可以是复选框集合或单选框集合。
让我们深入 PropTypes 来更好地理解 <CheckboxOrRadioGroup /> 组件。
- CheckboxGroup.propTypes = {
- title: React.PropTypes.string.isRequired,
- type: React.PropTypes.oneOf(['checkbox', 'radio']).isRequired,
- setName: React.PropTypes.string.isRequired,
- options: React.PropTypes.array.isRequired,
- selectedOptions: React.PropTypes.array,
- controlFunc: React.PropTypes.func.isRequired
- };
这是本表单应用中最有趣的组件,让我们来看一下:
- import React from 'react';
-
- const CheckboxOrRadioGroup = (props) => (
- <div>
- <label className="form-label">{props.title}</label>
- <div className="checkbox-group">
- {props.options.map(opt => {
- return (
- <label key={opt} className="form-label capitalize">
- <input
- className="form-checkbox"
- name={props.setName}
- onChange={props.controlFunc}
- value={opt}
- checked={ props.selectedOptions.indexOf(opt) > -1 }
- type={props.type} /> {opt}
- </label>
- );
- })}
- </div>
- </div>
- );
-
- CheckboxOrRadioGroup.propTypes = {
- title: React.PropTypes.string.isRequired,
- type: React.PropTypes.oneOf(['checkbox', 'radio']).isRequired,
- setName: React.PropTypes.string.isRequired,
- options: React.PropTypes.array.isRequired,
- selectedOptions: React.PropTypes.array,
- controlFunc: React.PropTypes.func.isRequired
- };
-
- export default CheckboxOrRadioGroup;
checked={ props.selectedOptions.indexOf(option) > -1 } 这一行代码表示单选框或复选框是否被选中的逻辑。
属性 checked 接收一个布尔值,用来表示 input 组件是否应该被渲染成选中状态。我们在检查到 input 的值是否是 props.selectedOptions 数组的元素之一时生成该布尔值。 myArray.indexOf(item) 方法返回 item 在数组中的索引值。如果 item 不在数组中,返回 -1,因此,我们写了 > -1。
注意,0 是一个合法的索引值,所以我们需要 > -1 ,否则代码会有 bug。如果没有 > -1,selectedOptions 数组中的第一个 item —— 其索引为 0 —— 将永远不会被渲染成选中状态,因为 0 是一个类 false 的值(译注:在 checked 属性中,0 会被当成 false 处理)。
本组件的处理方法同样比其他的有趣。
- handlePetSelection(e) {
-
- const newSelection = e.target.value;
- let newSelectionArray;
-
- if(this.state.selectedPets.indexOf(newSelection) > -1) {
- newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection)
- } else {
- newSelectionArray = [...this.state.selectedPets, newSelection];
- }
-
- this.setState({ selectedPets: newSelectionArray });
- }
如同所有处理方法一样,事件对象被传入方法,这样一来我们就能拿到事件对象的值(译注:准确来说,应该是事件目标元素的值)。我们将该值赋给newSelection 常量。接着我们在函数顶部附近定义 newSelectionArray 变量。因为我们将在一个 if/else 代码块里对该变量进行赋值,所以用 let 而非 const 来定义它。我们在代码块外部进行定义,这样一来被定义变量的作用域就是函数内部的最外沿,并且函数内的代码块都能访问到外部定义的变量。
该方法需要处理两种可能的情况。
如果 input 组件的值不在 selectedOptions 数组中,我们要将值添加进该数组。 如果 input 组件的值在 selectedOptions 数组中,我们要从数组中删除该值。
添加(第 8 - 10 行): 为了将新值添加进选项数组,我们通过解构旧数组(数组前的三点...表示解构)创建一个新数组,并且将新值添加到数组的尾部 newSelectionArray = [...this.state.selectedPets, newSelection];。
注意,我们创建了一个新数组,而不是通过类似 .push() 的方法来改变原数组。不改变已存在的对象和数组,而是创建新的对象和数组,这在 React 中是又一个最佳实践。开发者这样做可以更容易地跟踪 state 的变化,而第三方 state 管理库,如 Redux 则可以做高性能的浅比较,而不是阻塞性能的深比较。
删除(第 6 - 8 行):if 代码块借助此前用到的 .indexOf() 小技巧,检查选项是否在数组中。如果选项已经在数组中,通过.filter()方法,该选项将被移除。 该方法返回一个包含所有满足 filter 条件的元素的新数组(记住要避免在 React 直接修改数组或对象!)。
newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection)
在这种情况下,除了传入到方法中的选项之外,其他选项都会被返回。
<TextArea />和我们已提到的那些组件非常相似,除了resize和rows,目前你应该对它的 props 很熟悉了。
- TextArea.propTypes = {
- title: React.PropTypes.string.isRequired,
- rows: React.PropTypes.number.isRequired,
- name: React.PropTypes.string.isRequired,
- content: React.PropTypes.string.isRequired,
- resize: React.PropTypes.bool,
- placeholder: React.PropTypes.string,
- controlFunc: React.PropTypes.func.isRequired
- };
<TextArea /> 组件的完整代码:
- import React from 'react';
-
- const TextArea = (props) => (
- <div className="form-group">
- <label className="form-label">{props.title}</label>
- <textarea
- className="form-input"
- style={props.resize ? null : {resize: 'none'}}
- name={props.name}
- rows={props.rows}
- value={props.content}
- onChange={props.controlFunc}
- placeholder={props.placeholder} />
- </div>
- );
-
- TextArea.propTypes = {
- title: React.PropTypes.string.isRequired,
- rows: React.PropTypes.number.isRequired,
- name: React.PropTypes.string.isRequired,
- content: React.PropTypes.string.isRequired,
- resize: React.PropTypes.bool,
- placeholder: React.PropTypes.string,
- controlFunc: React.PropTypes.func.isRequired
- };
-
- export default TextArea;
TextAreas />组件的控制方法和<SingleInput />如出一辙。细节部分请参考<SingleInput />组件。
handleClearForm 和 handleFormSubmit 方法操作整个表单。
既然我们在表单的各处都使用了单向数据流,那么清除表单数据对我们来说也是小菜一碟。<FormContainer /> 组件的 state 控制了每个表单元素的值。该容器的 state 通过 props 传入子组件。只有当 <FormContainer /> 组件的 state 改变时,表单组件显示的值才会改变。
清除表单子组件中显示的数据很简单,只要把容器的 state (译注:这里是指 state 对象上挂载的各个变量)设置成空数组和空字符串就可以了(如果有数字输入框的话则是将值设置成 0)。
- handleClearForm(e) {
- e.preventDefault();
- this.setState({
- ownerName: '',
- selectedPets: [],
- ownerAgeRangeSelection: '',
- siblingSelection: [],
- currentPetCount: 0,
- description: ''
- });
- }
注意,e.preventDefault() 阻止了页面重新加载,接着 setState() 方法用来清除表单数据。
为了提交表单数据,我们从 state 中抽取需要提交的属性值,创建了一个对象。接着使用 AJAX 库或技术将这些数据发送给 API(本文不包含此类内容)。
- handleFormSubmit(e) {
- e.preventDefault();
-
- const formPayload = {
- ownerName: this.state.ownerName,
- selectedPets: this.state.selectedPets,
- ownerAgeRangeSelection: this.state.ownerAgeRangeSelection,
- siblingSelection: this.state.siblingSelection,
- currentPetCount: this.state.currentPetCount,
- description: this.state.description
- };
-
- console.log('Send this in a POST request:', formPayload);
- this.handleClearForm(e);
- }
请注意我们在提交数据后执行this.handleClearForm(e)清除了表单。
受控表单组件非常适合自定义表单校验。假设要从<TextArea />组件中排除字母 "e",可以这样做:
- handleDescriptionChange(e) {
- const textArray = e.target.value.split('').filter(x => x !== 'e');
-
- console.log('string split into array of letters',textArray);
-
- const filteredText = textArray.join('');
- this.setState({ description: filteredText });
- }
把 e.target.value 字符串分割成字母数组,就生成了上述的 textArray。这样字母 “e” (或其他设法排除的字母)就被过滤掉了。再把剩余的字母组成的数组拼成字符串,最后用该新字符串去设置组件 state。还不错吧?
以上代码放在本文的仓库中,但我将它们注释掉了,你可以按自己的需求自由地调整。
下面是我承诺给你们的<FormContainer />组件完整代码,
- import React, {Component} from 'react';
- import CheckboxOrRadioGroup from '../components/CheckboxOrRadioGroup';
- import SingleInput from '../components/SingleInput';
- import TextArea from '../components/TextArea';
- import Select from '../components/Select';
-
- class FormContainer extends Component {
- constructor(props) {
- super(props);
- this.state = {
- ownerName: '',
- petSelections: [],
- selectedPets: [],
- ageOptions: [],
- ownerAgeRangeSelection: '',
- siblingOptions: [],
- siblingSelection: [],
- currentPetCount: 0,
- description: ''
- };
- this.handleFormSubmit = this.handleFormSubmit.bind(this);
- this.handleClearForm = this.handleClearForm.bind(this);
- this.handleFullNameChange = this.handleFullNameChange.bind(this);
- this.handleCurrentPetCountChange = this.handleCurrentPetCountChange.bind(this);
- this.handleAgeRangeSelect = this.handleAgeRangeSelect.bind(this);
- this.handlePetSelection = this.handlePetSelection.bind(this);
- this.handleSiblingsSelection = this.handleSiblingsSelection.bind(this);
- this.handleDescriptionChange = this.handleDescriptionChange.bind(this);
- }
- componentDidMount() {
- // 模拟请求用户数据
- //(create-react-app 构建配置里包含了 fetch 的 polyfill)
- fetch('./fake_db.json')
- .then(res => res.json())
- .then(data => {
- this.setState({
- ownerName: data.ownerName,
- petSelections: data.petSelections,
- selectedPets: data.selectedPets,
- ageOptions: data.ageOptions,
- ownerAgeRangeSelection: data.ownerAgeRangeSelection,
- siblingOptions: data.siblingOptions,
- siblingSelection: data.siblingSelection,
- currentPetCount: data.currentPetCount,
- description: data.description
- });
- });
- }
- handleFullNameChange(e) {
- this.setState({ ownerName: e.target.value });
- }
- handleCurrentPetCountChange(e) {
- this.setState({ currentPetCount: e.target.value });
- }
- handleAgeRangeSelect(e) {
- this.setState({ ownerAgeRangeSelection: e.target.value });
- }
- handlePetSelection(e) {
- const newSelection = e.target.value;
- let newSelectionArray;
- if(this.state.selectedPets.indexOf(newSelection) > -1) {
- newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection)
- } else {
- newSelectionArray = [...this.state.selectedPets, newSelection];
- }
- this.setState({ selectedPets: newSelectionArray });
- }
- handleSiblingsSelection(e) {
- this.setState({ siblingSelection: [e.target.value] });
- }
- handleDescriptionChange(e) {
- this.setState({ description: e.target.value });
- }
- handleClearForm(e) {
- e.preventDefault();
- this.setState({
- ownerName: '',
- selectedPets: [],
- ownerAgeRangeSelection: '',
- siblingSelection: [],
- currentPetCount: 0,
- description: ''
- });
- }
- handleFormSubmit(e) {
- e.preventDefault();
-
- const formPayload = {
- ownerName: this.state.ownerName,
- selectedPets: this.state.selectedPets,
- ownerAgeRangeSelection: this.state.ownerAgeRangeSelection,
- siblingSelection: this.state.siblingSelection,
- currentPetCount: this.state.currentPetCount,
- description: this.state.description
- };
-
- console.log('Send this in a POST request:', formPayload)
- this.handleClearForm(e);
- }
- render() {
- return (
- <form className="container" onSubmit={this.handleFormSubmit}>
- <h5>Pet Adoption Form</h5>
- <SingleInput
- inputType={'text'}
- title={'Full name'}
- name={'name'}
- controlFunc={this.handleFullNameChange}
- content={this.state.ownerName}
- placeholder={'Type first and last name here'} />
- <Select
- name={'ageRange'}
- placeholder={'Choose your age range'}
- controlFunc={this.handleAgeRangeSelect}
- options={this.state.ageOptions}
- selectedOption={this.state.ownerAgeRangeSelection} />
- <CheckboxOrRadioGroup
- title={'Which kinds of pets would you like to adopt?'}
- setName={'pets'}
- type={'checkbox'}
- controlFunc={this.handlePetSelection}
- options={this.state.petSelections}
- selectedOptions={this.state.selectedPets} />
- <CheckboxOrRadioGroup
- title={'Are you willing to adopt more than one pet if we have siblings for adoption?'}
- setName={'siblings'}
- controlFunc={this.handleSiblingsSelection}
- type={'radio'}
- options={this.state.siblingOptions}
- selectedOptions={this.state.siblingSelection} />
- <SingleInput
- inputType={'number'}
- title={'How many pets do you currently own?'}
- name={'currentPetCount'}
- controlFunc={this.handleCurrentPetCountChange}
- content={this.state.currentPetCount}
- placeholder={'Enter number of current pets'} />
- <TextArea
- title={'If you currently own pets, please write their names, breeds, and an outline of their personalities.'}
- rows={5}
- resize={false}
- content={this.state.description}
- name={'currentPetInfo'}
- controlFunc={this.handleDescriptionChange}
- placeholder={'Please be thorough in your descriptions'} />
- <input
- type="submit"
- className="btn btn-primary float-right"
- value="Submit"/>
- <button
- className="btn btn-link float-left"
- onClick={this.handleClearForm}>Clear form</button>
- </form>
- );
- }
- }
-
- export default FormContainer;
我承认用 React 构建受控表单组件要做一些重复劳动(比如容器组件中的处理方法),但就你对应用的掌控度和 state 变更的透明度来说,预先投入精力是超值的。你的代码会变得可维护并且很高效。
如果想在我发布新文章时接到通知,你可以在博客的导航栏部分注册我的邮件发送清单。
文章转自 | 学致编程
文章地址 | 玩转 React 表单 —— 受控组件详解
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。