翻譯 | 玩轉 React 表單 —— 受控元件詳解

iKcamp發表於2017-07-27

本文涵蓋以下受控元件:

  • 文字輸入框
  • 數字輸入框
  • 單選框
  • 核取方塊
  • 文字域
  • 下拉選擇框

同時也包含:

  • 表單資料的清除和重置
  • 表單資料的提交
  • 表單校驗

點選這裡直接檢視示例程式碼。
檢視示例
請在執行示例時開啟瀏覽器的控制檯。

介紹

在學習 React.js 時我遇到了一個問題,那就是很難找到受控元件的真實示例。受控文字輸入框的例子倒是很豐富,但核取方塊、單選框、下拉選擇框的例子卻不盡人意。

本文列舉了真實的受控表單元件示例,要是我在學習 React 的時候早點發現這些示例就好了。除了日期和時間輸入框需要另開篇幅詳細討論,文中列舉了所有的表單元素。

有時候,為了減少開發時間,有時候人們很容易為了一些東西(譬如表單元素)引入一個庫。而對於表單,我發現當需要新增自定義行為或表單校驗時,使用庫會讓事情變得更復雜。不過一旦掌握合適的 React 模式,你會發現構建表單元件並非難事,並且有些東西完全可以自己動手,豐衣足食。請把本文的示例程式碼當作你建立表單元件的起點或靈感之源。

除了提供單獨的元件程式碼,我還將這些元件放進表單中,方便你理解子元件如何更新父元件 state ,以及接下來父元件如何通過 props(單向資料流)更新子元件。

注意:本表單示例由很讚的 create-react-app 構建配置生成,如果你還沒有安裝該構建配置,我強烈推薦你安裝一下(npm install -g create-react-app)。目前這是搭建 React 應用最簡單的方式。

什麼是受控元件?

受控元件有兩個特點:

  1. 受控元件提供方法,讓我們在每次 onChange 事件發生時控制它們的資料,而不是一次性地獲取表單資料(例如使用者點提交按鈕時)。“被控制“ 的表單資料儲存在 state 中(在本文示例中,是父元件或容器元件的 state)。
    (譯註:這裡作者的意思是通過受控元件, 可以跟蹤使用者操作表單時的資料,從而更新容器元件的 state ,再單向渲染表單元素 UI。如果不使用受控元件,在使用者實時操作表單時,比如在輸入框輸入文字時,不會同步到容器元件的 state,雖然能同步輸入框本身的 value,但與容器元件的 state 無關,因此容器元件只能在某一時間,比如提表單時一次性地拿到(通過 refs 或者選擇器)表單資料,而難以跟蹤)
  2. 受控元件的展示資料是其父元件通過 props 傳遞下來的。

這個單向迴圈 —— (資料)從(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 元件。

插曲: 容器(智慧)元件 VS 普通(木偶)元件

是時候提及一下容器(智慧)元件和普通(木偶)元件了。容器元件包含業務邏輯,它會發起資料請求或進行其他業務操作。普通元件則從它的父(容器)元件接收資料。木偶元件有可能觸發更新 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>
  );
}複製程式碼

我們勾勒出了應用基礎結構,接下來我們一起瀏覽下每個子元件的細節。

<SingleInput /> 元件

該元件可以是 textnumber 輸入框,這取決於傳入的 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:

  1. inputType:接收兩個字串:'text''number'。該設定指定渲染 <input type="text" /> 元件或 <input type="number" /> 元件。
  2. title:接收一個字串,我們將它渲染到輸入框的 label 元素中。
  3. name:輸入框的 name 屬性。
  4. controlFunc:它是從父元件或容器元件傳下來的方法。因為該方法掛載在 React 的 onChange 處理方法上,所以每當輸入框的輸入值改變時,該方法都會被執行,從而更新父元件或容器元件的 state。
  5. content:輸入框內容。受控輸入框只會顯示通過 props 傳入的資料。
  6. placeholder:輸入框的佔位符文字,是一個字串。

既然該元件不需要任何邏輯行為和內部 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 方法(它被傳入到 controlFunc prop 屬性)來更新 <FormContainer /> 容器元件的 state。

// FormContainer.js

handleFullNameChange(e) {  
  this.setState({ ownerName: e.target.value });
}
// constructor 方法裡別漏掉了這行:
// this.handleFullNameChange = this.handleFullNameChange.bind(this);複製程式碼

隨後我們將容器元件更新後的 state (譯註:這裡指 state 上掛載的 ownerName 屬性)通過 content prop 傳回 <SingleInput /> 元件。

<Select /> 元件

選擇元件(就是下拉選擇元件),接收以下 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
};複製程式碼
  1. name:填充表單元素上 name 屬性的字串變數。
  2. options:是一個陣列(本例是字串陣列)。通過在元件的 render 方法中使用 props.options.map(), 該陣列中的每一項都會被渲染成一個選擇項。
  3. selectedOption:用以顯示錶單填充的預設選項,或使用者已選擇的選項(例如當使用者編輯之前已提交過的表單資料時,可以使用這個 prop)。
  4. controlFunc:它是從父元件或容器元件傳下來的方法。因為該方法掛載在 React 的 onChange 處理方法上,所以每當改變選擇框元件的值時,該方法都會被執行,從而更新父元件或容器元件的 state。
  5. placeholder:作為佔位文字的字串,用來填充第一個 <option> 標籤。本元件中,我們將第一個選項的值設定成空字串(參看下面程式碼的第 10 行)。
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 /> 元件

<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
};複製程式碼
  1. title:一個字串,用以填充單選或核取方塊集合的 label 標籤內容。
  2. type:接收 'checkbox''radio' 兩種配置的一種,並用指定的配置渲染輸入框(譯註:這裡指複選輸入框或單選輸入框)。
  3. setName:一個字串,用以填充每個單選或核取方塊的 name 屬性值。
  4. options:一個由字串元素組成的陣列,陣列元素用以渲染每個單選框或核取方塊的值和 label 的內容。例如,['dog', 'cat', 'pony'] 陣列中的元素將會渲染三個單選框或核取方塊。
  5. selectedOptions:一個由字串元素組成的陣列,用來表示預選項。在示例 4 中,如果 selectedOptions 陣列包含 'dog''pony' 元素,那麼相應的兩個選項會被渲染成選中狀態,而 'cat' 選項則被渲染成未選中狀態。當使用者提交表單時,該陣列將會是使用者的選擇資料。
  6. controlFunc:一個方法,用來處理從 selectedOptions 陣列 prop 中新增或刪除字串的操作。

這是本表單應用中最有趣的元件,讓我們來看一下:

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。如果沒有 > -1selectedOptions 陣列中的第一個 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 /> 元件

<TextArea /> 和我們已提到的那些元件非常相似,除了 resizerows,目前你應該對它的 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
};複製程式碼
  1. title:接收一個字串,用以渲染文字域的 label 標籤內容。
  2. rows:接收一個整數,用來指定文字域的行數。
  3. name:文字域的 name 屬性。
  4. content:文字域的內容。受控元件只會顯示通過 props 傳入的資料。
  5. resize: 接受一個布林值,用來指定文字域能否調整大小。
  6. placeholder:充當文字域佔位文字的字串。
  7. controlFunc: 它是從父元件或容器元件傳下來的方法。因為該方法掛載在 React 的 onChange 處理方法上,所以每當改變選擇框元件的值時,該方法都會被執行,從而更新父元件或容器元件的 state。

<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 /> 元件。

表單操作

handleClearFormhandleFormSubmit 方法操作整個表單。

1. handleClearForm

既然我們在表單的各處都使用了單向資料流,那麼清除表單資料對我們來說也是小菜一碟。<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() 方法用來清除表單資料。

2. handleFormSubmit

為了提交表單資料,我們從 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 /> 元件

下面是我承諾給你們的 <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 變更的透明度來說,預先投入精力是超值的。你的程式碼會變得可維護並且很高效。

如果想在我釋出新文章時接到通知,你可以在部落格的導航欄部分註冊我的郵件傳送清單。

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

相關文章