iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 8
2
Modern Web

React 30天系列 第 8

Day 08-淚水交織的表單(Form)

前情提要:
昨天又粗略的把事件處理複習一遍,其實跟事件處理密不可分的還有一個角色,對!它就是表單(Form)

處理表單過程中的愛恨糾葛簡直可以寫出一篇長恨歌。react處理的方式可以分成兩類,分別為:

  • Uncontrolled Components
  • Controlled Components

Uncontrolled Components

先講容易但react不推薦的Uncontrolled Components吧XD
Uncontrolled Components的特色就是表單裡面的data交給DOM自己處理,所以我們需要插手的只有使用ref來從DOM裡面取得表單value。在render mothod裡,藉由設定ref可以取得DOM節點或react element。
以下範例從react複製並說明用法(其實很多範例都是官方挖來的啦,有code才好懂啊!)
建立ref的方法:

  1. 在render method裡設定ref屬性
  2. 在constructor使用React.createRef()建立ref讓整個component都可以引用ref
    使用ref的方法:
  3. 透過this.input.current取得該element,如要取得value則使用this.input.current.value
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();  // 2. 使用React.createRef()
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value); // 3. 取得ref
    event.preventDefault(); // 昨天講到的避免瀏覽器默認行為,這邊的默認行為是form submit
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} /> {/* 1. 設定ref屬性 */}
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

看了uncontrolled components後,其實把它想像成一般DOM處理表單就很好理解,透過DOM處理任何事,就像jQueryㄧ樣。

Controlled Components

接下來是一舉一動都在掌握中的controlled components,它的特色就是監控使用者輸入行為,把使用者輸入的每一個字都透過state管理,我們來看看controlled components的使用範例:

  1. 初始化state,並設為空字串
  2. 在input element上設定value為state的textValue
  3. 設定onChange事件處理使用者輸入的字串
  4. 在處理onChange事件的method裡,取得當下的event.target.value更新textValue
  5. onSubmit時,可以取得state的值做最終資料確認,避免資料未經前端驗證就送出
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {textValue: ''}; // 1. 初始化state

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

  handleChange(event) {  // 4. 更新textValue
    this.setState({textValue: event.target.value});
  }

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

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input 
            type="text" 
            value={this.state.textValue}  {/* 2. 設定value為state的textValue */}
            onChange={this.handleChange}  {/* 3. 設定onChange事件處理使用者輸入的字串 */}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

其實不管是uncontrolled components或是controlled components都可以做到即時驗證的功能,關鍵點只在onChange事件監聽。

根據react官網的描述,在大多數的情況推薦使用controlled components,除了<input type="file" />,它被歸類於永遠都是uncontrolled components,因為file的value只能由使用者上傳,我們也無法做更動。

不同element也都有不同的value property,以下整理的表格來自Controlled and uncontrolled form inputs in React don't have to be complicated:

Element Value property Change callback New value in the callback
<input type="text" /> value="string" onChange event.target.value
<input type="checkbox" /> checked={boolean} onChange event.target.checked
<input type="radio" /> checked={boolean} onChange event.target.checked
<textarea /> value="string" onChange event.target.value
<select /> value="option value" onChange event.target.value

處理多個input element

當state要處理很多表單欄位時,如果一個一個欄位setState一定很擾民,因此react提出另一個方法告訴大家,不用擔心!只要把input的name跟state的key值互相對應,我們就可以透過event取得element的name,而這個name就等於我們在state初始化設定的key值,透過setState就可以輕易把target value對應key值更新儲存。

以實例來說,比如我有一個聯絡人表單,現在很簡單的只有姓名和地址,實際畫面如下:
https://i.imgur.com/zeBBhrk.gif
我想要共用同一個method更新state,程式碼如下:(codepen)
下面的component有幾個重點

  1. state的key值和input的name是相同的
  2. 兩個input element在onChange時都共用handleInputChange這個method
  3. 透過[name]取得name的value並更新state value
class Contact extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
      address: ''
    };

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

  handleInputChange(event) {
    const target = event.target;
    const { value, name } = target;

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

  render() {
    return (
      <form>
        <label>
          姓名
          <input
            name="name"
            type="text"
            value={this.state.name}
            onChange={this.handleInputChange} />
        </label>
        <label>
          地址
          <input
            name="address"
            type="text"
            value={this.state.address}
            onChange={this.handleInputChange} />
        </label>
        <hr/>
        <div>
          {this.state.name} <br/>
          {this.state.address}
        </div>
      </form>
    );
  }
}

ReactDOM.render(<Contact />, document.getElementById('app'));

Formik & Yup 處理表單的小幫手

光兩個input element就這麼冗長了,很難想像如果需要填寫其他資料的話,畫面看起來很簡單,背後處理很心酸。這實在不是一件開心的事,更不用說還沒處理到表單驗證這塊。
不哭不哭,市場上有很多解決方案,其中一個就是使用Formik。它的自述是Build forms in React, without tears. XDDDD
Formik幫我們處理三個惱人的地方

  1. 從state獲取表單的值
  2. 驗證和處理錯誤訊息
  3. 處理onSubmit

在表單驗證這邊,除了自己寫code驗證value外還可以引用第三方library Yup,Yup的描述也蠻爆笑的Dead simple Object schema validation XDDDD

試做一個簡單到炸的註冊表單

需求:

  1. email(type="email"), 密碼(type="password")和同意註冊條款(type="checkbox")
  2. 驗證規則:email(email格式,必填)、密碼(至少6字,必填)、同意條款(必勾選)
  3. 送出後alert form value並reset form

首先先在開發環境安裝formik和yup

yarn add formik yup

接著在src/components下建立register.js

import React from 'react'
import { withFormik, Form, Field, ErrorMessage } from 'formik'
import * as yup from 'yup'

const errMsg = {  // 錯誤訊息的style
  color: 'red',
  fontSize: '12px',
  paddingLeft: '5px'
};

// 表單內容 ▼
const Register = ({
  values,
  isSubmitting
}) => (
  <Form>
    <div>
      <Field type="email" name="email" placeholder="Email"/>
      <ErrorMessage name="email" component="span" style={errMsg}/>
    </div>
    <div>
      <Field type="password" name="password" placeholder="Password"/>
      <ErrorMessage name="password" component="span" style={errMsg}/>
    </div>
    <div>
      <label>
        <Field type="checkbox" name="rule" checked={values.rule}/>
        同意註冊<a href="#">條款</a>
      </label>
      <ErrorMessage name="rule" component="span" style={errMsg}/>
    </div>
    <button type="submit" disabled={isSubmitting}>送出</button>
  </Form>
)

export default withFormik({
  // input 預設值
  mapPropsToValues({ email, password, rule }) {
    return {
      email: email || '',
      password: password || '',
      rule: rule || false
    }
  },
  // 表單驗證條件&錯誤訊息
  validationSchema: yup.object().shape({
    email: yup.string().email('Email不符合格式').required('必填'),
    password: yup.string().min(6, '密碼至少大於6').required('必填'),
    rule: yup.boolean().oneOf([true], '一定要同意!'),
  }),
  // 點擊送出時
  handleSubmit(values, { resetForm, setSubmitting }) {
    setTimeout(() => {
      resetForm();  //重設表單
      setSubmitting(false); //狀態更新(true:傳送中, false:傳送完成)
      alert(JSON.stringify(values, null, 2));  //alert values
    }, 1000)
  }
})(Register)

最後一樣在index.js匯入register component

import React from 'react';
import ReactDOM from 'react-dom';

import Register from './src/components/register';

const App = () => {
  return (
    <div>
      <Register />
    </div>
  );
};

ReactDOM.render(<App/>, document.getElementById('app'));

實際操作畫面如下:
https://i.imgur.com/UbFo2yv.gif
github傳送門

這次的小練習沒有太多說明,寫完後大概也可以變成新的一篇(?!)
大家有空可以自己玩玩看!


今日總結:

  • Form的用法分為
    • controlled components (推薦)
    • uncontrolled components (input type="file"交給它)
  • Formik & Yup幫忙簡化冗長的表單處理

今日完結。


上一篇
Day 07-來點互動吧(Handling Events)
下一篇
Day 09-[番外]繽紛世界(CSS / Autoprefixer / SCSS with Parcel)
系列文
React 30天30

1 則留言

0
cythilya
iT邦新手 4 級 ‧ 2019-03-10 20:00:39

感謝分享,寫得很棒!

我要留言

立即登入留言