转载

rc-form源码解读

在开发过程中,进行表单校验是一个很常用的功能。

表单校验通常需要实现以下几个功能:

  1. 收集各表单项的数据,如Input输入框,Select选择框等。

  2. 按照需求,对表单项数据进行校验,并显示校验结果。

  3. 需要提交表单时,对表单中所有数据进行校验,并收集所有数据。

这些功能看似简单,自己实现的话,还是会产生不少问题。因此,最好使用已有的库来实现此功能。我在开发中通常使用Ant Design的Form组件。从文档中的介绍可以看出,Form组件的功能实现主要是引用了rc-form。

rc-form在开发中帮了我不少忙,我对它的实现方式也很感兴趣,于是研究了它的源码,现在与大家分享一下。

阅读本文你将得到什么

我将为你梳理rc-form的主要实现思路,为你讲解rc-form源码中部分方法的用途,并提供部分代码的注释。

最后,我还会自己实现一个精简版的rc-form组件,供你参考。

开始前准备

  1. 本文中的Demo使用TypeScript编写,如果你对TypeScript不了解,可以查看TypeScript文档。但只要有JavaScript基础,就不会影响阅读。

  2. 为了方便理解,你还可以从GitHub下载 Ant Design 和 rc-form 的源码。

  3. 获取文中示例项目代码,请点击 这里 。

Demo运行方法:

$ yarn install
$ yarn start		# visit http://localhost:3000/
复制代码

运行后你会看到一个这样的Demo页面:

rc-form源码解读

rc-form表单Demo

下图是一个rc-form表单Demo,你可以在 http://localhost:3000/ 页面中,点击 表单弹窗 按钮,查看效果。

rc-form源码解读

这个表单实现了如下功能:

  1. 在用户输入时和点击确认按钮时,会进行表单项的非空校验。

  2. 用户输入和选择的结果,会显示在表单下方。

  3. 点击确认按钮,若校验通过,会弹窗提示用户输入结果。

rc-form表单Demo实现代码

该表单是使用Ant Design Form组件实现的,代码如下:

示例代码位置: /src/components/FormModal.tsx

import React from 'react'
import {Form, Input, Select} from 'antd'
import {FormComponentProps} from 'antd/lib/form'
import FormItem, {FormItemProps} from 'antd/lib/form/FormItem'
import Modal, {ModalProps} from 'antd/lib/modal'

const Option = Select.Option

// FormItem宽度兼容
export const formItemLayout: FormItemProps = {
  labelCol: {
    xs: {span: 24},
    sm: {span: 6}
  },
  wrapperCol: {
    xs: {span: 24},
    sm: {span: 16}
  }
}
// 性别枚举
enum SexEnum {
  male = 'male',
  female = 'female'
}

// 性别名称枚举
enum SexNameEnum {
  male = '男',
  female = '女'
}

// 表单字段类型
export class FormModalValues {
  username: string = ''
  sex: SexEnum = SexEnum.male
}

export interface Props extends ModalProps, FormComponentProps {

}

export class State {
  visible: boolean = false
}

export class FormModalComponent extends React.Component<Props, State> {

  constructor(props: Props) {
    super(props)

    this.state = new State()
  }

  // 打开弹窗
  public show = (): void => {
    this.setState({
      visible: true
    })
  }

  // 关闭弹窗
  public hide = (): void => {
    this.setState({
      visible: false
    })
  }

  public onOk = async () => {
    // 方法1:使用回调函数获取表单验证结果
    /* this.props.form.validateFields(async (errors: any, {username, sex}: FormModalValues) => {
      if (!errors) {
        Modal.success({
          title: '表单输入结果',
          content: `用户名:${username},性别:${SexNameEnum[sex]}。`
        })
        this.hide()
      }
    }) */
    // 方法2:使用async函数获取表单验证结果
    try {
      // @ts-ignore
      const {username, sex}: FormModalValues = await this.props.form.validateFields()
      Modal.success({
        title: '表单输入结果',
        content: `用户名:${username},性别:${SexNameEnum[sex]}。`
      })
      this.hide()
    } catch (error) {
      console.error(error)
      return error
    }
  }

  // 关闭弹窗后初始化弹窗参数
  public afterClose = (): void => {
    // 重置表单
    this.props.form.resetFields()
    this.setState(new State())
  }

  componentDidMount() {
    // 为表单设置初始值,这里与getFieldDecorator方法中的initialValue重复。
    this.props.form.setFieldsValue(new FormModalValues())
  }

  render() {
    const form = this.props.form
    // 获取用户输入的表单数据
    const username: string = form.getFieldValue('username')
    const sex: SexEnum = form.getFieldValue('sex')

    return (
      <Modal
        visible={this.state.visible}
        title={'新建用户'}
        maskClosable
        onCancel={this.hide}
        onOk={this.onOk}
        afterClose={this.afterClose}
      >
        <FormItem
          label={'请输入用户名'}
          required={true}
          {...formItemLayout}
        >
          {
            // getFieldDecorator为表单字段绑定value和onChange等事件,并实现校验等功能
            form.getFieldDecorator<FormModalValues>(
              // 表单项数据字段
              'username',
              {
                // 表单初始值
                initialValue: '',
                // 表单校验规则
                rules: [
                  {
                    required: true,
                  }
                ]
              }
            )(
              <Input />
            )
          }
        </FormItem>
        <FormItem
          label={'请选择性别'}
          required={true}
          {...formItemLayout}
        >
          {
            // getFieldDecorator为表单字段绑定value和onChange等事件,并实现校验等功能
            form.getFieldDecorator<FormModalValues>(
              // 表单项数据字段
              'sex',
              {
                // 表单初始值
                initialValue: SexEnum.male,
                // 表单校验规则
                rules: [
                  {
                    required: true,
                  }
                ]
              }
            )(
              <Select
                style={{width: '60px'}}
              >
                <Option
                  value={'male'}
                >
                  男
                </Option>
                <Option
                  value={'female'}
                >
                  女
                </Option>
              </Select>
            )
          }
        </FormItem>
        <FormItem
          label={'输入的用户名'}
          {...formItemLayout}
        >
          {username}
        </FormItem>
        <FormItem
          label={'选择的性别'}
          {...formItemLayout}
        >
          {
            SexNameEnum[sex]
          }
        </FormItem>
      </Modal>
    )
  }

}

const FormModal = Form.create<Props>()(FormModalComponent)

export default FormModal
复制代码

实现Demo用到的方法

在这个Demo中,我们主要用到了以下几个方法:

  1. Form.create:创建一个新的表单组件,提供表单校验、获取数据等方法,以及存储表单数据功能。

  2. this.props.form.getFieldDecorator:为表单字段绑定value和onChange等事件,并实现校验等功能。

  3. this.props.form.getFieldValue:获取表单字段值。

  4. this.props.form.setFieldsValue:为表单字段设置值。

  5. this.props.form.validateFields:进行表单校验,并返回校验结果和当前表单数据。

  6. this.props.form.resetFields:重置表单数据为初始值。

Form.create方法解读

上面列出的方法中,除了Form.create,都是rc-form提供的方法。

但如果查看create方法的实现方式,可以发现它直接调用了 rc-form 下的 createDOMForm ,如下:

示例代码位置: /ant-design/components/form/Form.tsx

import createDOMForm from 'rc-form/lib/createDOMForm';

static create = function create<TOwnProps extends FormComponentProps>(
  options: FormCreateOption<TOwnProps> = {},
): FormWrappedProps<TOwnProps> {
  return createDOMForm({
    fieldNameProp: 'id',
    ...options,
    fieldMetaProp: FIELD_META_PROP,
    fieldDataProp: FIELD_DATA_PROP,
  });
};
复制代码

createDOMForm方法解读

查看rc-form源码,可以看到 createDOMForm 方法仅仅是调用了 createBaseForm 方法。

createDOMForm示例代码位置: /rc-form/src/createDOMForm.js

function createDOMForm(option) {
  return createBaseForm({
    ...option,
  }, [mixin]);
}
复制代码

高阶组件(HOC)

createBaseForm的实现方式

现在我们的重点应当放在 createBaseForm 方法上,不过它的代码足足有600多行,很难在短时间内弄清楚所有细节。

但我们只要理解 createBaseForm 的大体结构,就可以知道它主要完成了哪些功能。

以下是我简化过的 createBaseForm 代码:

createBaseForm示例代码位置: /rc-form/src/createBaseForm.js

function createBaseForm(option = {}, mixins = []) {
  return function decorate(WrappedComponent) {
    const Form = createReactClass({
      render() {
        return <WrappedComponent {...props} />
      }
    })

    return Form
  }
}

export default createBaseForm
复制代码

从这段代码可以看出, createBaseForm 方法实际上就是实现了一个高阶组件(HOC)。

getFieldDecorator的实现方式

我们现在已经知道 createBaseForm 其实是一个高阶组件(HOC),那么再来看与之用法相似的 getFieldDecorator 方法,它的也是实现了一个高阶组件(HOC)`。

我简化过的 getFieldDecorator 代码如下:

getFieldDecorator示例代码位置: /rc-form/src/createBaseForm.js

getFieldDecorator(name, fieldOption) {
  const props = this.getFieldProps(name, fieldOption);

  return (fieldElem) => {
    return React.cloneElement(fieldElem, {
      ...props,
    });
  };
}
复制代码

高阶组件(HOC)简介

高阶组件是一个获取组件并返回新组件的函数。

高阶组件(HOC)有以下特点:

  1. 高阶组件是对已有组件的封装,形成了一个新组件,新组件实现了特定的业务逻辑,并将其通过props传给原有组件。

  2. 高阶组件通常不需要实现UI,其UI由传入的原组件实现,它只是为原组件提供了额外的功能或数据。

高阶组件(HOC)Demo

下面来看一个简单的HOC例子:

示例代码位置:/src/utils/createTimer.tsx

import React from 'react'

export interface Props {
  wrappedComponentRef?: React.RefObject<any>
}

export class State {
  time: Date = new Date()
}

export interface TimerProps {
  time: Date
}

function createTimer(WrappedComponent: React.ComponentClass<TimerProps>): React.ComponentClass<Props> {

  class Timer extends React.Component<Props, State> {

    timer: number = 0

    constructor(props: Props) {
      super(props)

      this.state = new State()
    }

    componentDidMount() {
      this.timer = window.setInterval(() => {
        this.setState({
          time: new Date()
        })
      }, 1000)
    }

    componentWillUnmount() {
      clearInterval(this.timer)
    }

    render() {
      // 为原组件提供time的props后,将其作为组件返回显示,不对UI做修改
      return (
        <WrappedComponent
          ref={this.props.wrappedComponentRef}
          time={this.state.time}
        />
      )
    }

  }

  // 返回新组件
  return Timer

}

export default createTimer
复制代码

这个例子实现了一个计时器的高阶组件,它将当前要显示的时间通过props中名为time的属性传入原组件。

同时,在返回的新组件中,可以通过设置wrappedComponentRef属性,可以获取到原组件。

高阶组件(HOC)的简单使用

下面是一个使用 createTimer 显示计时器的一个简单例子,该组件接收了HOC传过来的time属性,放入一个p标签中显示。

你可以在 http://localhost:3000/ 页面中, 表单弹窗 按钮下方看到显示的时间。

rc-form源码解读

示例代码位置:/src/components/ShowTimer.tsx

import React from 'react'
import moment from 'moment'
import createTimer, {TimerProps} from '../utils/createTimer';

// 表单字段类型
export interface Props extends TimerProps {

}

export class State {

}

export class ShowTimerComponent extends React.Component<Props, State> {

  constructor(props: Props) {
    super(props)

    this.state = new State()
  }

  render() {
    return (
      <p>
        {moment(this.props.time).format('YYYY-MM-DD HH:mm:ss')}
      </p>
    )
  }

}

// 导出用HOC创建的新组件
const ShowTimer = createTimer(ShowTimerComponent)

export default ShowTimer
复制代码

高阶组件(HOC)的结合弹窗使用

下面是一个使用 createTimer 创建弹窗显示计时器的例子,弹窗组件接收了HOC传过来的time属性,并将其显示出来。

同时将弹窗组件通过wrappedComponentRef属性提供给外部使用,实现了打开、关闭弹窗功能。

你可以在 http://localhost:3000/ 页面中,点击 时间弹窗 按钮,查看效果。

rc-form源码解读

示例代码位置:/src/components/ShowTimerModal.tsx

import React from 'react'
import moment from 'moment'
import {Modal} from 'antd';
import {ModalProps} from 'antd/lib/modal';
import createTimer, {TimerProps} from '../utils/createTimer';

// 表单字段类型
export interface Props extends ModalProps, TimerProps {

}

export class State {
  visible: boolean = false
}

export class ShowTimerModalComponent extends React.Component<Props, State> {

  constructor(props: Props) {
    super(props)

    this.state = new State()
  }

  // 打开弹窗
  public show = (): void => {
    this.setState({
      visible: true
    })
  }

  // 关闭弹窗
  public hide = (): void => {
    this.setState({
      visible: false
    })
  }

  render() {
    return (
      <Modal
        visible={this.state.visible}
        title={'弹窗显示时间'}
        maskClosable
        cancelButtonProps={{style: {display: 'none'}}}
        onCancel={this.hide}
        onOk={this.hide}
      >
        {moment(this.props.time).format('YYYY-MM-DD HH:mm:ss')}
      </Modal>
    )
  }

}

// 导出用HOC创建的新组件
const ShowTimerModal = createTimer(ShowTimerModalComponent)

export default ShowTimerModal
复制代码

rc-form源码解读

阅读源码的意见

有了HOC的知识作为铺垫,我们就可以正式进入rc-form源码解读了。

开始正式的解读之前,我先说说我个人对于阅读源码的意见,以rc-form为例,它实现的功能虽然不是十分复杂,但由于这是一个要提供给很多人使用的库,为了避免出错,就需要进行很多的校验,以及开发环境提示等等。虽然这些都是必要的,但却会导致代码十分冗长,如果对代码不熟悉的话,会有不小的阅读障碍。

因此,我个人认为在阅读源码的时候,可以把重点放在以下两个方面:

  1. 理解作者进行开发的思路。就如同谈Redux的时候,都要了解的Flux架构。建议在阅读源码的时候,重点放在理解作者“为什么要这么做”,而不是研究作者是如何实现某个功能的。

  2. 学习作者的优秀习惯、技巧。上面说重点要理解作者的思路,但并不是让你放弃关注细节,而是要有取舍地看。一些自己完全有能力实现,或者作者只是在做一些报错提示之类的代码,可以直接跳过。当然如果看到作者的一些优秀习惯、技巧,或者是一些自己没有想过的实现方式,还是很有必要借鉴的。

rc-form的实现思路

我梳理了rc-form的实现思路,供大家参考,本次源码解读会按照下图进行讲解。建议你在查看rc-form源码时,时常对照这张图,这样更加便于理解。

rc-form源码解读

在之前的高阶组件(HOC)讲解中,已经解读过 createBaseForm 方法的实现方式,这里就不再赘述。

接下来将依次以 createBaseForm 中的各个方法,讲解一下rc-form的实现逻辑,每段代码解读都会提供该段代码实现的主要功能,为方便理解,在代码中也提供了部分注释。

getInitialState方法解读

createBaseForm 使用了 createReactClass 方法创建一个React组件类, getInitialState 主要用来为组件创建一些初始化参数、方法等,相当于ES6中的 constructor

从代码中可以看到, createFieldsStore 方法为该组件创建了存储、读取、设置表单数据等功能,并存储在 this.fieldsStore 属性中。

表单原始数据,如initialValue(表单项初始值)、rules(表单校验规则)等,都会存储在 this.fieldsStore.fieldsMeta 属性中。

当前的表单数据,会存储在 this.fieldsStore.fields 属性中。

示例代码位置: /rc-form/src/createBaseForm.js

getInitialState() {
  // option.mapPropsToFields:	将值从props转换为字段。用于从redux store读取字段。
  const fields = mapPropsToFields && mapPropsToFields(this.props);
  // createFieldsStore为该组件提供了存储、读取、设置表单数据等功能
  this.fieldsStore = createFieldsStore(fields || {});

  this.instances = {};
  this.cachedBind = {};
  this.clearedFieldMetaCache = {};

  this.renderFields = {};
  this.domFields = {};

  // 为组件绑定了一系列方法,这些方法会通过props传入新组件供其使用
  // HACK: https://github.com/ant-design/ant-design/issues/6406
  ['getFieldsValue',
    'getFieldValue',
    'setFieldsInitialValue',
    'getFieldsError',
    'getFieldError',
    'isFieldValidating',
    'isFieldsValidating',
    'isFieldsTouched',
    'isFieldTouched'].forEach(key => {
      this[key] = (...args) => {
        if (process.env.NODE_ENV !== 'production') {
          warning(
            false,
            'you should not use `ref` on enhanced form, please use `wrappedComponentRef`. ' +
            'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140'
          );
        }

        // 该组件中的方法,直接调用了fieldsStore中的方法,也就是由createFieldsStore方法创建的
        return this.fieldsStore[key](...args);
      };
    });

  return {
    submitting: false,
  };
}
复制代码

render解读

render方法实现了组装新组件需要的props属性与方法,并将其传入新组件,这是一个普通的高阶组件实现方式。

新组件的props主要来自于 createBaseForm.jscreateForm.js 中定义的mixin对象,如下面的代码:

示例代码位置: /rc-form/src/createForm.js

export const mixin = {
  getForm() {
    return {
      getFieldsValue: this.fieldsStore.getFieldsValue,
      getFieldValue: this.fieldsStore.getFieldValue,
      getFieldInstance: this.getFieldInstance,
      setFieldsValue: this.setFieldsValue,
      setFields: this.setFields,
      setFieldsInitialValue: this.fieldsStore.setFieldsInitialValue,
      getFieldDecorator: this.getFieldDecorator,
      getFieldProps: this.getFieldProps,
      getFieldsError: this.fieldsStore.getFieldsError,
      getFieldError: this.fieldsStore.getFieldError,
      isFieldValidating: this.fieldsStore.isFieldValidating,
      isFieldsValidating: this.fieldsStore.isFieldsValidating,
      isFieldsTouched: this.fieldsStore.isFieldsTouched,
      isFieldTouched: this.fieldsStore.isFieldTouched,
      isSubmitting: this.isSubmitting,
      submit: this.submit,
      validateFields: this.validateFields,
      resetFields: this.resetFields,
    };
  },
};
复制代码

也就是说,mixin定义了需要传递给新组件使用的方法。

示例代码位置: /rc-form/src/createBaseForm.js

render() {
    const {wrappedComponentRef, ...restProps} = this.props; // eslint-disable-line
    const formProps = {
      // getForm方法来自于createDOMForm方法调用createBaseForm方法时,传入的mixin对象
      // mixin合并了createForm.js中导出的的mixin
      [formPropName]: this.getForm(),
    };
    if (withRef) {
      if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
        warning(
          false,
          '`withRef` is deprecated, please use `wrappedComponentRef` instead. ' +
          'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140'
        );
      }
      formProps.ref = 'wrappedComponent';
    } else if (wrappedComponentRef) {
      formProps.ref = wrappedComponentRef;
    }

    // 创建新组件的props
    const props = mapProps.call(this, {
      ...formProps,
      ...restProps,
    });
    return <WrappedComponent {...props} />;
  },
});
复制代码

getFieldDecorator(HOC)解读

getFieldDecorator 方法主要实现了一个高阶组件(HOC),它主要为新组件增加了绑定value属性和onChange事件,以及实现了onChange时的表单校验功能。

新组件的props是通过 getFieldProps 方法创建,该方法主要实现了绑定onChange事件,确保表单能够获取到表单项输入的值,在onChange的同时使用async-validator进行校验。

示例代码位置: /rc-form/src/createBaseForm.js

// 获取当前表单项的field、fieldMeta数据
onCollectCommon(name, action, args) {
  const fieldMeta = this.fieldsStore.getFieldMeta(name);
  if (fieldMeta[action]) {
    fieldMeta[action](...args);
  } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
    fieldMeta.originalProps[action](...args);
  }
  // 通过getValueFromEvent方法,从event中获取当前表单项的值,fieldMeta.getValueFromEvent为用户自定义的方法。
  const value = fieldMeta.getValueFromEvent ?
    fieldMeta.getValueFromEvent(...args) :
    getValueFromEvent(...args);
  if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) {
    const valuesAll = this.fieldsStore.getAllValues();
    const valuesAllSet = {};
    valuesAll[name] = value;
    Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]));
    onValuesChange({
      [formPropName]: this.getForm(),
      ...this.props
    }, set({}, name, value), valuesAllSet);
  }
  // 获取相应字段的field数据
  const field = this.fieldsStore.getField(name);
  return ({name, field: {...field, value, touched: true}, fieldMeta});
},

// 设置表单数据
onCollect(name_, action, ...args) {
  // 获取当前表单数据及设置
  const {name, field, fieldMeta} = this.onCollectCommon(name_, action, args);
  const {validate} = fieldMeta;

  this.fieldsStore.setFieldsAsDirty();

  const newField = {
    ...field,
    dirty: hasRules(validate),
  };
  this.setFields({
    [name]: newField,
  });
},

onCollectValidate(name_, action, ...args) {
  // 获取当前表单数据及设置
  const {field, fieldMeta} = this.onCollectCommon(name_, action, args);
  const newField = {
    ...field,
    dirty: true,
  };

  this.fieldsStore.setFieldsAsDirty();

  // 进行表单校验,并存储表单数据
  this.validateFieldsInternal([newField], {
    action,
    options: {
      firstFields: !!fieldMeta.validateFirst,
    },
  });
},

// 返回一个表单项的onChange事件
getCacheBind(name, action, fn) {
  if (!this.cachedBind[name]) {
    this.cachedBind[name] = {};
  }
  const cache = this.cachedBind[name];
  if (!cache[action] || cache[action].oriFn !== fn) {
    cache[action] = {
      fn: fn.bind(this, name, action),
      oriFn: fn,
    };
  }
  return cache[action].fn;
},

// 创建新的表单项组件
getFieldDecorator(name, fieldOption) {
  // 注册表单项,获取新表单项的props,主要是value属性和onChange事件等
  const props = this.getFieldProps(name, fieldOption);

  return (fieldElem) => {
    // We should put field in record if it is rendered
    this.renderFields[name] = true;

    const fieldMeta = this.fieldsStore.getFieldMeta(name);
    const originalProps = fieldElem.props;

    // 这段是在生产环境的打印提示语
    if (process.env.NODE_ENV !== 'production') {
      const valuePropName = fieldMeta.valuePropName;
      warning(
        !(valuePropName in originalProps),
        `/`getFieldDecorator/` will override /`${valuePropName}/`, ` +
        `so please don't set /`${valuePropName}/` directly ` +
        `and use /`setFieldsValue/` to set it.`
      );
      const defaultValuePropName =
        `default${valuePropName[0].toUpperCase()}${valuePropName.slice(1)}`;
      warning(
        !(defaultValuePropName in originalProps),
        `/`${defaultValuePropName}/` is invalid ` +
        `for /`getFieldDecorator/` will set /`${valuePropName}/`,` +
        ` please use /`option.initialValue/` instead.`
      );
    }

    fieldMeta.originalProps = originalProps;
    fieldMeta.ref = fieldElem.ref;

    return React.cloneElement(fieldElem, {
      ...props,
      // 该方法用于返回当前表单存储的value值
      ...this.fieldsStore.getFieldValuePropValue(fieldMeta),
    });
  };
},

// 创建表单项组件的props
getFieldProps(name, usersFieldOption = {}) {
  if (!name) {
    throw new Error('Must call `getFieldProps` with valid name string!');
  }
  if (process.env.NODE_ENV !== 'production') {
    warning(
      this.fieldsStore.isValidNestedFieldName(name),
      `One field name cannot be part of another, e.g. /`a/` and /`a.b/`. Check field: ${name}`
    );
    warning(
      !('exclusive' in usersFieldOption),
      '`option.exclusive` of `getFieldProps`|`getFieldDecorator` had been remove.'
    );
  }

  delete this.clearedFieldMetaCache[name];

  const fieldOption = {
    name,
    trigger: DEFAULT_TRIGGER,
    valuePropName: 'value',
    validate: [],
    ...usersFieldOption,
  };

  const {
    rules,
    trigger,
    validateTrigger = trigger,
    validate,
  } = fieldOption;

  const fieldMeta = this.fieldsStore.getFieldMeta(name);
  if ('initialValue' in fieldOption) {
    fieldMeta.initialValue = fieldOption.initialValue;
  }

  const inputProps = {
    ...this.fieldsStore.getFieldValuePropValue(fieldOption),
    ref: this.getCacheBind(name, `${name}__ref`, this.saveRef),
  };
  if (fieldNameProp) {
    inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name;
  }

  // 获取表单项校验触发事件及校验规则
  const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
  // 获取表单项校验事件
  const validateTriggers = getValidateTriggers(validateRules);
  validateTriggers.forEach((action) => {
    // 若已绑定了校验事件,则返回
    if (inputProps[action]) return;
    // 绑定收集表单数据及校验事件
    inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
  });

  // 若validateTriggers为空,则绑定普通事件,不进行校验
  // 使用onCollect方法,获取绑定表单项输入值事件,将其存储到inputProps中,并返回给组件用作props
  // make sure that the value will be collect
  if (trigger && validateTriggers.indexOf(trigger) === -1) {
    // getCacheBind负责返回一个表单项的onChange事件
    inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
  }

  // 将当前已设置的表单选项,与新表单选项合并,并存入fieldsMeta属性
  const meta = {
    ...fieldMeta,
    ...fieldOption,
    validate: validateRules,
  };
  // 注册表单项,将表单设置如initialValue、validateRules等,存储到this.fieldsStore.fieldsMeta[name]中
  this.fieldsStore.setFieldMeta(name, meta);
  if (fieldMetaProp) {
    inputProps[fieldMetaProp] = meta;
  }

  if (fieldDataProp) {
    inputProps[fieldDataProp] = this.fieldsStore.getField(name);
  }

  // This field is rendered, record it
  this.renderFields[name] = true;

  return inputProps;
},
复制代码

示例代码位置: /rc-form/src/utils.js

/**
 * 将validate、rules、validateTrigger三个参数配置的校验事件及规则,整理成统一的校验事件、规则
 * @param {Array<object>} validate 校验事件、规则
 * @param {string} validate[].trigger 校验事件
 * @param {object[]} validate[].rules 校验规则,参考async-validator,https://github.com/yiminghe/async-validator
 * @param {object[]} rules 校验规则,参考async-validator,https://github.com/yiminghe/async-validator
 * @param {string} validateTrigger 校验事件
 * @returns {Array<object>} validateRules 校验事件、规则
 * @returns {string[]} validateRules[].trigger 校验事件
 * @returns {object[]} validateRules[].rules 校验规则,参考async-validator,https://github.com/yiminghe/async-validator
 */
export function normalizeValidateRules(validate, rules, validateTrigger) {
  const validateRules = validate.map((item) => {
    const newItem = {
      ...item,
      trigger: item.trigger || [],
    };
    if (typeof newItem.trigger === 'string') {
      newItem.trigger = [newItem.trigger];
    }
    return newItem;
  });
  if (rules) {
    validateRules.push({
      trigger: validateTrigger ? [].concat(validateTrigger) : [],
      rules,
    });
  }
  return validateRules;
}

/**
 * 将validate、rules、validateTrigger三个参数配置的校验事件及规则,整理成统一的校验事件、规则
 * @param {Array<object>} validateRules 校验事件、规则
 * @param {string[]} validateRules[].trigger 校验事件
 * @param {object[]} validateRules[].rules 校验规则,参考async-validator,https://github.com/yiminghe/async-validator
 * @returns {Array<string>} 校验事件
 */
export function getValidateTriggers(validateRules) {
  return validateRules
    .filter(item => !!item.rules && item.rules.length)
    .map(item => item.trigger)
    .reduce((pre, curr) => pre.concat(curr), []);
}

// 判断表单项类型,获取表单数据,默认支持通过event.target.value或event.target.checked获取
export function getValueFromEvent(e) {
  // To support custom element
  if (!e || !e.target) {
    return e;
  }
  const {target} = e;
  return target.type === 'checkbox' ? target.checked : target.value;
}
复制代码

getFieldsValue/getFieldValue解读

createBaseForm.js 中并未实现 getFieldsValuegetFieldValue 方法,而是直接调用了 this.fieldsStore.getFieldsValuethis.fieldsStore.getFieldValue 方法,它们实现的功能是从存储的数据中,查找出指定的数据。

this.fieldsStore.getFieldsValue 方法如未指定需要查找的数据,则返回所有数据。

this.fieldsStore.getNestedField 是一个公用方法,根据传入的字段名,或者表单已存储的字段名,使用传入的回调函数获取所需的数据。

this.fieldsStore.getValueFromFields 方法,根据传入的字段名,获取当前表单的值,若值不存在,则返回已设置的initialValue。

示例代码位置: /rc-form/src/createFieldsStore.js

getFieldsValue = (names) => {
  return this.getNestedFields(names, this.getFieldValue);
}

getNestedFields(names, getter) {
  const fields = names || this.getValidFieldsName();
  return fields.reduce((acc, f) => set(acc, f, getter(f)), {});
}

getFieldValue = (name) => {
  const {fields} = this;
  return this.getNestedField(name, (fullName) => this.getValueFromFields(fullName, fields));
}

// 从传入的fields中,按name获取相应的值,若没有则直接返回fieldMeta中设置的initialValue
getValueFromFields(name, fields) {
  const field = fields[name];
  if (field && 'value' in field) {
    return field.value;
  }
  const fieldMeta = this.getFieldMeta(name);
  return fieldMeta && fieldMeta.initialValue;
}

// 根据传入的name,获取fieldMeta中存在的字段名称,最终调用getter函数获取相应的值
getNestedField(name, getter) {
  const fullNames = this.getValidFieldsFullName(name);
  if (
    fullNames.length === 0 || // Not registered
    (fullNames.length === 1 && fullNames[0] === name) // Name already is full name.
  ) {
    return getter(name);
  }
  const isArrayValue = fullNames[0][name.length] === '[';
  const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1;
  return fullNames
    .reduce(
      (acc, fullName) => set(
        acc,
        fullName.slice(suffixNameStartIndex),
        getter(fullName)
      ),
      isArrayValue ? [] : {}
    );
}

// 获取存储的表单字段名称
getValidFieldsFullName(maybePartialName) {
  const maybePartialNames = Array.isArray(maybePartialName) ?
    maybePartialName : [maybePartialName];
  return this.getValidFieldsName()
    .filter(fullName => maybePartialNames.some(partialName => (
      fullName === partialName || (
        startsWith(fullName, partialName) &&
        ['.', '['].indexOf(fullName[partialName.length]) >= 0
      )
    )));
}

// 获取存储的表单字段名称
getValidFieldsName() {
  const {fieldsMeta} = this;
  // 过滤出fieldsMeta中存储的未被设置为hidden的数据
  return fieldsMeta ?
    Object.keys(fieldsMeta).filter(name => !this.getFieldMeta(name).hidden) :
    [];
}

// 获取存储的字段数据
getFieldMeta(name) {
  this.fieldsMeta[name] = this.fieldsMeta[name] || {};
  return this.fieldsMeta[name];
}
复制代码

setFieldsValue解读

setFieldsValue 方法,实现的就是设置表单数据的功能,代码按如下流程调用:

this.setFieldsValuethis.setFieldsthis.fieldsStore.setFields

最终新数据存储在 this.fieldsStore.fields 中。

示例代码位置: /rc-form/src/createBaseForm.js

setFieldsValue(changedValues, callback) {
  const {fieldsMeta} = this.fieldsStore;

  // 过滤出已注册的表单项的值
  const values = this.fieldsStore.flattenRegisteredFields(changedValues);
  const newFields = Object.keys(values).reduce((acc, name) => {
    const isRegistered = fieldsMeta[name];
    if (process.env.NODE_ENV !== 'production') {
      warning(
        isRegistered,
        'Cannot use `setFieldsValue` until ' +
        'you use `getFieldDecorator` or `getFieldProps` to register it.'
      );
    }
    if (isRegistered) {
      const value = values[name];
      acc[name] = {
        value,
      };
    }
    return acc;
  }, {});

  // 设置表单的值
  this.setFields(newFields, callback);

  if (onValuesChange) {
    const allValues = this.fieldsStore.getAllValues();
    onValuesChange({
      [formPropName]: this.getForm(),
      ...this.props
    }, changedValues, allValues);
  }
}

setFields(maybeNestedFields, callback) {
  const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields);
  this.fieldsStore.setFields(fields);
  if (onFieldsChange) {
    const changedFields = Object.keys(fields)
      .reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {});
    onFieldsChange({
      [formPropName]: this.getForm(),
      ...this.props
    }, changedFields, this.fieldsStore.getNestedAllFields());
  }
  this.forceUpdate(callback);
}
复制代码

示例代码位置: /rc-form/src/createFieldsStore.js

// 设置表单值
setFields(fields) {
  const fieldsMeta = this.fieldsMeta;
  // 将当前数据和传入的新数据合并
  const nowFields = {
    ...this.fields,
    ...fields,
  };
  const nowValues = {};
  // 按照fieldsMeta中已注册的字段,从nowFields中取出这些字段的最新值,如果为空则设置为initialValue,形成新表单数据nowValues
  Object.keys(fieldsMeta)
    .forEach((f) => {
      nowValues[f] = this.getValueFromFields(f, nowFields);
    });
  // 如果该表单项有设置normalize方法,则返回normalize之后的数据
  // 可参考如这个例子:https://codepen.io/afc163/pen/JJVXzG?editors=001
  Object.keys(nowValues).forEach((f) => {
    const value = nowValues[f];
    const fieldMeta = this.getFieldMeta(f);
    if (fieldMeta && fieldMeta.normalize) {
      const nowValue =
        fieldMeta.normalize(value, this.getValueFromFields(f, this.fields), nowValues);
      if (nowValue !== value) {
        nowFields[f] = {
          ...nowFields[f],
          value: nowValue,
        };
      }
    }
  });
  this.fields = nowFields;
}
复制代码

resetFields解读

resetFields 方法,实现了重置表单为初始值功能。它的实现方式是:

this.fieldsStore.resetFields
this.fieldsStore.setFields

示例代码位置: /rc-form/src/createBaseForm.js

resetFields(ns) {
  // 获取清空了所有fields中存储数据的对象
  const newFields = this.fieldsStore.resetFields(ns);
  if (Object.keys(newFields).length > 0) {
    // 为newFields中的各个字段赋值,由于数据都为空,则会从fieldsMeta中查找initialValue赋值。
    this.setFields(newFields);
  }
  if (ns) {
    const names = Array.isArray(ns) ? ns : [ns];
    names.forEach(name => delete this.clearedFieldMetaCache[name]);
  } else {
    this.clearedFieldMetaCache = {};
  }
}
复制代码

示例代码位置: /rc-form/src/createFieldsStore.js

resetFields(ns) {
  const {fields} = this;
  // 获取需要重置的字段名称
  const names = ns ?
    this.getValidFieldsFullName(ns) :
    this.getAllFieldsName();

  // 如果当前fields中存在数据,则清空。最终返回的是清空了所有现有数据的对象。
  return names.reduce((acc, name) => {
    const field = fields[name];
    if (field && 'value' in field) {
      acc[name] = {};
    }
    return acc;
  }, {});
}
复制代码

getFieldError解读

this.getFieldError 用于获取传入表单项的校验结果,包括校验的属性名称和提示语。

this.getFieldError 的调用方式与 this.getFieldValue 类似,最终也是通过调用 this.fieldsStore.getNestedField 方法,同时传入相应的回调函数,获取到需要的校验结果。

示例代码位置: /rc-form/src/createBaseForm.js

// 获取校验结果,返回的是校验错误提示语的数组,格式为string[]
getFieldError = (name) => {
  return this.getNestedField(
    name,
    (fullName) => getErrorStrs(this.getFieldMember(fullName, 'errors'))
  );
}

// 根据传入的name,获取fieldMeta中存在的字段名称,最终调用getter函数获取相应的值
getNestedField(name, getter) {
  const fullNames = this.getValidFieldsFullName(name);
  if (
    fullNames.length === 0 || // Not registered
    (fullNames.length === 1 && fullNames[0] === name) // Name already is full name.
  ) {
    return getter(name);
  }
  const isArrayValue = fullNames[0][name.length] === '[';
  const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1;
  return fullNames
    .reduce(
      (acc, fullName) => set(
        acc,
        fullName.slice(suffixNameStartIndex),
        getter(fullName)
      ),
      isArrayValue ? [] : {}
    );
}

// 从当前字段数据中,获取传入member类型的数据
getFieldMember(name, member) {
  return this.getField(name)[member];
}

// 获取存储的字段数据及校验结果,并补充字段名称
getField(name) {
  return {
    ...this.fields[name],
    name,
  };
}
复制代码

示例代码位置: /rc-form/src/utils.js

// 将错误数据整理后返回,返回的是校验错误提示语的数组,格式为string[]
function getErrorStrs(errors) {
  if (errors) {
    return errors.map((e) => {
      if (e && e.message) {
        return e.message;
      }
      return e;
    });
  }
  return errors;
}
复制代码

validateFields解读

this.validateFields 实现的是,先过滤出有配置rules校验规则的表单项,调用 async-validator 进行校验,并返回校验结果。

示例代码位置: /rc-form/src/createBaseForm.js

// 校验表单方法
validateFieldsInternal(fields, {
  fieldNames,
  action,
  options = {},
}, callback) {
  const allRules = {};
  const allValues = {};
  const allFields = {};
  const alreadyErrors = {};
  // 先清空已有的校验结果
  fields.forEach((field) => {
    const name = field.name;
    if (options.force !== true && field.dirty === false) {
      if (field.errors) {
        set(alreadyErrors, name, {errors: field.errors});
      }
      return;
    }
    const fieldMeta = this.fieldsStore.getFieldMeta(name);
    const newField = {
      ...field,
    };
    newField.errors = undefined;
    newField.validating = true;
    newField.dirty = true;
    allRules[name] = this.getRules(fieldMeta, action);
    allValues[name] = newField.value;
    allFields[name] = newField;
  });
  // 设置清空后的表单校验结果
  this.setFields(allFields);
  // in case normalize
  Object.keys(allValues).forEach((f) => {
    allValues[f] = this.fieldsStore.getFieldValue(f);
  });
  if (callback && isEmptyObject(allFields)) {
    callback(isEmptyObject(alreadyErrors) ? null : alreadyErrors,
      this.fieldsStore.getFieldsValue(fieldNames));
    return;
  }

  // 使用AsyncValidator进行校验,并返回校验结果
  const validator = new AsyncValidator(allRules);
  if (validateMessages) {
    validator.messages(validateMessages);
  }
  validator.validate(allValues, options, (errors) => {
    const errorsGroup = {
      ...alreadyErrors,
    };
    // 如果校验不通过,则整理AsyncValidator返回的数据,并存储到表单数据中
    if (errors && errors.length) {
      errors.forEach((e) => {
        const errorFieldName = e.field;
        let fieldName = errorFieldName;

        // Handle using array validation rule.
        // ref: https://github.com/ant-design/ant-design/issues/14275
        Object.keys(allRules).some((ruleFieldName) => {
          const rules = allRules[ruleFieldName] || [];

          // Exist if match rule
          if (ruleFieldName === errorFieldName) {
            fieldName = ruleFieldName;
            return true;
          }

          // Skip if not match array type
          if (rules.every(({type}) => type !== 'array') && errorFieldName.indexOf(ruleFieldName) !== 0) {
            return false;
          }

          // Exist if match the field name
          const restPath = errorFieldName.slice(ruleFieldName.length + 1);
          if (/^/d+$/.test(restPath)) {
            fieldName = ruleFieldName;
            return true;
          }

          return false;
        });

        const field = get(errorsGroup, fieldName);
        if (typeof field !== 'object' || Array.isArray(field)) {
          set(errorsGroup, fieldName, {errors: []});
        }
        const fieldErrors = get(errorsGroup, fieldName.concat('.errors'));
        fieldErrors.push(e);
      });
    }
    const expired = [];
    const nowAllFields = {};
    Object.keys(allRules).forEach((name) => {
      const fieldErrors = get(errorsGroup, name);
      const nowField = this.fieldsStore.getField(name);
      // avoid concurrency problems
      if (!eq(nowField.value, allValues[name])) {
        expired.push({
          name,
        });
      } else {
        nowField.errors = fieldErrors && fieldErrors.errors;
        nowField.value = allValues[name];
        nowField.validating = false;
        nowField.dirty = false;
        nowAllFields[name] = nowField;
      }
    });
    // 存储新表单数据及结果
    this.setFields(nowAllFields);
    if (callback) {
      if (expired.length) {
        expired.forEach(({name}) => {
          const fieldErrors = [{
            message: `${name} need to revalidate`,
            field: name,
          }];
          set(errorsGroup, name, {
            expired: true,
            errors: fieldErrors,
          });
        });
      }

      callback(isEmptyObject(errorsGroup) ? null : errorsGroup,
        this.fieldsStore.getFieldsValue(fieldNames));
    }
  });
},

// 校验表单方法,主要用于整理需要校验的表单项数据后,调用validateFieldsInternal进行校验
validateFields(ns, opt, cb) {
  const pending = new Promise((resolve, reject) => {
    // 因传入的3个参数都为可选,需要将它们整理成固定的names, options, callback参数。
    const {names, options} = getParams(ns, opt, cb);
    let {callback} = getParams(ns, opt, cb);
    if (!callback || typeof callback === 'function') {
      const oldCb = callback;
      callback = (errors, values) => {
        if (oldCb) {
          oldCb(errors, values);
        } else if (errors) {
          reject({errors, values});
        } else {
          resolve(values);
        }
      };
    }
    const fieldNames = names ?
      this.fieldsStore.getValidFieldsFullName(names) :
      this.fieldsStore.getValidFieldsName();

    // 获取需要校验的表单项
    const fields = fieldNames
      // 过滤出已配置rules的字段
      .filter(name => {
        const fieldMeta = this.fieldsStore.getFieldMeta(name);
        return hasRules(fieldMeta.validate);
      })
      // 获取当前表单数据
      .map((name) => {
        const field = this.fieldsStore.getField(name);
        field.value = this.fieldsStore.getFieldValue(name);
        return field;
      });
    if (!fields.length) {
      callback(null, this.fieldsStore.getFieldsValue(fieldNames));
      return;
    }
    if (!('firstFields' in options)) {
      options.firstFields = fieldNames.filter((name) => {
        const fieldMeta = this.fieldsStore.getFieldMeta(name);
        return !!fieldMeta.validateFirst;
      });
    }
    // 调用表单校验方法,进行校验
    this.validateFieldsInternal(fields, {
      fieldNames,
      options,
    }, callback);
  });
  pending.catch((e) => {
    if (console.error && process.env.NODE_ENV !== 'production') {
      console.error(e);
    }
    return e;
  });
  return pending;
},
复制代码

实现自己的rc-form

在上一小节,我已经为你梳理了rc-form的实现思路,以及部分常用方法的实现方式。

相信你已经发现,rc-form的实现思路其实不复杂,分别使用HOC为新表单和表单项提供了所需要的扩展方法。

createFieldsStore.js 主要是为了实现这些拓展方法,而这些实现较为分复杂,虽然都是必要的,但确实对理解代码造成了一些障碍。

在阅读的时候,可以不必要过于拘泥于其中的细节,其实只要理解了使用HOC进行封装这一点,即使不理解 createFieldsStore.js 中的具体实现方式,也足够指导我们按照自己的思路来实现rc-form了。

我根据分析的rc-form实现思路,自己实现了一个rc-form功能,你可以在 http://localhost:3000/ 页面中,点击 新表单弹窗 按钮,查看效果。

实现rc-form示例代码位置: /src/utils/createForm.tsx

import React from 'react'
import {observer} from 'mobx-react';
import {observable, runInAction, toJS} from 'mobx';
import hoistStatics from 'hoist-non-react-statics';
import AsyncValidator, {Rules, ValidateError, ErrorList, RuleItem} from 'async-validator';

// setFieldsValue设置表单数据时传入的数据类型
export class Values {
  [propName: string]: any
}

// 表单项设置
export class FieldOption {
  initialValue?: any
  rules: RuleItem[] = []
}

// 表单项数据
export class Field {
  value: any
  errors: ErrorList = []
}

// 表格数据
export class Fields {
  [propName: string]: Field
}

// 表单项设置数据
export class FieldMeta {
  name: string = ''
  fieldOption: FieldOption = new FieldOption()
}

// 表格设置数据
export class FieldsMeta {
  [propName: string]: FieldMeta
}

export interface Props {
  wrappedComponentRef: React.RefObject<React.Component<FormComponentProps, any, any>>
}

// 为原组件添加的form参数
export interface FormProps {
  getFieldDecorator: (name: string, fieldOption: FieldOption) => (fieldElem: React.ReactElement) => React.ReactElement
  getFieldValue: (name: string) => any
  setFieldsValue: (values: any) => void
  getFieldsValue: () => any
  validateFields: (callback?: (errors: any, values: any) => void) => void
  resetFields: () => void
  getFieldError: (name: string) => ErrorList
}

// 为原组件添加的props
export interface FormComponentProps {
  form: FormProps
}

export class State {

}

function createForm(WrappedComponent: React.ComponentClass<FormComponentProps>): React.ComponentClass<Props> {

  @observer
  class Form extends React.Component<Props, State> {

    // 表单数据
    @observable
    private fields: Fields = new Fields()

    // 表单原始数据
    @observable
    private fieldsMeta: FieldsMeta = new FieldsMeta()

    constructor(props: Props) {
      super(props)

      this.state = new State()
    }

    // 创建表单项的props,提供给getFieldDecorator绑定事件
    private getFieldProps = (
      name: string,
      fieldOption: FieldOption = new FieldOption()
    ): any => {
      const initialValue = fieldOption.initialValue

      runInAction(() => {
        if (!this.fields[name]) {
          this.fields[name] = new Field()
          if (initialValue) {
            this.fields[name].value = initialValue
          }
        }

        if (!this.fieldsMeta[name]) {
          this.fieldsMeta[name] = {
            name,
            fieldOption
          }
        }
      })

      return {
        value: toJS(this.fields)[name].value,
        onChange: (event: React.ChangeEvent<HTMLInputElement> | string): void => {
          if (typeof event === 'string') {
            this.fields[name].value = event
          } else {
            this.fields[name].value = event.target.value
          }
          this.forceUpdate()
          this.validateField(name)
        }
      }
    }

    // 创建新表单项组件的HOC
    private getFieldDecorator = (
      name: string,
      fieldOption: FieldOption = new FieldOption()
    ): (fieldElem: React.ReactElement) => React.ReactElement => {
      const props = this.getFieldProps(name, fieldOption)

      return (fieldElem: React.ReactElement): React.ReactElement => {
        return React.cloneElement(
          fieldElem,
          props
        )
      }
    }

    // 获取表单项数据
    private getFieldValue = (name: string): any => {
      const field = toJS(this.fields)[name]
      return field && field.value
    }

    // 获取所有表单数据
    private getFieldsValue = (): Values => {
      const fields = toJS(this.fields)
      let values: Values = {}
      Object.keys(fields).forEach((name: string): void => {
        values[name] = fields[name]
      })

      return values
    }

    // 设置表单项的值
    private setFieldsValue = (values: Values): void => {
      const fields = toJS(this.fields)
      Object.keys(values).forEach((name: string): void => {
        fields[name].value = values[name]
      })
      this.fields = fields
    }

    // 获取用于表单校验的值和规则
    private getRulesValues = (name?: string): {rules: Rules, values: Fields} => {
      const fields = toJS(this.fields)
      const fieldsMeta = toJS(this.fieldsMeta)
      const fieldMetaArr: FieldMeta[] = name ? [fieldsMeta[name]] : Object.values(fieldsMeta)
      const values: Fields = new Fields()
      const rules: Rules = fieldMetaArr.reduce((rules: Rules, item: FieldMeta): Rules => {
        if (item.fieldOption.rules.length) {
          values[item.name] = fields[item.name].value
          return {
            ...rules,
            [item.name]: item.fieldOption.rules
          }
        }
        return rules
      }, {})

      return {rules, values}
    }

    // 校验单个表单项
    private validateField = (name: string): void => {
      const {rules, values} = this.getRulesValues(name)
      const validator = new AsyncValidator(rules)

      validator.validate(values, {}, (errors: ErrorList): void => {
        this.fields[name].errors = []
        if (errors) {
          errors.forEach((error: ValidateError): void => {
            this.fields[name].errors.push(error)
          })
        }
      })
    }

    // 校验整个表单
    private validateFields = (callback?: (errors: ErrorList | null, values: Fields) => void): void => {
      const {rules, values} = this.getRulesValues()
      const validator = new AsyncValidator(rules)

      validator.validate(values, {}, (errors: ErrorList): void => {
        Object.keys(values).forEach((name: string): void => {
          this.fields[name].errors = []
        })
        if (errors) {
          errors.forEach((error: ValidateError): void => {
            this.fields[error.field].errors.push(error)
          })
        }
        callback && callback(errors, values)
      })

      // 强制渲染组件,避免
      this.forceUpdate()
    }

    // 重置表单
    private resetFields = (): void => {
      this.fields = Object.values(toJS(this.fieldsMeta)).reduce((fields: Fields, item: FieldMeta): Fields => {
        fields[item.name] = new Field()
        fields[item.name].value = item.fieldOption.initialValue
        return fields
      }, new Fields())
    }

    // 获取表单项的校验结果
    private getFieldError = (name: string): ErrorList => {
      return this.fields[name] ? this.fields[name].errors : []
    }

    render() {
      let props: FormComponentProps = {
        form: {
          getFieldDecorator: this.getFieldDecorator,
          getFieldValue: this.getFieldValue,
          getFieldsValue: this.getFieldsValue,
          setFieldsValue: this.setFieldsValue,
          validateFields: this.validateFields,
          resetFields: this.resetFields,
          getFieldError: this.getFieldError,
        }
      }

      return (
        <WrappedComponent
          ref={this.props.wrappedComponentRef}
          {...props}
        />
      )
    }

  }

  // 使用hoist-non-react-statics库,复制所有静态方法,请查看:
  // https://github.com/mridgway/hoist-non-react-statics
  // https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over
  return hoistStatics(Form, WrappedComponent)
}

export default createForm
复制代码

使用方法示例代码位置: /src/utils/NewFormModal.tsx

import React from 'react'
import {Input, Select} from 'antd'
import FormItem, {FormItemProps} from 'antd/lib/form/FormItem'
import Modal, {ModalProps} from 'antd/lib/modal'
import createForm, {FormComponentProps, FormProps} from '../utils/createForm'
import {ErrorList, ValidateError} from 'async-validator'

const Option = Select.Option

// FormItem宽度兼容
export const formItemLayout: FormItemProps = {
  labelCol: {
    xs: {span: 24},
    sm: {span: 6}
  },
  wrapperCol: {
    xs: {span: 24},
    sm: {span: 16}
  }
}
// 性别枚举
enum SexEnum {
  male = 'male',
  female = 'female'
}

enum SexNameEnum {
  male = '男',
  female = '女'
}

// 表单字段类型
export class FormModalValues {
  username: string = ''
  sex: SexEnum = SexEnum.male
}

export interface Props extends ModalProps, FormComponentProps {

}

export class State {
  visible: boolean = false
}

export class NewFormModalComponent extends React.Component<Props, State> {

  constructor(props: Props) {
    super(props)

    this.state = new State()
  }

  // 打开弹窗
  public show = (): void => {
    this.setState({
      visible: true
    })
  }

  // 关闭弹窗
  public hide = (): void => {
    this.setState({
      visible: false
    })
  }

  // 点击确认按钮
  public onOk = () => {
    // 读取当前表单数据
    const values: FormModalValues = this.props.form.getFieldsValue()
    console.log(values)

    this.props.form.validateFields((errors: any, {username, sex}: FormModalValues) => {
      if (!errors) {
        Modal.success({
          title: '表单输入结果',
          content: `用户名:${username},性别:${SexNameEnum[sex]}。`
        })
        this.hide()
      }
    })
  }

  // 关闭弹窗后初始化弹窗参数
  public afterClose = (): void => {
    this.props.form.resetFields()
    this.setState(new State())
  }

  componentDidMount() {
    this.props.form.setFieldsValue(new FormModalValues())
  }

  render() {
    const visible = this.state.visible
    const form: FormProps = this.props.form
    const username = form.getFieldValue('username')
    const sex: SexEnum = form.getFieldValue('sex')
    const usernameError: ErrorList = form.getFieldError('username')
    const sexError: ErrorList = form.getFieldError('sex')

    return (
      <Modal
        visible={visible}
        title={'新建用户'}
        onCancel={this.hide}
        onOk={this.onOk}
        afterClose={this.afterClose}
      >
        <FormItem
          label={'请输入用户名'}
          required={true}
          validateStatus={usernameError.length ? 'error' : undefined}
          help={usernameError.length ? usernameError.map((item: ValidateError) => item.message).join(',') : undefined}
          {...formItemLayout}
        >
          {
            form.getFieldDecorator(
              'username',
              {
                initialValue: '',
                rules: [
                  {
                    required: true,
                  }
                ]
              }
            )(
              <Input />
            )
          }
        </FormItem>
        <FormItem
          label={'请选择性别'}
          required={true}
          validateStatus={sexError.length ? 'error' : undefined}
          help={sexError.length ? sexError.map((item: ValidateError) => item.message).join(',') : undefined}
          {...formItemLayout}
        >
          {
            form.getFieldDecorator(
              'sex',
              {
                initialValue: SexEnum.male,
                rules: [
                  {
                    required: true,
                  }
                ]
              }
            )(
              <Select
                style={{width: '60px'}}
              >
                <Option
                  value={'male'}
                >
                  男
                </Option>
                <Option
                  value={'female'}
                >
                  女
                </Option>
              </Select>
            )
          }
        </FormItem>
        <FormItem
          label={'输入的用户名'}
          {...formItemLayout}
        >
          {username}
        </FormItem>
        <FormItem
          label={'选择的性别'}
          {...formItemLayout}
        >
          {
            SexNameEnum[sex]
          }
        </FormItem>
      </Modal>
    )
  }

}

const NewFormModal = createForm(NewFormModalComponent)

export default NewFormModal
复制代码
原文  https://juejin.im/post/5d9b2f8351882509751b1294
正文到此结束
Loading...