React Form组件的实现封装杂谈


Posted in Javascript onMay 07, 2018

前言

对于网页系统来说,表单提交是一种很常见的与用户交互的方式,比如提交订单的时候,需要输入收件人、手机号、地址等信息,又或者对系统进行设置的时候,需要填写一些个人偏好的信息。 表单提交是一种结构化的操作,可以通过封装一些通用的功能达到简化开发的目的。本文将讨论Form表单组件设计的思路,并结合有赞的ZentForm组件介绍具体的实现方式。本文所涉及的代码都是基于React v15的版本。

Form组件功能

一般来说,Form组件的功能包括以下几点:

  1. 表单布局
  2. 表单字段
  3. 封装表单验证&错误提示
  4. 表单提交

下面将对每个部分的实现方式做详细介绍。

表单布局

常用的表单布局一般有3种方式:

行内布局

React Form组件的实现封装杂谈

水平布局

React Form组件的实现封装杂谈

垂直布局

React Form组件的实现封装杂谈

实现方式比较简单,嵌套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的validateOnChangevalidateOnBlur来改变校验时机。

<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来告诉表单哪些字段需要同步进行验证。

表单提交

表单提交时,一般会经历如下几个步骤

  1. 表单验证
  2. 表单提交
  3. 提交成功处理
  4. 提交失败处理

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虽然功能强大,但仍有一些待改进之处:

  1. 父组件维护了所有字段的实例,直接调用实例的方法来取值或者验证。这种方式虽然简便,但有违React声明式编程和函数式编程的设计思想,并且容易产生副作用,在不经意间改变了字段的内部属性。
  2. 大部分的组件重使用了shouldComponentUpdate,并对state和props进行了深比较,对性能有比较大的影响,可以考虑使用PureComponent。
  3. 太多的情况下对整个表单字段进行了校验,比较合理的情况应该是某个字段修改的时候只校验本身,在表单提交时再校验所有的字段。
  4. 表单提交操作略显繁琐,还需要调用一次handleSubmit,不够优雅。

结语

本文讨论了Form表单组件设计的思路,并结合有赞的ZentForm组件介绍具体的实现方式。ZentForm的功能十分强大,本文只是介绍了其核心功能,另外还有表单的异步校验、表单的格式化和表单的动态添加删除字段等高级功能都还没涉及到,感兴趣的朋友可点击前面的链接自行研究。

希望阅读完本文后,你对React的Form组件实现有更多的了解,也欢迎留言讨论。

Javascript 相关文章推荐
js解析与序列化json数据(二)序列化探讨
Feb 01 Javascript
js获取键盘按键响应事件(兼容各浏览器)
May 16 Javascript
javascript实现日期格式转换
Dec 16 Javascript
jquery.uploadify插件在chrome浏览器频繁崩溃解决方法
Mar 01 Javascript
jQuery使用append在html元素后同时添加多项内容的方法
Mar 26 Javascript
JQuery中层次选择器用法实例详解
May 18 Javascript
jQuery Easyui 验证两次密码输入是否相等
May 13 Javascript
JS/jQuery判断DOM节点是否存在的简单方法
Nov 24 Javascript
基于$.ajax()方法从服务器获取json数据的几种方式总结
Jan 31 Javascript
Javascript如何实现双指控制图片功能
Feb 25 Javascript
Selenium执行Javascript脚本参数及返回值过程详解
Apr 01 Javascript
vue实现登录功能
Dec 31 Vue.js
vue如何将v-for中的表格导出来
May 07 #Javascript
浅谈Vue 数据响应式原理
May 07 #Javascript
浅谈Vue响应式(数组变异方法)
May 07 #Javascript
在HTML文档中嵌入JavaScript的四种方法
May 07 #Javascript
详解JavaScript的BUG和错误
May 07 #Javascript
vue实现个人信息查看和密码修改功能
May 06 #Javascript
基于vue-element组件实现音乐播放器功能
May 06 #Javascript
You might like
php通过字符串调用函数示例
2014/03/02 PHP
php检查是否是ajax请求的方法
2015/04/16 PHP
Laravel框架路由和控制器的绑定操作方法
2018/06/12 PHP
PHP7 mongoDB扩展使用的方法分享
2019/05/02 PHP
jquery text,radio,checkbox,select操作实现代码
2009/07/09 Javascript
关于js类的定义
2011/06/28 Javascript
拥抱模块化的JavaScript
2012/03/07 Javascript
javascript同页面多次调用弹出层具体实例代码
2013/08/16 Javascript
eclipse如何忽略js文件报错(附图)
2013/10/30 Javascript
javascript 获取网页标题代码实例
2014/01/22 Javascript
jQuery简单图表peity.js使用示例
2014/05/02 Javascript
node.js中的http.get方法使用说明
2014/12/14 Javascript
NodeJS学习笔记之Connect中间件模块(一)
2015/01/27 NodeJs
js实现微信分享代码
2020/10/11 Javascript
jQuery ajaxSubmit 实现ajax提交表单局部刷新
2016/07/04 Javascript
JS实现物体带缓冲的间歇运动效果示例
2016/12/22 Javascript
深入理解javascript的getTime()方法
2017/02/16 Javascript
React入门教程之Hello World以及环境搭建详解
2017/07/11 Javascript
ionic 3.0+ 项目搭建运行环境的教程
2017/08/09 Javascript
在vue-cli中组件通信的方法
2017/12/16 Javascript
Angularjs实现数组随机排序的方法
2018/10/02 Javascript
Vue动态路由缓存不相互影响的解决办法
2019/02/19 Javascript
Vue路由模块化配置的完整步骤
2019/08/14 Javascript
JS中比Switch...Case更优雅的多条件判断写法
2019/09/05 Javascript
Nuxt默认模板、默认布局和自定义错误页面的实现
2020/05/11 Javascript
[03:21]【TI9纪实】Old Boys
2019/08/23 DOTA
[01:03:41]DOTA2-DPC中国联赛 正赛 Dynasty vs XG BO3 第三场 2月2日
2021/03/11 DOTA
python 导入数据及作图的实现
2019/12/03 Python
python实现逆滤波与维纳滤波示例
2020/02/26 Python
解决python脚本中error: unrecognized arguments: True错误
2020/04/20 Python
纯css3制作的火影忍者写轮眼开眼至轮回眼及进化过程实例
2014/11/11 HTML / CSS
美国知名保健品网站:LuckyVitamin(支持中文)
2017/08/09 全球购物
设计模式的基本要素是什么
2014/04/21 面试题
软件设计的目标是什么
2016/12/04 面试题
单位一把手群众路线四风问题整改措施
2014/09/25 职场文书
2014年党支部工作总结
2014/11/13 职场文书