一个基本的例子
设想你要开发一个可以编辑用户数据的表单。不过,你的用户API端使用了具有类似下面的嵌套对象表达:
{ id: string,email: string,social: { facebook: string,twitter: string,// ... } }
最后,我们想使开发的对话框表单能够接收下面几个属性(props):user,updateUser和onClose(显然,user是一个对象,updateUser和onClose却都是两个方法)。
// User.js import React from 'react'; import Dialog from 'MySuperDialog'; import { Formik } from 'formik'; const EditUserDialog = ({ user,updateUser,onClose }) => { return ( <Dialog onClose={onClose}> <h1>Edit User</h1> <Formik initialValues={user /** { email,social } */} onSubmit={(values,actions) => { CallMyApi(user.id,values).then( updatedUser => { actions.setSubmitting(false); updateUser(updatedUser),onClose(); },error => { actions.setSubmitting(false); actions.setErrors(transformMyAPIErrorToAnObject(error)); } ); }} render={({ values,errors,touched,handleBlur,handleChange,handleSubmit,isSubmitting,}) => ( <form onSubmit={handleSubmit}> <input type="email" name="email" onChange={handleChange} onBlur={handleBlur} value={values.email} /> {errors.email && touched.email && <div>{errors.email}</div>} <input type="text" name="social.facebook" onChange={handleChange} onBlur={handleBlur} value={values.social.facebook} /> {errors.social && errors.social.facebook && touched.facebook && <div>{errors.social.facebook}</div>} <input type="text" name="social.twitter" onChange={handleChange} onBlur={handleBlur} value={values.social.twitter} /> {errors.social && errors.social.twitter && touched.twitter && <div>{errors.social.twitter}</div>} <button type="submit" disabled={isSubmitting}> Submit </button> </form> )} /> </Dialog> ); };
简化编码
为了简化表单组件的编码,Formik还提供了两个帮助API:
-
<Field>
-
<Form />
于是,下面的代码与前面一致,只是使用<Form />和<Field />这两个API进行了改写:
// EditUserDialog.js import React from 'react'; import Dialog from 'MySuperDialog'; import { Formik,Field,Form } from 'formik'; const EditUserDialog = ({ user,error => { actions.setSubmitting(false); actions.setErrors(transformMyAPIErrorToAnObject(error)); } ); }} render={({ errors,isSubmitting }) => ( <Form> <Field type="email" name="email" /> {errors.email && touched.social.email && <div>{errors.email}</div>} <Field type="text" name="social.facebook" /> {errors.social.facebook && touched.social.facebook && <div>{errors.social.facebook}</div>} <Field type="text" name="social.twitter" /> {errors.social.twitter && touched.social.twitter && <div>{errors.social.twitter}</div>} <button type="submit" disabled={isSubmitting}> Submit </button> </Form> )} /> </Dialog> ); };
React Native开发问题
Formik与React Native 和React Native Web开发完全兼容。然而,由于ReactDOM和React Native表单处理与文本输入方式的不同,有两个区别值得注意。本文将介绍这个问题并推荐更佳使用方式。
在进一步讨论前,先来最简要地概括一下如何在React Native中使用Formik。下面的轮廓代码展示了两者的关键区别:
// Formik +React Native示例 import React from 'react'; import { Button,TextInput,View } from 'react-native'; import { withFormik } from 'formik'; const enhancer = withFormik({ /*...*/ }); const MyReactNativeForm = props => ( <View> <TextInput onChangeText={props.handleChange('email')} onBlur={props.handleBlur('email')} value={props.values.email} /> <Button onPress={props.handleSubmit} title="Submit" /> </View> ); export default enhancer(MyReactNativeForm);
从上面代码中,你会明显注意到在React Native 和React DOM开发中使用Formik存在如下不同:
(1)Formik的props.handleSubmit被传递给一个<Button onPress={...} />,而不是HTML <form onSubmit={...} /> 组件(因为在React Native中没有<form />元素)。
(2)<TextInput />使用Formik的props.handleChange(fieldName)和handleBlur(fieldName),而不是直接把回调函数赋值给props,因为我们必须从某处得到fieldName,而在ReactNative中我们无法你在Web中一样自动获取它(使用input的name属性)。作为可选方案,你还可以使用 setFieldValue(fieldName,value) 和setTouched(fieldName,bool) 这两个函数。
避免在render中创建新函数
如果因某种原因你想在每一个render中避免创建新函数,那么我建议你把React Native的 <TextInput /> 当作它是一个第三方提供的定制输入元素:
- 编写你自己的针对定制输入元素的类包装器;
- 传递定制组件的props.setFieldValue,而不是传递props.handleChange;
- 使用一个定制的change函数回调,它将调用你传递给setFieldValue的任何内容。
请参考下面的代码:
// FormikReactNativeTextInput.js import * as React from 'react'; import { TextInput } from 'react-native'; export default class FormikReactNativeTextInput extends React.Component { handleChange = (value: string) => { // remember that onChangeText will be Formik's setFieldValue this.props.onChangeText(this.props.name,value); }; render() { // we want to pass through all the props except for onChangeText const { onChangeText,...otherProps } = this.props; return ( <TextInput onChangeText={this.handleChange} {...otherProps} // IRL,you should be more explicit when using TS /> ); } }
然后,你可以像下面这样使用这个定制输入组件:
// MyReactNativeForm.js import { View,Button } from 'react-native'; import TextInput from './FormikReactNativeTextInput'; import { Formik } from 'formik'; const MyReactNativeForm = props => ( <View> <Formik onSubmit={(values,actions) => { setTimeout(() => { console.log(JSON.stringify(values,null,2)); actions.setSubmitting(false); },1000); }} render={props => ( <View> <TextInput name="email" onChangeText={props.setFieldValue} value={props.values.email} /> <Button title="submit" onPress={props.handleSubmit} /> </View> )} /> </View> ); export default MyReactNativeForm;
使用TypeScript开发Formik表单
(一)TypeScript类型
Formik是使用TypeScript写的,Formik中的类型十分类似于React Router 4中的<Route>。
Render props (<Formik /> and <Field />) import * as React from 'react'; import { Formik,FormikProps,Form,FieldProps } from 'formik'; interface MyFormValues { firstName: string; } export const MyApp: React.SFC<{} /* whatever */> = () => { return ( <div> <h1>My Example</h1> <Formik initialValues={{ firstName: '' }} onSubmit={(values: MyFormValues) => alert(JSON.stringify(values))} render={(formikBag: FormikProps<MyFormValues>) => ( <Form> <Field name="firstName" render={({ field,form }: FieldProps<MyFormValues>) => ( <div> <input type="text" {...field} placeholder="First Name" /> {form.touched.firstName && form.errors.firstName && form.errors.firstName} </div> )} /> </Form> )} /> </div> ); };
(二)使用withFormik()
import React from 'react'; import * as Yup from 'yup'; import { withFormik,FormikErrors,Field } from 'formik'; // Shape of form values interface FormValues { email: string; password: string; } interface OtherProps { message: string; }
顺便提醒一下,你可以使用InjectedFormikProps<OtherProps,FormValues>来代替下面的实现方式。本质上,它们是相同的,只不过InjectedFormikProps是当Formik仅输出一个HOC(高阶组件)时的代替而已。而且,这个方法灵活性差一些,因为它需要对所有属性(props)进行包装。
const InnerForm = (props: OtherProps & FormikProps<FormValues>) => { const { touched,message } = props; return ( <Form> <h1>{message}</h1> <Field type="email" name="email" /> {touched.email && errors.email && <div>{errors.email}</div>} <Field type="password" name="password" /> {touched.password && errors.password && <div>{errors.password}</div>} <button type="submit" disabled={isSubmitting}> Submit </button> </Form> ); }; //MyForm接收的props的类型 interface MyFormProps { initialEmail?: string; message: string; // if this passed all the way through you might do this or make a union type } //使用withFormik高阶组件包装你的表单 const MyForm = withFormik<MyFormProps,FormValues>({ // Transform outer props into form values mapPropsToValues: props => { return { email: props.initialEmail || '',password: '',}; },//添加定制的校验函数(也有可能是异步的) validate: (values: FormValues) => { let errors: FormikErrors = {}; if (!values.email) { errors.email = 'required'; } else if (!isValidEmail(values.email)) { errors.email = 'Invalid email address'; } return errors; },handleSubmit: values => { // do submitting things },})(InnerForm); // 你可以在任何地方使用<MyForm /> const Basic = () => ( <div> <h1>My App</h1> <p>This can be anywhere in your application</p> <MyForm message="Sign up" /> </div> ); export default Basic;
Formik表单提交原理
要在Formik中提交表单,你需要以某种方式触发 handleSubmit(e) 或者submitForm属性调用(在Formik中这两个方法都是以属性的方式提供的)。 当调用其中一个方法时,Formik每次都会执行下面的伪代码:
(一)预提交
(1)修改所有字段
(2)把isSubmitting 设置为true
(3)submitCount + 1
(二)校验
(1)把isValidating设置为true
(2)异步运行所有字段级的校验和validationSchema,并深度合并执行结果
(3)判断是否存在错误:
如果存在错误:取消提交,把isValidating设置为false,设置错误信息,并把isSubmitting设置为false
如果不存在错误:Set isValidating to false,proceed to "Submission"
(三)提交
最后继续运行你的提交函数吧(例如是onSubmit或者handleSubmit)。你可以通过在你的处理器函数中调用setSubmitting(false) 来结束生命周期。
FAQ
(1)Q:怎么判定提交处理器(submission handler)正在执行中?
A:当isValidating为false且isSubmitting为true时。
(2)Q:为什么在提交前Formik要“润色一下(touch)”表单中所有字段?
A:通常,当UI表单中输入字段被操作过后(Formik中称为“touched”)只显示与之相关的错误信息。于是,在提交一个表单前,Formik会touch一下所有字段,这样所有可能隐藏的错误都会变得可见。
(3)Q:如何避免两次重复提交?
A:办法是当isSubmitting为true时,禁止所有能够触发提交的调用。
(4)Q:如何得知表单在提交前正在校验中?A:如果isValidating为true而且isSubmitting也为true的话,......