Formik and Yup

Formik

圖片來源

Formik

Formik 是一個輕巧的表單函式庫,主要做三件事情:第一,可從表單內部把值取出來(例如:做驗證)或把值設定給表單(例如:給值);第二,驗證和報錯;第三,表單的提交處理。由於 Formik 可以統一做好這三件事情,因此能讓表單在進行測試、重構和推理變得比較容易。

Formik 的 API 有 <Formik>withFormik()<Field /><FieldArray /><Form /><ErrorMessage />connet()<FastField />,以下一一說明。

<Formik>

<Formik> 用來建立表單,其屬性 render、component、children 用來指定表單的樣板,其中優先順序 component > render;將欄位指定 name or id 後,就可利用屬性 dirty 作 dirty check 以檢查欄位是否被更動,原理是利用 deeply equal 比對初始值 initialValues;屬性 errors 用來儲存各個欄位的錯誤訊息,並且若表單皆通過驗證,則 isValid 為 true;屬性 touched 可檢視各個欄位是否被觸碰(不一定有被修改);其他的 handler 像是 handleChange、handleBlur、handleSubmit 可分別用來操控表單的值被更新、移出欄位和提交表單的情況。

<Field /><FastField />

<Field /> 可自動將 <input><select>textarea 綁定在 Formik 上,它會利用 name 屬性來做表屬性的對應。

例如,原本表單的文字欄位是這樣寫的。

<input type='text' name='name' value={values.name} onChange={handleChange} onBlur={handleBlur} />

改寫如下,省去了重複對應和綁定工作。

<Field name='name' />

另外,<Field /> 樣板可客製化,用 children、render 或 component 的方式傳入皆可,並且欄位可獨立驗證。如果希望能有更好的效能,則可改用 <FastField />,這是表示效能被優化過的 <Field />,主要是利用 shouldComponentUpdate() 來做調整。

<Form />

<Form /> 會將 <form> 綁定在 Formik 上,並將 handleSubmit 和 handleReset 自動對應,因此,以下兩者是相同的。

<Form />

等同於

<form onReset={formikProps.handleReset} onSubmit={formikProps.handleSubmit} {...props} />

<ErrorMessage />

報錯訊息的顯示可以自己刻,或使用 Formik 提供的 <ErrorMessage />,而 <ErrorMessage /> 可傳入客製化的樣板。

例如,自己刻可能都是這麼寫的…

{
  errors.name && <div className='error-message'>{errors.name}</div>;
}

可改寫為

<ErrorMessage name='name' component='div' className='error-message' />

範例 1

這是一個簡易的註冊表單,共兩個欄位 Name 和 Email,初始值(initialValues)皆設定為空字串,按下 Reset 會清空表單,而 Submit 會送出表單。在此用屬性 validate 來設定要同步驗證的規則,例如:欄位 Name 是空或字數小於 3 時報錯,而欄位 Email 是空或不符合 @ 前後必須為英數時也會報錯。

values 表示目前欄位的值,errors 表示經由 validate 驗證後有哪些欄位是有錯誤的,在此會儲存錯誤訊息;isValid 表示表單的全部欄位是否皆通過驗證;dirty 表示表單是否等同於初始值;touched 表示哪些欄位被觸碰過,即使是未修改任何值。

例如,若目前 Name 輸入「Cat」,Email 輸入「sample」,由於驗證皆沒通過,因此 errors 會顯示錯誤訊息且 isValid 為 false;表單目前欄位的值已與 initialValues 不同,因此 dirty 為 true;Name 與 Email 皆觸碰過了,因此個別欄位的 touched 為 true。

Formik

若欄位皆輸入合法的值,按下 Submit 即會看到 console 出目前的欄位值。

Formik

console 結果。

Formik

點此看原始碼與 Demo。

驗證的部份也可以是非同步的,點此看原始碼與 Demo。

<FieldArray />

當欄位值是 array 或 list 即可使用 <FieldArray /> 來協助處理。

範例如下,在範例 1 中,name 與 email 兩個欄位是分別特別指定的,但在此可利用一個陣列 list 將兩個欄位都統一存在這裡,之後再用 map 迭代出來。

初始值設定。

Formik

表單改寫。

<form onSubmit={handleSubmit}>
  {values.list.map((item, index) => (
    <div className='field-control'>
      <div key={index} className='field'>
        <label>{item}</label>
        <input type='text' onChange={handleChange} onBlur={handleBlur} name={item} />
      </div>
      {errors[item] && <div className='error-message'>{errors[item]}</div>}
    </div>
  ))}
</form>

點此看原始碼與 Demo。

除了 touched 會顯示 list 的狀態外,其他結果皆同。

Formik

withFormik()

withFormik() 是個 HOC(Higher-Order Component),可用來將 props 和 form handlers 傳入表單元件中以供使用。

mapPropsToValues 的功用是讓 Formik 將表單的內部狀態或處理結果轉成 props 讓元件取用,若不指定則 Formik 只會將資料型別不是函式的部份轉成 props 讓表單取用,最常見的功用是表單欄位值的初始化;validate 是作為表單驗證;displayName 可以給表單名字。點此看範例。

connet()

connet() 也是個 HOC,功用是綁定任何東西到 Formik 的 context 上,用來內部建立 <Filed><Form> 或自行定義的元件。

範例如下,自製一個報錯元件,並且將 props 傳入,用以判斷目前是否有錯誤訊息和是否被觸碰過,若皆有就回傳報錯元件,反之則回傳 null。

import React from 'react';
import { connect, getIn } from 'formik';

const CustomErrorMessage = (props) => {
  const error = getIn(props.formik.errors, props.name);
  const touch = getIn(props.formik.touched, props.name);
  return touch && error ? <div className='error-message'>{error}</div> : null;
};

export default connect(CustomErrorMessage);

原先自製顯示的報錯部份。

{
  errors.name && <div className='error-message'>{errors.name}</div>;
}

{
  errors.email && <div className='error-message'>{errors.email}</div>;
}

改寫如下。

<CustomErrorMessage name='name' />

<CustomErrorMessage name='email' />

Yup

Formik 的驗證可在屬性 validate 自行定義外,也可搭配官方推薦的 Yup。Yup 使用 object schema 的語法,意即為 JavaScript 物件建立一套規則以便於驗證。

範例 2

與先前的範例大致相同,差異只在於是在 validationSchema 分別定義 name 與 email 兩個欄位的驗證方式

export default withFormik({
  mapPropsToValues: () => ({
    name: '',
    email: '',
  }),
  validationSchema: yup.object().shape({
    name: yup
      .string()
      .required('必填')
      .min(4, '字數必須超過 3 個字'),
    email: yup
      .string()
      .required('必填')
      .matches(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, '不合法的 Email,@ 前後必須為英數'),
  }),
  // ...
})(Contact);

點此看原始碼與 Demo。

注意,validate 與 validationSchema 無法同時使用,只能擇一。

參考資料


comments powered by Disqus