前言

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

Form组件功能

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

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

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

表单布局

常用的表单布局一般有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的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组件实现有更多的了解,也欢迎留言讨论。

React Form组件的实现封装杂谈的更多相关文章

  1. HTML5新增form控件和表单属性实例代码详解

    这篇文章主要介绍了HTML5新增form控件和表单属性实例代码详解,需要的朋友可以参考下

  2. HTML5表单验证特性(知识点小结)

    这篇文章主要介绍了HTML5表单验证特性的一些知识点,本文通过实例代码截图的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. amazeui页面分析之登录页面的示例代码

    这篇文章主要介绍了amazeui页面分析之登录页面的示例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  4. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  5. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  6. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  7. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  8. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  9. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  10. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部