使用Formik輕鬆開發更高質量的React表單(四)其他幾個API解析

舊話重提



前文中我特別提起Redux Form以及redux-form的問題,我覺得學習Formik你不得不提它們,當然還有它們的「老祖宗」React;既然選擇了,那麼你必須按照這個方向走下去。有一句叫作“沒有最好,只有更好”。這句話應用於開源技術的學習上也很貼切,基於React技術的表單開發,到底哪一種方案最好,相信國內外很多高手都在探討這個問題。較早的redux-form這個自不必說了,如果你選擇而且熱戀着redux,那麼很不難不注意redux-form,但是redux-form儘管在一定程度上簡化了基於redux的表單的開發但是也的確讓人覺得很辛苦,這一點從我自己追蹤學習redux-form來說就有深刻體會。而現在的Formik開發者也正是體會到了這種痛苦,於是基於前輩們的作者又開發了Formik,並提供口號“Build forms in React, without the tears”——能不能浪漫一些翻譯爲“React表單不相信眼淚”?(受名片《莫斯科不相信眼淚》誘發)。總之一句話,只有先了解了開發複雜React表單的痛苦你纔會深刻體會學習的Formik的必要性。當然,Formik本身也很年輕,只有1歲,版本目前是1.0.2。但是,我相信這個庫會很快地發展下去,除非短時間內出現了比Formik更爲優秀的React Form解決方案。

<Field />



<Field />會自動把表單中的輸入字段「加入」到Formik系統中。它使用name屬性匹配Formik中的狀態( state)。 <Field />會默認對應一個HTML的 <input />元素。複雜情形下,你可以改變這個底層元素——這可以通過指定此API的component屬性的方式實現(這些思路與redux-form都是一致的!)。在此,component屬性值可以是一個簡單的字符串,如“ select”,也可能是另一個複雜的React組件。當然, <Field /> 還擁有一個很重要的render屬性。
下面的代碼片斷給出了<Field />及其重要屬性(component屬性和render屬性)的典型應用展示。

import React from 'react';
import { Formik, Field } from 'formik';

const Example = () => (
  <div>
    <h1>My Form</h1>
    <Formik
      initialValues={{ email: '', color: 'red', firstName: '' }}
      onSubmit={(values, actions) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          actions.setSubmitting(false);
        }, 1000);
      }}
      render={(props: FormikProps<Values>) => (
        <form onSubmit={props.handleSubmit}>
          <Field type="email" name="email" placeholder="Email" />
          <Field component="select" name="color">
            <option value="red">Red</option>
            <option value="green">Green</option>
            <option value="blue">Blue</option>
          </Field>
          <Field name="firstName" component={CustomInputComponent} />
          <Field
            name="lastName"
            render={({ field /* _form */ }) => (
              <input {...field} placeholder="firstName" />
            )}
          />
          <button type="submit">Submit</button>
        </form>
      )}
    />
  </div>
);

const CustomInputComponent: React.SFC<
  FieldProps<Values> & CustomInputProps
> = ({
  field, // { name, value, onChange, onBlur }
  form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
  ...props
}) => (
  <div>
    <input type="text" {...field} {...props} />
    {touched[field.name] &&
      errors[field.name] && <div className="error">{errors[field.name]}</div>}
  </div>
);

字段級校驗

思路是使用:validate?: (value: any) => undefined | string | Promise<any>

你可以通過把一個具有校驗功能的函數傳遞給validate屬性來執行獨立的字段層面的校驗。此功能的觸發將會相應於在 <Field>的父級組件 <Formik>中指定的validateOnBlur 和validateOnChange配置選項,或者在withFormik方法調用中通過props指定的validateOnBlur 和validateOnChange這兩個選項。當然,校驗還會對應下面兩種情形:

(1)同步情形下,如果校驗無效將返回一個包含錯誤信息的字符串或者乾脆返回undefined。典型代碼如下:

// Synchronous validation for Field
const validate = value => {
  let errorMessage;
  if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) {
    errorMessage = 'Invalid email address';
  }
  return errorMessage;
};

(2)異步情形下,會返回一個能夠拋出包含錯誤信息的字符串異步的Promise對象。這種工作方式類似於Formik提供的validate函數,但不是返回一個errors對象,而僅返回一個字符串。

請參考下面的代碼:

// Async validation for Field
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const validate = value => {
  return sleep(2000).then(() => {
    if (['admin', 'null', 'god'].includes(value)) {
      throw 'Nice try';
    }
  });
};

【注意】考慮到i18n庫(實現界面的多語言版本所需要)的使用方面,TypeScript檢驗類型比較寬鬆——允許你返回一個函數(例如i18n('invalid'))。

特別提示

當你沒有使用定製組件而且你想訪問由<Field/>創建的底層的DOM結點(如調用focus)時,你可以通過把回調函數傳遞給innerRef屬性來實現。

<FieldArray />



<FieldArray />這個API本質上是一個有助於實現字段數組或者列表操作的組件。你可以傳遞給它一個name屬性——使其指向包含對應數組的values中的鍵所在路徑。於是,<FieldArray />可以讓你通過render這個prop訪問數組幫助方法(觀察下面代碼中的arrayHelpers)。 爲了方便起見,調用這些方法就可以觸發校驗並能管理表單字段的touched信息。

import React from 'react';
import { Formik, Form, Field, FieldArray } from 'formik';

//下面提供的表單示例中有一個可編輯的列表。緊鄰每一個輸入字段是控制插入與刪除的按鈕。
//若列表爲空,那麼會顯示一個添加項目的按鈕。

export const FriendList = () => (
  <div>
    <h1>Friend List</h1>
    <Formik
      initialValues={{ friends: ['jared', 'ian', 'brent'] }}
      onSubmit={values =>
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
        }, 500)
      }
      render={({ values }) => (
        <Form>
          <FieldArray
            name="friends"
            render={arrayHelpers => (
              <div>
                {values.friends && values.friends.length > 0 ? (
                  values.friends.map((friend, index) => (
                    <div key={index}>
                      <Field name={`friends.${index}`} />
                      <button
                        type="button"
                        onClick={() => arrayHelpers.remove(index)} // remove a friend from the list
                      >
                        -
                      </button>
                      <button
                        type="button"
                        onClick={() => arrayHelpers.insert(index, '')} // insert an empty string at a position
                      >
                        +
                      </button>
                    </div>
                  ))
                ) : (
                  <button type="button" onClick={() => arrayHelpers.push('')}>
                    {/* show this when user has removed all friends from the list */}
                    Add a friend
                  </button>
                )}
                <div>
                  <button type="submit">Submit</button>
                </div>
              </div>
            )}
          />
        </Form>
      )}
    />
  </div>
);

name: string

這個屬性指向values中相關聯的鍵的名字或者路徑。

validateOnChange?: boolean

默認值爲true,用來決定在運行任何數組操作後執行還是不執行表單校驗。

FieldArray——針對對象數組的使用情形

你還可以遍歷一個對象數組——通過使用一種格式爲object[index]property或者是object.index.property的方法,它們分別作爲<FieldArray />中的 <Field /> 或者<input />元素的name屬性的值。請參考下面代碼:

<Form>
  <FieldArray
    name="friends"
    render={arrayHelpers => (
      <div>
        {values.friends.map((friend, index) => (
          <div key={index}>
            <Field name={`friends[${index}]name`} />
            <Field name={`friends.${index}.age`} /> // both these conventions do
            the same
            <button type="button" onClick={() => arrayHelpers.remove(index)}>
              -
            </button>
          </div>
        ))}
        <button
          type="button"
          onClick={() => arrayHelpers.push({ name: '', age: '' })}
        >
          +
        </button>
      </div>
    )}
  />
</Form>

FieldArray校驗陷阱

當使用<FieldArray>時,進行校驗有些值得注意的地方。

第一,如果你使用validationSchema,並且你的表單正好有數組校驗需求 (例如一個最小長度值),以及在嵌套數組字段需求情況下,顯示錯誤信息時也需要小心一些——Formik/Yup會在外部顯示校驗錯誤信息。例如:

const schema = Yup.object().shape({
  friends: Yup.array()
    .of(
      Yup.object().shape({
        name: Yup.string()
          .min(4, 'too short')
          .required('Required'), // these constraints take precedence
        salary: Yup.string()
          .min(3, 'cmon')
          .required('Required'), // these constraints take precedence
      })
    )
    .required('Must have friends') // these constraints are shown if and only if inner constraints are satisfied
    .min(3, 'Minimum of 3 friends'),
});

既然Yup和你的定製校驗函數總會輸出字符串形式的錯誤信息,那麼你需要設法確定在顯示時是否你的嵌套錯誤信息是一個數組或者是一個字符串。
於是,爲了顯示“Must have friends”和“Minimum of 3 friends”(這是我們示例中的數組校驗約束)......

不利之處

// within a `FieldArray`'s render
const FriendArrayErrors = errors =>
  errors.friends ? <div>{errors.friends}</div> : null; // app will crash

有利之處

// within a FieldArray's render

const FriendArrayErrors = errors =>
  typeof errors.friends === 'string' ? <div>{errors.friends}</div> : null;

對於嵌套的字段錯誤信息而言,你應當假定並沒有預告定義對象的哪一部分——除非你事先檢查了它。這樣一來,你可以創建一個定製的<ErrorMessage />組件來幫助實現你的校驗,此組件的代碼類似如下:

import { Field, getIn } from 'formik';

const ErrorMessage = ({ name }) => (
  <Field
    name={name}
    render={({ form }) => {
      const error = getIn(form.errors, name);
      const touch = getIn(form.touched, name);
      return touch && error ? error : null;
    }}
  />
);
//使用上面定製組件的情形:
<ErrorMessage name="friends[0].name" />; // => null, 'too short', or 'required'

【注意】在Formik v0.12 / 1.0中,支持把一個新的meta屬性添加給Field和FieldArray,此屬性用於爲你提供類似於error和touch這樣的相關的元數據信息,這可以使你免於不得不使用Formik或者lodash的getIn方法來檢查是否你自己定義了路徑部分。

FieldArray幫助函數

下面的幫助函數可以經由render這個屬性用來輔助操作字段數組:


  • push: (obj: any) => void: 把一個值添加到數組最後**

  • swap: (indexA: number, indexB: number) => void: 交換數組中的兩個值

  • move: (from: number, to: number) => void: 把數組中的一個元素從一個索引位置移動到另一個索引位置

  • insert: (index: number, value: any) => void: 在一個給定索引位置插入一個元素

  • unshift: (value: any) => number: 把一個元素添加到數組開始並返回新的數組長度

  • remove<T>(index: number): T | undefined: 刪除指定索引位置的一個元素並返回這個元素

  • pop<T>(): T | undefined: 刪除並返回數組尾端的元素**


FieldArray的render方法

有三種方式可以渲染<FieldArray />中包含的內容。請參考下面的代碼:

  • <FieldArray name="..." component>

  • <FieldArray name="..." render>

情形一:render: (arrayHelpers: ArrayHelpers) => React.ReactNode

import React from 'react';
import { Formik, Form, Field, FieldArray } from 'formik'

export const FriendList = () => (
  <div>
    <h1>Friend List</h1>
    <Formik
      initialValues={{ friends: ['jared', 'ian', 'brent'] }}
      onSubmit={...}
      render={formikProps => (
        <FieldArray
          name="friends"
          render={({ move, swap, push, insert, unshift, pop }) => (
            <Form>
              {/*... use these however you want */}
            </Form>
          )}
        />
    />
  </div>
);

情況二:當component屬性爲React組件時

import React from 'react';
import { Formik, Form, Field, FieldArray } from 'formik'

export const FriendList = () => (
  <div>
    <h1>Friend List</h1>
    <Formik
      initialValues={{ friends: ['jared', 'ian', 'brent'] }}
      onSubmit={...}
      render={formikProps => (
        <FieldArray
          name="friends"
          component={MyDynamicForm}
        />
    />
  </div>
);

// 除去數組幫助函數,Formik中的狀態和它本身的幫助函數
// (例如values, touched, setXXX, etc)都是經由表單的prop形式提供的
//
export const MyDynamicForm = ({
  move, swap, push, insert, unshift, pop, form
}) => (
 <Form>
  {/**  whatever you need to do */}
 </Form>
);

<Form />



類似於<Field />, <Form />其實也是一個幫助性質的組件(helper component,用於簡化表單編寫並提高開發效率)。實際上,它是一個圍繞<form onSubmit={context.formik.handleSubmit} />實現的包裝器。這意味着,你不需要顯式地書寫<form onSubmit={props.handleSubmit} />——如果你不想幹的話。

只圍繞ReactDOM的使用情形

import React from 'react';
import { Formik, Field, Form } from 'formik';

const Example = () => (
  <div>
    <h1>My Form</h1>
    <Formik
      initialValues={{ email: '', color: 'red' }}
      onSubmit={(values, actions) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          actions.setSubmitting(false);
        }, 1000);
      }}
      component={MyForm}
    />
  </div>
);

const MyForm = () => (
  <Form>
    <Field type="email" name="email" placeholder="Email" />
    <Field component="select" name="color">
      <option value="red">Red</option>
      <option value="green">Green</option>
      <option value="blue">Blue</option>
    </Field>
    <button type="submit">Submit</button>
  </Form>
);

withFormik(options)



此方法用於構建一個高階React組件類,該類把props和form handlers (即"FormikBag")傳遞進你的根據提供的選項生成的組件中。

重要options成員解析


(1)displayName?: string

在你的內部表單組件是一個無狀態函數組件情況下,你可以使用displayName這個選項來給組件一個合適的名字——從而從React DevTools(調試工具,在我以前的博客中專門討論過)中可以更容易地觀察到它。如果指定了這個屬性,你的包裝表單中將顯示成這樣——Formik(displayName)。如果忽略這個屬性,顯示樣式爲Formik(Component)。不過,這個選項對於類組件(例如class XXXXX extends React.Component {..})並不必要。

(2)enableReinitialize?: boolean

默認爲false。這個屬性用來控制當包裝組件屬性變化(使用深度相等比較,using deep equality)時Formik是否應當復位表單。

(3)handleSubmit: (values: Values, formikBag: FormikBag) => void

這是表單提交處理器。其中的參照是描述你的表單的values對象,還有“FormikBag”。其中,FormikBag:(a)包含一個對象,此對象擁有被注入的屬性和方法的子集(如所有類似於set<Thing>格式的方法,還有方法resetForm);(b)包含任何屬性——這些屬性都將被傳遞給包裝的組件。

(4)The "FormikBag":

  • props (傳遞給包裝組件的props)
  • resetForm
  • setErrors
  • setFieldError
  • setFieldTouched
  • setFieldValue
  • setStatus
  • setSubmitting
  • setTouched
  • setValues
    【注意】errors,touched,status及所有的事件處理器都沒有包含在FormikBag中。

(5)isInitialValid?: boolean | (props: Props) => boolean

默認爲 false. 此選擇用於控制表單加載前isValid屬性的初始值。你還可以傳遞一個函數。 Useful for situations when you want to enable/disable a submit and reset buttons on initial mount.

(6)mapPropsToValues?: (props: Props) => Values

If this option is specified, then Formik will transfer its results into updatable form state and make these values available to the new component as props.values. If mapPropsToValues is not specified, then Formik will map all props that are not functions to the inner component's props.values. That is, if you omit it, Formik will only pass props where typeof props[k] !== 'function', where k is some key.

Even if your form is not receiving any props from its parent, use mapPropsToValues to initialize your forms empty state.

(7)validate?: (values: Values, props: Props) => FormikErrors<Values> | Promise<any>

【注意】Formik作者極力推薦使用validationSchema與Yup進行表單校驗。但是,就校驗這個任務而言,你可以任意選擇自己喜歡的直觀高效的校驗方案。

使用函數校驗表單的values對象。這個函數可能是下面兩種情形之一:

(A)同步函數,並且返回一個對象errors。
// Synchronous validation
const validate = (values, props) => {
  let errors = {};

  if (!values.email) {
    errors.email = 'Required';
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
    errors.email = 'Invalid email address';
  }

  //...

  return errors;
};

(B)異步函數,它返回一個包含errors對象的Promise。

// Async Validation
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const validate = (values, props) => {
  return sleep(2000).then(() => {
    let errors = {};
    if (['admin', 'null', 'god'].includes(values.username)) {
      errors.username = 'Nice try';
    }
    // ...
    if (Object.keys(errors).length) {
      throw errors;
    }
  });
};

(8)validateOnBlur?: boolean

默認爲true。在blur事件觸發時(更具體一些說,是當調用handleBlur,setFieldTouched或者是setTouched時)使用這個選項進行校驗。

(9)validateOnChange?: boolean

默認爲true。 通過此選項告訴Formik在change事件或者相關方法(更具體一些說,是當調用handleChange,setFieldValue或者setValues)觸發時進行校驗。

(10)validationSchema?: Schema | ((props: Props) => Schema)

這個屬性極爲重要,它定義了一個Yup模式( schema)或者是返回Yup模式的一個函數。在校驗是使用這個屬性是非常有用的。錯誤信息被映射到內部組件的errors對象。它的鍵應當匹配values中對應的鍵。

注入屬性與方法

這些與<Formik render={props => ...} />是一致的。

在Formik中使用connect()



connect()是一個高階組件,它用於把原始的Formik上下文以屬性方式(命名爲formik)注入到內部組件中。另外一個有趣的事實是:Formik在底層上也利用了connect()來封裝<Field/>,<FastField>和<Form>。因此,在開發定製組件時很有經驗的程序員可能會發現connect()還是很有用的。請參考下面的代碼瞭解這個函數的基本用法:

import { connect } from 'formik';

const SubmitCount = ({ formik }) => <div>{formik.submitCount}</div>;

export default connect(SubmitCount);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章