旧话重提
前文中我特别提起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 Fieldconst 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 Fieldconst 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 renderconst 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 validationconst 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 Validationconst 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);