React Form组件的实现封装杂谈

前言

对于网页系统来说,表单提交是一种很常见的与用户交互的方式,比如提交订单的时候,需要输入收件人、手机号、地址等信息,又或者对系统进行设置的时候,需要填写一些个人偏好的信息。 表单提交是一种结构化的操作,可以通过封装一些通用的功能达到简化开发的目的。本文将讨论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组件实现有更多的了解,也欢迎留言讨论。

您可能感兴趣的文章:

  • 详解Angular Reactive Form 表单验证
  • Angular 4.x中表单Reactive Forms详解
(0)

相关推荐

  • 详解Angular Reactive Form 表单验证

    本文我们将介绍 Reactive Form 表单验证的相关知识,具体内容如下: 使用内建的验证规则 动态调整验证规则 自定义验证器 自定义验证器 (支持参数) 跨字段验证 基础知识 内建验证规则 Angular 提供了一些内建的 validators,我们可以在 Template-Driven 或 Reactive 表单中使用它们. 目前 Angular 支持的内建 validators 如下: required - 设置表单控件值是非空的. email - 设置表单控件值的格式是 email.

  • Angular 4.x中表单Reactive Forms详解

    Angular 4.x 中有两种表单: Template-Driven Forms - 模板驱动式表单 (类似于 Angular 1.x 中的表单 ) Reactive Forms (Model-Driven Forms) - 响应式表单 Template-Driven Forms (模板驱动表单) ,我们之前的文章已经介绍过了,了解详细信息,请查看 - Angular 4.x Template-Driven Forms. Contents ngModule and reactive forms

  • React Form组件的实现封装杂谈

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

  • 从零搭建react+ts组件库(封装antd)的详细过程

    目录 整体需求 开发与打包工具选型 使用webpack作为打包工具 使用babel来处理typescript代码 使用less-loader.css-loader等处理样式代码 项目搭建思路 整体结构 方案思路 项目搭建实施 初始化 Babel引入 了解Babel webpack的基于babel-loader的处理流程 引入React相关库(externals方式) 编写组件代码 编译打包组件库 效果演示 处理样式(less编译与css导出) 依赖引入 配置webpack 编写样式代码 编译组件

  • 在react项目中使用antd的form组件,动态设置input框的值

    问题: 创建账号时,输入账号后不搜索直接保存,提示查询后,再点搜索就不能搜索这个账号了 原因: 点击保存之后,对表单进行了验证,导致之后请求的数据无法在更新到input框中,也就是说即使在state中有值,也不会更新initialValue值,就导致搜索后的值不能正确填入input中,表单也就提交不了. 解决办法: 不使用initialValue设置动态更新的值,而是使用 this.props.form.setFieldValue({name:data}); 用于动态更新值,就可以解决了. if

  • React如何利用Antd的Form组件实现表单功能详解

    一.构造组件 1.表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等. 这里先引用了封装的表单域 <Form.Item /> 2.使用Form.create处理后的表单具有自动收集数据并校验的功能,但如果不需要这个功能,或者默认的行为无法满足业务需求,可以选择不使用Form.create并自行处理数据 经过Form.create()包装过的组件会自带this.props.form属性,this.props.form提供了很多API来处理数据,如getFieldDe

  • 详解对于React结合Antd的Form组件实现登录功能

    一.React 结合 Antd 实现登录功能 引入所需的 Antd 组件,代码如下所示: import { Form, Icon, Input, Button, message } from 'antd' 在 Login.jsx 中,创建一个 Login 组件.当对外暴露组件时,需要使用 Form 组件进行包装,包装 Form 组件生成一个新的组件 Form(Login),同时新组件会向 Form 组件传递一个强大的对象属性 form,这样就可以取到 Form 表单的值,这也是高阶组件和高阶函数

  • 浅谈React中组件间抽象

    关于今天要学习的组件间抽象其实我这小白看了几次还没弄明白,这次决定一探究竟.在组件构建中,通常有一类功能需要被不同的组件公用,此时就涉及抽象的概念,在React中我们主要了解mixin和高阶组件. mixin mixin的特性广泛存在于各个面向对象语言中,在ruby中,include关键词就是mixin,是将一个模块混入到另外一个模块中,或者是类中. 封装mixin方法 const mixin = function(obj, mixins) { const newObj = obj newObj

  • 详解使用React进行组件库开发

    最近针对日常业务需求使用react封装了一套[组件库], 大概记录下整个开发过程中的心得.由于篇幅原因,在这里只对开发过程中比较纠结的选型和打包等进行讨论,后续再对具体组件的封装进行讨论. 概述 我们都知道,组件化的开发模式对于我们的开发效率有着极大的提升,针对我们日常使用的基本组件进行封装,可以大量的简化我们对于基本UI的关注度,让我们的工作聚焦在业务逻辑上,很好的分离业务与基础UI的代码,使得整个项目更有调理,这也是我们要进行本组件库开发的原因. 然而现有React开源组件有很多,像ant-

  • Python Web框架之Django框架Form组件用法详解

    本文实例讲述了Python Web框架之Django框架Form组件用法.分享给大家供大家参考,具体如下: Form简介 在HTTP中,表单(form标签),是用来提交数据的,其action属性说明了其传输数据的方法:如何传.如何接收. 访问网站时,表单可以实现客户端与服务器之间的通信.例如查询,就用到了表单(其属性中,action=get). 再比如说注册与登陆,也是要用到表单的.但这里由于涉及到隐私问题,需要保证数据传输的安全性,因此其传输方法就应当使用post而非get. 总之,对客户端来

  • 利用Vue构造器创建Form组件的通用解决方法

    前言 在前端平常的业务中,无论是官网.展示页还是后台运营系统都离不开表单,它承载了大部分的数据采集工作.所以如何更好地实现它,是平常工作中的一个重要问题. 在应用Vue框架去开发业务时,会将页面上每个独立的可视/可交互区域拆分为一个组件,再通过多个组件的自由组合来组成新的页面.例如 <template> <header></header> ... <content></content> ... <footer></footer&

  • 详解在React中跨组件分发状态的三种方法

    当我问自己第一百次时,我正在研究一个典型的CRUD屏幕:"我应该将状态保留在这个组件中还是将其移动到父组件?". 如果需要对子组件的状态进行轻微控制.您可能也遇到了同样的问题. 让我们通过一个简单的例子和​​三种修复方法来回顾它.前两种方法是常见的做法,第三种方法不太常规. 问题: 为了向您展示我的意思,我将使用一个简单的书籍CRUD(译者注:增加(Create).读取查询(Retrieve).更新(Update)和删除(Delete))屏幕(如此简单,它没有创建和删除操作). 我们有

随机推荐