react之form表单工具:formik+yup
从网上搜到的form表单解决方案如下:
1.uform,地址:https://uformjs.org/#/MpI2Ij/dNFzFyTb
UForm 和核心特性:
- 基于标准 JSON Schema 协议,数据化构建表单
- 基于 rxjs 对表单内部的副作用做统一管理,轻松解决各种复杂联动校验场景
- 支持各种表单布局方案
- 支持可视化构建表单
- 支持自定义组件扩展
- 分布式状态管理,表单性能更高
不足:基于styled-components来开发的,涉及到的自定义样式主要是Form和FormItem层面上的样式。
样式很难hack,很难自定义。
2.react-jsonschema-form,地址:https://github.com/rjsf-team/react-jsonschema-form
- 强耦合 Bootstrap,不方便扩展
- JSON 描述不能很好的在 JSX 中描述
- 没能很好的解决表单布局,表单联动的各种复杂问题
- 性能不行,内部数据管理走的 React 的全量 rerender 机制来做数据同步
最终使用:Formik+yup组合来处理表单。github上star数目最多。
formik地址:https://github.com/jaredpalmer/formik
yup地址:https://github.com/jquense/yup
使用实例:
a. 不与yup结合,只使用formik,form表单代码如下:
import React from 'react'; import { useFormik } from 'formik'; const validate = values => { const errors = {}; if (!values.firstName) { errors.firstName = 'Required'; } else if (values.firstName.length > 15) { errors.firstName = 'Must be 15 characters or less'; } if (!values.lastName) { errors.lastName = 'Required'; } else if (values.lastName.length > 20) { errors.lastName = 'Must be 20 characters or less'; } 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; }; const SignupForm = () => { const formik = useFormik({ initialValues: { firstName: '', lastName: '', email: '', }, validate, onSubmit: values => { alert(JSON.stringify(values, null, 2)); }, }); return ( <form onSubmit={formik.handleSubmit}> <label htmlFor="firstName">First Name</label> <input id="firstName" name="firstName" type="text" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.firstName} /> {formik.touched.firstName && formik.errors.firstName ? ( <div>{formik.errors.firstName}</div> ) : null} <label htmlFor="lastName">Last Name</label> <input id="lastName" name="lastName" type="text" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.lastName} /> {formik.touched.lastName && formik.errors.lastName ? ( <div>{formik.errors.lastName}</div> ) : null} <label htmlFor="email">Email Address</label> <input id="email" name="email" type="email" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.email} /> {formik.touched.email && formik.errors.email ? ( <div>{formik.errors.email}</div> ) : null} <button type="submit">Submit</button> </form> ); };
以上代码也是非常冗余。form表单校验全靠手动维护,很不方便。接下来
b.与yup结合。formik暴露了配置validationSchema与yup结合。示例如下:
import React from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; const SignupForm = () => { const formik = useFormik({ initialValues: { firstName: '', lastName: '', email: '', }, validationSchema: Yup.object({ firstName: Yup.string() .max(15, 'Must be 15 characters or less') .required('Required'), lastName: Yup.string() .max(20, 'Must be 20 characters or less') .required('Required'), email: Yup.string() .email('Invalid email address') .required('Required'), }), onSubmit: values => { alert(JSON.stringify(values, null, 2)); }, }); return ( <form onSubmit={formik.handleSubmit}> <label htmlFor="firstName">First Name</label> <input id="firstName" name="firstName" type="text" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.firstName} /> {formik.touched.firstName && formik.errors.firstName ? ( <div>{formik.errors.firstName}</div> ) : null} <label htmlFor="lastName">Last Name</label> <input id="lastName" name="lastName" type="text" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.lastName} /> {formik.touched.lastName && formik.errors.lastName ? ( <div>{formik.errors.lastName}</div> ) : null} <label htmlFor="email">Email Address</label> <input id="email" name="email" type="email" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.email} /> {formik.touched.email && formik.errors.email ? ( <div>{formik.errors.email}</div> ) : null} <button type="submit">Submit</button> </form> ); };
如上,dom节点处的代码依然是比较复杂和重复的。
c.formik提供了一个Field组件
import React from 'react'; import { Formik, Field, Form, ErrorMessage } from 'formik'; import * as Yup from 'yup'; const SignupForm = () => { return ( <Formik initialValues={{ firstName: '', lastName: '', email: '' }} validationSchema={Yup.object({ firstName: Yup.string() .max(15, 'Must be 15 characters or less') .required('Required'), lastName: Yup.string() .max(20, 'Must be 20 characters or less') .required('Required'), email: Yup.string() .email('Invalid email address') .required('Required'), })} onSubmit={(values, { setSubmitting }) => { setTimeout(() => { alert(JSON.stringify(values, null, 2)); setSubmitting(false); }, 400); }} > <Form> <label htmlFor="firstName">First Name</label> <Field name="firstName" type="text" /> <ErrorMessage name="firstName" /> <label htmlFor="lastName">Last Name</label> <Field name="lastName" type="text" /> <ErrorMessage name="lastName" /> <label htmlFor="email">Email Address</label> <Field name="email" type="email" /> <ErrorMessage name="email" /> <button type="submit">Submit</button> </Form> </Formik> ); };
Field组件默认渲染为input。
<Field name="message" as="textarea" className="form-input"/> // <select className="my-select"/> <Field name="colors" as="select" className="my-select"> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </Field>
最后,再进一步用法:
import React from 'react'; import ReactDOM from 'react-dom'; import { Formik, Form, useField } from 'formik'; import styled from '@emotion/styled'; import * as Yup from 'yup'; const MyTextInput = ({ label, ...props }) => { // useField() returns [formik.getFieldProps(), formik.getFieldMeta()] // which we can spread on <input> and also replace ErrorMessage entirely. const [field, meta] = useField(props); return ( <> <label htmlFor={props.id || props.name}>{label}</label> <input className="text-input" {...field} {...props} /> {meta.touched && meta.error ? ( <div className="error">{meta.error}</div> ) : null} </> ); }; const MyCheckbox = ({ children, ...props }) => { // We need to tell useField what type of input this is // since React treats radios and checkboxes differently // than inputs/select/textarea. const [field, meta] = useField({ ...props, type: 'checkbox' }); return ( <> <label className="checkbox"> <input type="checkbox" {...field} {...props} /> {children} </label> {meta.touched && meta.error ? ( <div className="error">{meta.error}</div> ) : null} </> ); }; // Styled components .... const StyledSelect = styled.select` /** ... * / `; const StyledErrorMessage = styled.div` /** ... * / `; const StyledLabel = styled.label` /** ...* / `; const MySelect = ({ label, ...props }) => { const [field, meta] = useField(props); return ( <> <StyledLabel htmlFor={props.id || props.name}>{label}</StyledLabel> <StyledSelect {...field} {...props} /> {meta.touched && meta.error ? ( <StyledErrorMessage>{meta.error}</StyledErrorMessage> ) : null} </> ); }; // And now we can use these const SignupForm = () => { return ( <> <h1>Subscribe!</h1> <Formik initialValues={{ firstName: '', lastName: '', email: '', acceptedTerms: false, // added for our checkbox jobType: '', // added for our select }} validationSchema={Yup.object({ firstName: Yup.string() .max(15, 'Must be 15 characters or less') .required('Required'), lastName: Yup.string() .max(20, 'Must be 20 characters or less') .required('Required'), email: Yup.string() .email('Invalid email address') .required('Required'), acceptedTerms: Yup.boolean() .required('Required') .oneOf([true], 'You must accept the terms and conditions.'), jobType: Yup.string() .oneOf( ['designer', 'development', 'product', 'other'], 'Invalid Job Type' ) .required('Required'), })} onSubmit={(values, { setSubmitting }) => { setTimeout(() => { alert(JSON.stringify(values, null, 2)); setSubmitting(false); }, 400); }} > <Form> <MyTextInput label="First Name" name="firstName" type="text" placeholder="Jane" /> <MyTextInput label="Last Name" name="lastName" type="text" placeholder="Doe" /> <MyTextInput label="Email Address" name="email" type="email" placeholder="jane@formik.com" /> <MySelect label="Job Type" name="jobType"> <option value="">Select a job type</option> <option value="designer">Designer</option> <option value="development">Developer</option> <option value="product">Product Manager</option> <option value="other">Other</option> </MySelect> <MyCheckbox name="acceptedTerms"> I accept the terms and conditions </MyCheckbox> <button type="submit">Submit</button> </Form> </Formik> </> ); };
实际使用:
1.新建一个form表单元素的通用js代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import { useField } from 'formik'; import React from 'react'; export const MyTextInput = ({ label, ...props }) => { // useField() returns [formik.getFieldProps(), formik.getFieldMeta()] // which we can spread on <input> and also replace ErrorMessage entirely. const [field, meta] = useField(props); return ( <> <label htmlFor={props.id || props.name}>{label}</label> <input className={meta.touched && meta.error?'text-input error-input':'text-input'} {...field} {...props} /> {meta.touched && meta.error ? ( <div className="error">{meta.error=="请输入"?meta.error+label:label+meta.error}</div> ) : null} </> ); }; export const MyCheckbox = ({ children, ...props }) => { // We need to tell useField what type of input this is // since React treats radios and checkboxes differently // than inputs/select/textarea. const [field, meta] = useField({ ...props, type: 'checkbox' }); return ( <> <label className="checkbox"> <input type="checkbox" {...field} {...props} /> {children} </label> {meta.touched && meta.error ? ( <div className="error">{meta.error}</div> ) : null} </> ); }; export const MySelect = ({ label, ...props }) => { const [field, meta] = useField(props); return ( <> <label htmlFor={props.id || props.name}>{label}</label> <select {...field} {...props} /> {meta.touched && meta.error ? ( <div className="error">{meta.error}</div> ) : null} </> ); };
2.新建一个统一校验规则js,代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import * as Yup from 'yup' /* 设置提示文字 */ export function yupSetLocale(){ Yup.setLocale({ mixed: { required:'请输入' }, string: { max: '字符最多${max}个' }, }); } export const password = {'message':'规则提示信息','reg':/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&*,.?></]+$)(?![a-z\d]+$)(?![A-Z\d]+$)(?![a-z!@#$%^&*,.?></]+$)(?![\d!@#$%^&*,.?></]+$)(?![A-Z!@#$%^&*,.?></]+$)[a-zA-z\d!@#$%^&*,.?></]{8,20}$/}
3.使用
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import React ,{Component} from 'react' import { Formik, Field, Form, ErrorMessage } from 'formik'; import {MyTextInput,MyCheckbox,MySelect} from './../../../common/formItem' import * as Yup from 'yup'; import * as myYup from './../../../common/validator'; /* 设置提示文字 */ myYup.yupSetLocale(); const SignupForm = () => { const formik = Yup.object({ firstName: Yup.string() .required().matches(myYup.password.reg,myYup.password.message), lastName: Yup.string() .max(20) .required(), email: Yup.string().email('Invalid email address').required(), acceptedTerms: Yup.boolean() .required('请选择') .oneOf([true], '你必须勾选'), jobType: Yup.string().required('请选择'), }) return ( <Formik initialValues={{ firstName: '', lastName: '', email: '' }} validationSchema={formik} onSubmit={(values, { setSubmitting }) => { setTimeout(() => { alert(JSON.stringify(values, null, 2)); setSubmitting(false); }, 400); }} > <Form> <div> <MyTextInput label="姓名" name="firstName" type="text" placeholder="Jane" /> </div> <div> <MyTextInput label="用户名" name="lastName" type="text" placeholder="Doe" /> </div> <div> <MyTextInput label="邮箱" name="email" type="email" placeholder="jane@formik.com" /> </div> <div> <MySelect label="Job Type" name="jobType"> <option value="">Select a job type</option> <option value="designer">Designer</option> <option value="development">Developer</option> <option value="product">Product Manager</option> <option value="other">Other</option> </MySelect> </div> <div> <MyCheckbox name="acceptedTerms"> I accept the terms and conditions </MyCheckbox> </div> <div> <button type="submit" >Submit</button> </div> </Form> </Formik> ); };