React Form组件杂谈

前端之家收集整理的这篇文章主要介绍了React Form组件杂谈前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

前言

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

Form组件功能

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

  • 表单布局
  • 表单字段封装
  • 表单验证&错误提示
  • 表单提交

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

表单布局

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

  • 行内布局

  • 水平布局

  • 垂直布局

实现方式比较简单,嵌套css就行。比如form的结构是这样:

  1. <form class="form">
  2. <label class="label"/>
  3. <field class="field"/>
  4. </form>

对应3种布局,只需要在form标签增加对应的class:

  1. <!--行内布局-->
  2. <form class="form inline">
  3. <label class="label"/>
  4. <field class="field"/>
  5. </form>
  6.  
  7. <!--水平布局-->
  8. <form class="form horizontal">
  9. <label class="label"/>
  10. <field class="field"/>
  11. </form>
  12.  
  13. <!--垂直布局-->
  14. <form class="form vertical">
  15. <label class="label"/>
  16. <field class="field"/>
  17. </form>

相应的,要定义3种布局的css:

  1. .inline .label {
  2. display: inline-block;
  3. ...
  4. }
  5. .inline .field {
  6. display: inline-block;
  7. ...
  8. }
  9.  
  10. .horizontal .label {
  11. display: inline-block;
  12. ...
  13. }
  14. .horizontal .field {
  15. display: inline-block;
  16. ...
  17. }
  18.  
  19. .vertical .label {
  20. display: block;
  21. ...
  22. }
  23. .vertical .field {
  24. display: block;
  25. ...
  26. }

表单字段封装

字段封装部分一般是对组件库的组件针对Form再做一层封装,如Input组件、Select组件、CheckBox组件等。当现有的字段不能满足需求时,可以@R_301_210@。

表单的字段一般包括两部分,一部分是标题,另一部分是内容。ZentForm通过getControlGroup这一高阶函数对结构和样式做了一些封装,它的入参是要显示的组件:

  1. export default Control => {
  2. render() {
  3. return (
  4. <div className={groupClassName}>
  5. <label className="zent-form__control-label">
  6. {required ? <em className="zent-form__required">*</em> : null}
  7. {label}
  8. </label>
  9. <div className="zent-form__controls">
  10. <Control {...props} {...controlRef} />
  11. {showError && (
  12. <p className="zent-form__error-desc">{props.error}</p>
  13. )}
  14. {notice && <p className="zent-form__notice-desc">{notice}</p>}
  15. {helpDesc && <p className="zent-form__help-desc">{helpDesc}</p>}
  16. </div>
  17. </div>
  18. );
  19. }
  20. }

这里用到的label和error等信息,是通过Field组件传入的:

  1. <Field
  2. label="预约门店:"
  3. name="dept"
  4. component={CustomizedComp}
  5. validations={{
  6. required: true,}}
  7. validationErrors={{
  8. required: '预约门店不能为空',}}
  9. required
  10. />

这里的CustomizedComp是通过getControlGroup封装后返回的组件。

字段与表单之间的交互是一个需要考虑的问题,表单需要知道它包含的字段值,需要在适当的时机对字段进行校验。ZentForm的实现方式是在Form的高阶组件内维护一个字段数组,数组内容是Field的实例。后续通过操作这些实例的方法来达到取值和校验的目的。

ZentForm的使用方式如下:

  1. class FieldForm extends React.Component {
  2. render() {
  3. return (
  4. <Form>
  5. <Field
  6. name="name"
  7. component={CustomizedComp}
  8. </Form>
  9. )
  10. }
  11. }
  12.  
  13. export default createForm()(FieldForm);

其中Form和Field是组件库提供的组件,CustomizedComp是自定义的组件,createForm是组件库提供的高阶函数。在createForm返回的组件中,维护了一个fields的数组,同时提供了attachToForm和detachFromForm两个方法,来操作这个数组。这两个方法保存在context对象当中,Field就能在加载和卸载的时候调用了。简化后的代码如下:

  1. /**
  2. * createForm高阶函数
  3. */
  4. const createForm = (config = {}) => {
  5. ...
  6. return WrappedForm => {
  7. return class Form extends Component {
  8. constructor(props) {
  9. super(props);
  10. this.fields = [];
  11. }
  12. getChildContext() {
  13. return {
  14. zentForm: {
  15. attachToForm: this.attachToForm,detachFromForm: this.detachFromForm,}
  16. }
  17. }
  18. attachToForm = field => {
  19. if (this.fields.indexOf(field) < 0) {
  20. this.fields.push(field);
  21. }
  22. };
  23. detachFromForm = field => {
  24. const fieldPos = this.fields.indexOf(field);
  25. if (fieldPos >= 0) {
  26. this.fields.splice(fieldPos,1);
  27. }
  28. };
  29. render() {
  30. return createElement(WrappedForm,{...});
  31. }
  32. }
  33. }
  34. }
  35.  
  36. /**
  37. * Field组件
  38. */
  39. class Field extends Component {
  40. componentWillMount() {
  41. this.context.zentForm.attachToForm(this);
  42. }
  43. componentWillUnmount() {
  44. this.context.zentForm.detachFromForm(this);
  45. }
  46. render() {
  47. const { component } = this.props;
  48. return createElement(component,{...});
  49. }
  50. }

当需要获取表单字段值的时候,只需要遍历fields数组,再调用Field实例的相应方法就可以:

  1. /**
  2. * createForm高阶函数
  3. */
  4. const createForm = (config = {}) => {
  5. ...
  6. return WrappedForm => {
  7. return class Form extends Component {
  8. getFormValues = () => {
  9. return this.fields.reduce((values,field) => {
  10. const name = field.getName();
  11. const fieldValue = field.getValue();
  12. values[name] = fieldValue;
  13. return values;
  14. },{});
  15. };
  16. }
  17. }
  18. }
  19. /**
  20. * Field组件
  21. */
  22. class Field extends Component {
  23. getValue = () => {
  24. return this.state._value;
  25. };
  26. }

表单验证&错误提示

表单验证是一个重头戏,只有验证通过了才能提交表单。验证的时机也有多种,如字段变更时、鼠标移出时和表单提交时。ZentForm提供了一些常用的验证规则,如非空验证,长度验证,邮箱地址验证等。当然还能自定义一些更复杂的验证方式。自定义验证方法可以通过两种方式传入ZentForm,一种是通过给createForm传参:

  1. createForm({
  2. formValidations: {
  3. rule1(values,value){
  4. },rule2(values,}
  5. })(FormComp);

另一种方式是给Field组件传属性

  1. <Field
  2. validations={{
  3. rule1(values,}}
  4. validationErrors={{
  5. rule1: 'error1',rule2: 'error2'
  6. }}
  7. />

使用createForm传参的方式,验证规则是共享的,而Field的属性传参是字段专用的。validationErrors指定校验失败后的提示信息。这里的错误信息会显示在前面getControlGroup所定义HTML中{showError && (<p className="zent-form__error-desc">{props.error}</p>)}

ZentForm的核心验证逻辑是createForm的runRules方法

  1. runRules = (value,currentValues,validations = {}) => {
  2. const results = {
  3. errors: [],Failed: [],};
  4.  
  5. function updateResults(validation,validationMethod) {
  6. // validation方法可以直接返回错误信息,否则需要返回布尔值表明校验是否成功
  7. if (typeof validation === 'string') {
  8. results.errors.push(validation);
  9. results.Failed.push(validationMethod);
  10. } else if (!validation) {
  11. results.Failed.push(validationMethod);
  12. }
  13. }
  14. Object.keys(validations).forEach(validationMethod => {
  15. ...
  16. // 使用自定义校验方法或内置校验方法(可以按需添加
  17. if (typeof validations[validationMethod] === 'function') {
  18. const validation = validations[validationMethod](
  19. currentValues,value
  20. );
  21. updateResults(validation,validationMethod);
  22. } else {
  23. const validation = validationRules[validationMethod](
  24. currentValues,value,validations[validationMethod]
  25. );
  26. }
  27. });
  28. return results;
  29. };

默认的校验时机是字段值改变的时候,可以通过Field的validateOnChangevalidateOnBlur来改变校验时机。

  1. <Field
  2. validateOnChange={false}
  3. validateOnBlur={false}
  4. validations={{
  5. required: true,matchRegex: /^[a-zA-Z]+$/
  6. }}
  7. validationErrors={{
  8. required: '值不能为空',matchRegex: '只能为字母'
  9. }}
  10. />

对应的,在Field组件中有2个方法来处理change和blur事件:

  1. class Field extends Component {
  2. handleChange = (event,options = { merge: false }) => {
  3. ...
  4. this.setValue(newValue,validateOnChange);
  5. ...
  6. }
  7. handleBlur = (event,validateOnBlur);
  8. ...
  9. }
  10. setValue = (value,needValidate = true) => {
  11. this.setState(
  12. {
  13. _value: value,_isDirty: true,},() => {
  14. needValidate && this.context.zentForm.validate(this);
  15. }
  16. );
  17. };
  18. }

当触发验证的时候,ZentForm是会对表单对所有字段进行验证,可以通过指定relatedFields来告诉表单哪些字段需要同步进行验证。

表单提交

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

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

ZentForm通过handleSubmit高阶函数定义了上述几个步骤,只需要传入表单提交的逻辑即可:

  1. const handleSubmit = (submit,zentForm) => {
  2. const doSubmit = () => {
  3. ...
  4. result = submit(values,zentForm);
  5. ...
  6. return result.then(
  7. submitResult => {
  8. ...
  9. if (onSubmitSuccess) {
  10. handleOnSubmitSuccess(submitResult);
  11. }
  12. return submitResult;
  13. },submitError => {
  14. ...
  15. const error = handleSubmitError(submitError);
  16. if (error || onSubmitFail) {
  17. return error;
  18. }
  19.  
  20. throw submitError;
  21. }
  22. );
  23. }
  24. const afterValidation = () => {
  25. if (!zentForm.isValid()) {
  26. ...
  27. if (onSubmitFail) {
  28. handleOnSubmitError(new SubmissionError(validationErrors));
  29. }
  30. } else {
  31. return doSubmit();
  32. }
  33. };
  34. const allIsValidated = zentForm.fields.every(field => {
  35. return field.props.validateOnChange || field.props.validateOnBlur;
  36. });
  37.  
  38. if (allIsValidated) {
  39. // 不存在没有进行过同步校验的field
  40. afterValidation();
  41. } else {
  42. zentForm.validateForm(true,afterValidation);
  43. }
  44. }

使用方式如下:

  1. const { handleSubmit } = this.props;
  2. <Form onSubmit={handleSubmit(this.submit)} horizontal>

ZentForm不足之处

ZentForm虽然功能强大,但仍有一些待改进之处:

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

结语

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

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

猜你在找的React相关文章