React Forms 表單處理

表單 (form) 元件透過和使用者互動的過程會產生資料狀態的變化,表單元件像是 <input>, <textarea>, <select>, <option>

React 表單元件有一個重要的屬性 value 用來設定表單元件的值。React 還提供了 onChange 屬性用來設定 callback function 來監聽 (listen) 表單元件資料狀態的變化。當 <input>, <textarea> value 被改變時、當 <input> 被勾選/反勾選時、或當 <option> 被選取時,React 會執行 onChange callback 傳入一個 event object 來通知你元件的資料狀態有被改變。

受控元件 (Controlled Components)

React 稱有設定 value 屬性的表單元件叫做受控元件 (Controlled Components),受控元件的欄位內容值是使用者無法自由更動的,只能由你主動監聽 onChange 來改變 value 然後顯示回畫面,所以會叫做受控元件,通常有設定 value 的元件也會自己維護相對應的 state

例如像下面這個例子,欄位值會固定是 Hello 不會變:

<input type="text" value="Hello" />

點我看這個例子的結果 (你可以試試看在輸入框打字)

如果要將輸入框改變的內容能反應回畫面,你需要實作 onChange 事件 callback 來將資料狀態更新回 state。例如:

class MyForm extends React.Component {
  constructor(props) {
    super(props);
    
    // 初始化輸入框的 state 值為空
    this.state = {value: ''};

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

  // onChange 事件處理函示
  handleChange(event) {
    // event.target 是當前的 DOM elment
    // 從 event.target.value 取得 user 剛輸入的值
    // 將 user 輸入的值更新回 state
    this.setState({value: event.target.value});
  }

  // form onSubmit 事件處理函式
  handleSubmit(event) {
    alert('Submit ' + this.state.value);
    event.preventDefault();
  }

  render() {
    // 將 value 設定為 this.state.value
    // 並監聽 onChange 來更新 state
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="text" value={this.state.value} onChange={this.handleChange} />
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(
  <MyForm />,
  document.getElementById('root')
);

點我看這個例子的結果

非受控元件 (Uncontrolled Components)

相反的,沒有設定 value 屬性 (或將 value 設為 null value={null}) 的表單元件叫做非受控元件 (Uncontrolled Components),也就是讓正常的 DOM 行為自己管理元素狀態,通常我們會建議避免使用非受控元件。

例如:

<input type="text" />

使用者一打什麼就會立刻反應到畫面,跟一般的 HTML DOM 一樣。

點我看這個例子的結果 (你可以試試看在輸入框打字)

非受控元件可以用 defaultValue 屬性來給定一個預設值:

<input type="text" defaultValue="haha" />

點我看這個例子的結果

對於可以 checked 的欄位則是可以用 defaultChecked 來預設選取。

而像受控元件一樣,非受控元件也可以透過 onChange 事件監聽值的變化。

<textarea>

在 React 中,<textarea> 一樣是用 value 來設定值,不像一般 HTML textarea 的內容是放在子元素中。

像是:

<textarea value={this.state.value} onChange={this.handleChange} />

<select>

<select> 是一個下拉式的清單,在 HTML 中我們會使用 selected<option> 來選定一個值:

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

然而在 React 中,一樣是統一用 value 來指定:

<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>

如果是多重選單 (multiple options) 的語法則是這樣子:

<select multiple={true} value={['B', 'C']}>

<input type="file" />

file 元件比較特別,它是一個非受控元件,因為 file 是唯讀 (read-only) 的。

我們可以透過 JavaScript 的 File API 來存取值:

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  
  // onSubmit callback
  handleSubmit(event) {
    event.preventDefault();
    
    // 利用 JS File API 來操作 file 元素
    // files[0].name 可以拿到檔名
    alert(
      `Selected file - ${this.fileInput.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          上傳檔案:
          <input
            type="file"
            ref={input => {
              this.fileInput = input;
            }}
          />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

ReactDOM.render(
  <FileInput />,
  document.getElementById('root')
);

上面例子中有用到 React 的 ref 語法

點我看這個例子的結果

處理多個 Input 欄位的技巧

通常一個表單會有很多個 input 欄位,避免寫一堆不同的 onChange callback,你也可以用同一個 callback 然後用欄位的名稱 (name) 來做分辨:

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

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

  handleInputChange(event) {
    // 從 event object 拿到 target
    const target = event.target;
    
    // 從 target.type 可以知道欄位的 type
    // 分別再從 target.checked 得到選取的狀態
    // 或從 target.value 取出輸入的欄位值
    const value = target.type === 'checkbox' ? target.checked : target.value;
    
    // 從 target.name 得到該欄位設定的 name
    const name = target.name;

    // 分別更新不同 name 欄位的 state
    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>
    );
  }
}

ReactDOM.render(
  <Reservation />,
  document.getElementById('root')
);

上面例子中有用到的 [name]: value 語法是 ES6 的 computed property name

點我看這個例子的結果