前言
对于网页系统来说, 表单提交是一种很常见的与用户交互的方式, 比如提交订单的时候, 需要输入收件人, 手机号, 地址等信息, 又或者对系统进行设置的时候, 需要填写一些个人偏好的信息. 表单提交是一种结构化的操作, 可以通过封装一些通用的功能达到简化开发的目的. 本文将讨论 Form 表单组件设计的思路, 并结合有赞的 ZentForm https://www.youzanyun.com/zanui/zent/zh/component/form 组件介绍具体的实现方式. 本文所涉及的代码都是基于 React v15 的版本.
Form 组件功能
一般来说, Form 组件的功能包括以下几点:
表单布局
表单字段
封装表单验证 & 错误提示
表单提交
下面将对每个部分的实现方式做详细介绍.
表单布局
常用的表单布局一般有 3 种方式:
行内布局
水平布局
垂直布局
实现方式比较简单, 嵌套 CSS 就行. 比如 form 的结构是这样:
- <form class="form">
- <label class="label"/>
- <field class="field"/>
- </form>
对应 3 种布局, 只需要在 form 标签增加对应的 class:
- <!-- 行内布局 -->
- <form class="form inline">
- <label class="label"/>
- <field class="field"/>
- </form>
- <!-- 水平布局 -->
- <form class="form horizontal">
- <label class="label"/>
- <field class="field"/>
- </form>
- <!-- 垂直布局 -->
- <form class="form vertical">
- <label class="label"/>
- <field class="field"/>
- </form>
相应的, 要定义 3 种布局的 css:
- .inline .label {
- display: inline-block;
- ...
- }
- .inline .field {
- display: inline-block;
- ...
- }
- .horizontal .label {
- display: inline-block;
- ...
- }
- .horizontal .field {
- display: inline-block;
- ...
- }
- .vertical .label {
- display: block;
- ...
- }
- .vertical .field {
- display: block;
- ...
- }
表单字段封装
字段封装部分一般是对组件库的组件针对 Form 再做一层封装, 如 Input 组件, Select 组件, Checkbox 组件等. 当现有的字段不能满足需求时, 可以自定义字段.
表单的字段一般包括两部分, 一部分是标题, 另一部分是内容. ZentForm 通过 getControlGroup 这一高阶函数对结构和样式做了一些封装, 它的入参是要显示的组件:
- export default Control => {
- render() {
- return (
- <div className={groupClassName}>
- <label className="zent-form__control-label">
- {required ? <em className="zent-form__required">*</em> : null}
- {label}
- </label>
- <div className="zent-form__controls">
- <Control {...props} {...controlRef} />
- {showError && (
- <p className="zent-form__error-desc">{props.error}</p>
- )}
- {notice && <p className="zent-form__notice-desc">{notice}</p>}
- {helpDesc && <p className="zent-form__help-desc">{helpDesc}</p>}
- </div>
- </div>
- );
- }
- }
这里用到的 label 和 error 等信息, 是通过 Field 组件传入的:
- <Field
- label="预约门店:"
- name="dept"
- component={CustomizedComp}
- validations={{
- required: true,
- }}
- validationErrors={{
- required: '预约门店不能为空',
- }}
- required
- />
这里的 CustomizedComp 是通过 getControlGroup 封装后返回的组件.
字段与表单之间的交互是一个需要考虑的问题, 表单需要知道它包含的字段值, 需要在适当的时机对字段进行校验. ZentForm 的实现方式是在 Form 的高阶组件内维护一个字段数组, 数组内容是 Field 的实例. 后续通过操作这些实例的方法来达到取值和校验的目的.
ZentForm 的使用方式如下:
- class FieldForm extends React.Component {
- render() {
- return (
- <Form>
- <Field
- name="name"
- component={CustomizedComp}
- </Form>
- )
- }
- }
- export default createForm()(FieldForm);
其中 Form 和 Field 是组件库提供的组件, CustomizedComp 是自定义的组件, createForm 是组件库提供的高阶函数. 在 createForm 返回的组件中, 维护了一个 fields 的数组, 同时提供了 attachToForm 和 detachFromForm 两个方法, 来操作这个数组. 这两个方法保存在 context 对象当中, Field 就能在加载和卸载的时候调用了. 简化后的代码如下:
- /**
- * createForm 高阶函数
- */
- const createForm = (config = {}) => {
- ...
- return WrappedForm => {
- return class Form extends Component {
- constructor(props) {
- super(props);
- this.fields = [];
- }
- getChildContext() {
- return {
- zentForm: {
- attachToForm: this.attachToForm,
- detachFromForm: this.detachFromForm,
- }
- }
- }
- attachToForm = field => {
- if (this.fields.indexOf(field) <0) {
- this.fields.push(field);
- }
- };
- detachFromForm = field => {
- const fieldPos = this.fields.indexOf(field);
- if (fieldPos>= 0) {
- this.fields.splice(fieldPos, 1);
- }
- };
- render() {
- return createElement(WrappedForm, {...});
- }
- }
- }
- }
- /**
- * Field 组件
- */
- class Field extends Component {
- componentWillMount() {
- this.context.zentForm.attachToForm(this);
- }
- componentWillUnmount() {
- this.context.zentForm.detachFromForm(this);
- }
- render() {
- const { component } = this.props;
- return createElement(component, {...});
- }
- }
当需要获取表单字段值的时候, 只需要遍历 fields 数组, 再调用 Field 实例的相应方法就可以:
- /**
- * createForm 高阶函数
- */
- const createForm = (config = {}) => {
- ...
- return WrappedForm => {
- return class Form extends Component {
- getFormValues = () => {
- return this.fields.reduce((values, field) => {
- const name = field.getName();
- const fieldValue = field.getValue();
- values[name] = fieldValue;
- return values;
- }, {});
- };
- }
- }
- }
- /**
- * Field 组件
- */
- class Field extends Component {
- getValue = () => {
- return this.state._value;
- };
- }
表单验证 & 错误提示
表单验证是一个重头戏, 只有验证通过了才能提交表单. 验证的时机也有多种, 如字段变更时, 鼠标移出时和表单提交时. ZentForm 提供了一些常用的验证规则, 如非空验证, 长度验证, 邮箱地址验证等. 当然还能自定义一些更复杂的验证方式. 自定义验证方法可以通过两种方式传入 ZentForm, 一种是通过给 createForm 传参:
- createForm({
- formValidations: {
- rule1(values, value){
- },
- rule2(values, value){
- },
- }
- })(FormComp);
另一种方式是给 Field 组件传属性:
- <Field
- validations={{
- rule1(values, value){
- },
- rule2(values, value){
- },
- }}
- validationErrors={{
- rule1: 'error1',
- rule2: 'error2'
- }}
- />
使用 createForm 传参的方式, 验证规则是共享的, 而 Field 的属性传参是字段专用的. validationErrors 指定校验失败后的提示信息. 这里的错误信息会显示在前面 getControlGroup 所定义 html 中
{showError && (<p className="zent-form__error-desc">{props.error}</p>)}
ZentForm 的核心验证逻辑是 createForm 的 runRules 方法,
- runRules = (value, currentValues, validations = {}) => {
- const results = {
- errors: [],
- failed: [],
- };
- function updateResults(validation, validationMethod) {
- // validation 方法可以直接返回错误信息, 否则需要返回布尔值表明校验是否成功
- if (typeof validation === 'string') {
- results.errors.push(validation);
- results.failed.push(validationMethod);
- } else if (!validation) {
- results.failed.push(validationMethod);
- }
- }
- Object.keys(validations).forEach(validationMethod => {
- ...
- // 使用自定义校验方法或内置校验方法 (可以按需添加)
- if (typeof validations[validationMethod] === 'function') {
- const validation = validations[validationMethod](
- currentValues,
- value
- );
- updateResults(validation, validationMethod);
- } else {
- const validation = validationRules[validationMethod](
- currentValues,
- value,
- validations[validationMethod]
- );
- }
- });
- return results;
- };
默认的校验时机是字段值改变的时候, 可以通过 Field 的 validateOnChange 和 validateOnBlur 来改变校验时机.
- <Field
- validateOnChange={false}
- validateOnBlur={false}
- validations={{
- required: true,
- matchRegex: /^[a-zA-Z]+$/
- }}
- validationErrors={{
- required: '值不能为空',
- matchRegex: '只能为字母'
- }}
- />
对应的, 在 Field 组件中有 2 个方法来处理 change 和 blur 事件:
- class Field extends Component {
- handleChange = (event, options = { merge: false }) => {
- ...
- this.setValue(newValue, validateOnChange);
- ...
- }
- handleBlur = (event, options = { merge: false }) => {
- ...
- this.setValue(newValue, validateOnBlur);
- ...
- }
- setValue = (value, needValidate = true) => {
- this.setState(
- {
- _value: value,
- _isDirty: true,
- },
- () => {
- needValidate && this.context.zentForm.validate(this);
- }
- );
- };
- }
当触发验证的时候, ZentForm 是会对表单对所有字段进行验证, 可以通过指定 relatedFields 来告诉表单哪些字段需要同步进行验证.
表单提交
表单提交时, 一般会经历如下几个步骤
表单验证
表单提交
提交成功处理
提交失败处理
ZentForm 通过 handleSubmit 高阶函数定义了上述几个步骤, 只需要传入表单提交的逻辑即可:
- const handleSubmit = (submit, zentForm) => {
- const doSubmit = () => {
- ...
- result = submit(values, zentForm);
- ...
- return result.then(
- submitResult => {
- ...
- if (onSubmitSuccess) {
- handleOnSubmitSuccess(submitResult);
- }
- return submitResult;
- },
- submitError => {
- ...
- const error = handleSubmitError(submitError);
- if (error || onSubmitFail) {
- return error;
- }
- throw submitError;
- }
- );
- }
- const afterValidation = () => {
- if (!zentForm.isValid()) {
- ...
- if (onSubmitFail) {
- handleOnSubmitError(new SubmissionError(validationErrors));
- }
- } else {
- return doSubmit();
- }
- };
- const allIsValidated = zentForm.fields.every(field => {
- return field.props.validateOnChange || field.props.validateOnBlur;
- });
- if (allIsValidated) {
- // 不存在没有进行过同步校验的 field
- afterValidation();
- } else {
- zentForm.validateForm(true, afterValidation);
- }
- }
使用方式如下:
- const { handleSubmit } = this.props;
- <Form onSubmit={handleSubmit(this.submit)} horizontal>
ZentForm 不足之处
ZentForm 虽然功能强大, 但仍有一些待改进之处:
父组件维护了所有字段的实例, 直接调用实例的方法来取值或者验证. 这种方式虽然简便, 但有违 React 声明式编程和函数式编程的设计思想, 并且容易产生副作用, 在不经意间改变了字段的内部属性.
大部分的组件重使用了 shouldComponentUpdate, 并对 state 和 props 进行了深比较, 对性能有比较大的影响, 可以考虑使用 PureComponent.
太多的情况下对整个表单字段进行了校验, 比较合理的情况应该是某个字段修改的时候只校验本身, 在表单提交时再校验所有的字段.
表单提交操作略显繁琐, 还需要调用一次 handleSubmit, 不够优雅.
结语
本文讨论了 Form 表单组件设计的思路, 并结合有赞的 ZentForm https://www.youzanyun.com/zanui/zent/zh/component/form 组件介绍具体的实现方式. ZentForm 的功能十分强大, 本文只是介绍了其核心功能, 另外还有表单的异步校验, 表单的格式化和表单的动态添加删除字段等高级功能都还没涉及到, 感兴趣的朋友可点击前面的链接自行研究.
希望阅读完本文后, 你对 React 的 Form 组件实现有更多的了解, 也欢迎留言讨论.
来源: http://www.jb51.net/article/139600.htm