实现一个比ant-design更好form组件,可用于生产环境!

5,789 阅读21分钟

前言

本篇文章是基于字节arco design的form。小弟大言不惭,自以为我比他们做这个form组件的人还熟悉这个组件,文章末尾有所有代码的注释(本文代码在原有基础上改造了百分之30,质量进一步提高了)。

arco design的form说白了是借鉴了ant的form,但质量是比ant高的,原因有不少,就拿最重要的一条来说,arco的表单在查找数据的时候,是O1的复杂度,基于哈希表的,ant则不然,时间复杂度非常高,查找前要先建立哈希表,然后再查,在这点上,实在是我无法接受,觉得ant的form性能真的可以做的比现在好很多。我们来举个例子,为啥他们会有这个差别。

<FormItem label="Username" field="user.name" >
    <Input placeholder="please enter your username" />
</FormItem>

上面是arco design的form用法,注意field字段,举个例子有啥用,例如Input输入框你输入了"张三",那么form表单得到:

{
    user: {
        name: '张三'
    }
}

在acro form内部怎么查user.name 最终的值呢,

get(this.store, 'user.name')

这个get就是lodash(一个javascript的函数库)的方法,我们看下用法:

var object = { 'a': [{ 'b': { 'c': 3 } }] };
lodash.get(object, 'a[0].b.c'); // => 3

我们上面的this.store可以看做

{
    user: {
        name: '张三'
    }
}

所以直接get,就是O1的复杂度,哈希表查找很快。

ant呢,我们看看ant表单怎么用:

<FormItem label="Username" name={['user', 'name']}>
    <Input placeholder="please enter your username" />
</FormItem>

ant 内部并没有哈希表,需要先建立一张,类似这样

{
    "user_name": "张三"
}

然后再找 "user_name",为啥ant不能向arco的from之前就建立哈希表呢,因为name的命名方式不一样,我们可以看到一个直接是字符串作为对象属性,一个是数组,所以数组要先转化成哈希表。

好了,到这里前言结束,开始正题,我们写form首先看下整体大致的数据流向设计,然后会把所有代码都过一遍,建议大家看下数据流向图就算学习完毕了,具体代码,我是每一行都要讲,实在是非常枯燥,供有兴趣的同学一起讨论,😋。

数据流向图

首先,调用useForm建立一个中介者(可以联想中介者模式或者发布订阅模式),他是调动整个form运转的大脑,在代码叫叫Store类。

我们分为初次渲染和更新两个阶段介绍。

我们是这么使用组件的

import { Form, Input, Button, InputNumber } from '@arco-design/web-react';

const FormItem = Form.Item;

function Demo() {
  const [form] = Form.useForm();

  return (
    <Form
      form={form}
      style={{ width: 600 }}
      initialValues={{ name: 'admin' }}
      onValuesChange={(v, vs) => {
        console.log(v, vs);
      }}
      onSubmit={(v) => {
        console.log(v);
      }}
    >
      <FormItem label='Username' field='name' rules={[{ required: true }]}>
        <Input placeholder='please enter your username' />
      </FormItem>
        <Button type='primary' htmlType='submit' style={{ marginRight: 24 }}>
          Submit
        </Button>
      </FormItem>
    </Form>
}

上面有几个重点组件和方法

  • useForm方法,返回的form能操作整个表单
  • Form组件
  • FormItem组件

好了,我们接下来去介绍。

初次渲染

大家不用关心下面图的一些名字你看不懂,这个根本不重要,一言以蔽之,这张图就是新建了一个Store类的实例,所以后面马上讲Store类有啥用。

未命名文件 (1).jpg

首先useForm首次调用,实际上是创建一个FormInstance实例,这货就是把部分Store类实例的方法给了FormInstance实例。

Store类有这样几个比较重要属性,

  • store,存储当前form表单的值
  • registerFields,存储FormItem包裹的表单元素的实例对象,也就是说Store类控制着所有的表单,为啥它能控制呢,是因为在FormItem组件componentDidMount的时候,把自己放入registerFields数组了

再介绍几个比较重要的方法:

  • setFieldsValue,给store设置值的方法,暴露给formInstance,这个formInstance是useForm导出的,所以外面的使用者能拿到
  • notify,通知给FormItem更新值,让包裹的比如Input组件的值产生UI变化,在上面setFieldsValue时store的值变了,就接着会notify,所以就形成了store类值变化,notify让组件UI产生变化
  • 还有一些比如restFieldsValue,重置表单值,getFieldsValue是获取表单值,等等方法,都是值操作在store,notify让UI产生变化

form组件首次渲染

我们先看图,再详细解释 -下面图忘说了一点,没加到图上,就是form会把初始化数据赋值到store的intialValue上

未命名文件 (2).jpg

  • formProviderCtx是可以控制多个Form表单的,所以就包含了所有form组件的引用
  • 我们会向form组件传一些方法,比如 onValuesChange、onChange,来监听form表单值变化的事件,这些事件为啥要挂载到Store的实例上呢,你想想onValuesChange,当值变化的时候,是不是store变化呢,所以当store变化的时候是不是要调用onValuesChange
  • 虽然从scrollToField的有啥用呢,就是当某个值validate校验失败了,就要滚动到第一个失败的表单那里,他就是实现的函数。然后form组件把接收到的参数以及formInstance实例传给了下面的FormItem

formItem

未命名文件 (3).jpg

  • formItem 组件把error和warning的展示信息放到了这里,因为formItem组件下面还有Control组件,这个Control组件包裹表单元素,比如Input元素,如下图:

image.png

  • formItem自己把formContext上的信息+一个updateFormItem方法形成FormItem Context传给下面的Control组件

Control组件

未命名文件 (6).jpg

  • Control组件是个类组件,在Constructor时,让store更新Control组件的initialValue,但是实际工作中,我真的不太推荐大家使用initialValue,这个后面看详细代码才能理解,还是用setFieldsValue还原initialValue好一些。

  • 在componentDidMount时把自己的this注册到store

  • render其实要分是函数还是非函数组件,最常用的是非函数组件,这里可以忽略这个,这个是你在使用shouldupdate这个参数会用到的,shouldupdate可以实现字段的显隐,比如A字段选张三,B表单就隐藏,A字段选李四,B表单就显示,这个我不多扯了,扯API能扯1天。。。

  • render的时候默认监听包裹单表单onChange事件,触发validate校验也是默认这个事件,而且默认把value值传下去,啥意思呢,例如

<FormItem label="Username" field="user.name" >
    <Input placeholder="please enter your username" />
</FormItem>

Input组件的onChange其实是被监听的,你值变化了,就会体现在store里的值跟着变化,并且store把值的value传入Input的,你如果这么写,就是受控组件了

<FormItem label="Username" field="user.name" >
    <A />
</FormItem>

const A = (props) => {
    return  <Input onChange={props.onChange} value={props.value} placeholder="please enter your username" />
}

好了,接着看更新阶段的图示

form更新

我们拿表单元素变化,也就是触发onChange事件来看更新截断。其实更新截断分3个事件,表单元素值变化,用户主动调用setFieldsvalue,还有rest,我们只拿onChange来讲,其他的原理是一样的。

未命名文件 (7).jpg

更新截断的逻辑一言以蔽之:

  • 先更新存储表单store的数据变化,然后Store类的notify方法通知Control组件进行UI变化,这样数据和UI就统一了。

详细代码注释

接下来的代码,偏向枯燥的代码讲解,适合自己业务写一个form组件的同学,并再次基础上进行拓展业务定制功能。比如低代码平台的form内核完全可以用这个,我为什么没有细讲呢,因为今年是要写一套组件库出来的,加上平时业务和学习和必要的休息,实在没啥时间了,所以就以代码注释作为讲解。

组件库每个组件和都会出文章,是一个系列,下面这个是已经写完的开发和打包cli工具,欢迎大家给star

github.com/lio-mengxia…

文件目录大概是这样

  • Form
  • style文件夹 (样式文件,本文略)
  • interface文件夹(放ts定义,本文略)
  • useForm.ts (取form的hook)
  • form.tsx (From组件)
  • form-item.tsx (FormItem组件)
  • form-label.tsx(Label组件)
  • control.tsx(包裹表单组件,让表单受控的核心组件)
  • ...其他不重要的文件略

实现useForm

实现useForm需要解决几个难点:

  • typescript如何定义是类似这样的对象类型
{
  a: 1,
  b: 'str'
}

答案是:可以使用Record<string, any>

  • typescript定义如何获取这个对象所有属性的类型,如何获取这个对象所有值的类型呢?例如:
// 例如有这么一个interface
interface Person {
  name: string;
  age: number;
  gender: string;
}

还记得keyof这个操作符的作用吗,就是得到一个interface的属性的联合类型的

type P = keyof Person; // "name" | "age" | "gender"

接着我们再来获取interface的value类型

type V = Person[keyof Person] // string | number
  • react的hooks里如何实现单例模式
export default function useSingletenHooks(value) {
  const formRef = useRef(null);
  if (!formRef.current) {
      formRef.current = value;
    } else {
      return formRef.current
    }
  }
}

好了,有了这些只是作为基础,我们写一下useForm,应该很轻松就看懂了吧


export type IFormData = Record<string, any>;

/*
 * 可以学一下hooks如何写单例模式
 * 一言以蔽之就是把Store实例的方法赋予formRef.current
 * 这些实例可以控制整个form的增删改查
 */
export default function useForm<FormData extends IFormData>(
  form?: FormInstance<FormData>
): [FormInstance<FormData>] {
  const formRef = useRef<FormInstance<FormData>>(form);
  if (!formRef.current) {
    if (form) {
      formRef.current = form;
    } else {
      formRef.current = getFormInstance<FormData>();
    }
  }
  return [formRef.current];
}

我们可以看到,form是FormInstance类型,还有一个getFormInstance方法,其实返回的也是FormInstance类型,我们看看这个类型长啥样。

下面的代码简单扫一下,一言以蔽之,FormInstance包含15个方法,这些方法有啥用,我们马上一一道来。

export type FormInstance<
  FormData = Record<string, any>,
  FieldValue = FormData[keyof FormData],
  FieldKey extends KeyType = keyof FormData
> = Pick<
  Store<FormData, FieldValue, FieldKey>,
  | 'getFieldsValue'
  | 'getFieldValue'
  | 'getFieldError'
  | 'getFieldsError'
  | 'getTouchedFields'
  | 'getFields'
  | 'setFieldValue'
  | 'setFieldsValue'
  | 'setFields'
  | 'resetFields'
  | 'clearFields'
  | 'submit'
  | 'validate'
> & {
  scrollToField: (field: FieldKey, options?: ScrollIntoViewOptions) => void;
  getInnerMethods: (inner?: boolean) => InnerMethodsReturnType<FormData, FieldValue, FieldKey>;
};

这里简单描述一下这些方法的作用,后面讲store的时候会一一详解。不就纠结看不懂(马上就要讲Store这个类了)

// 外部调用设置单个表单字段值
setFieldValue(field: FieldKey, value: FieldValue) => void

// 外部调用,设置多个表单控件的值
setFieldsValue (values: DeepPartial<FormData>) => void

// 获取单个form里的值
getFieldValue (field: FieldKey) => FieldValue

// 获取单个form里的值
// 也可以根据传入的field 数组 拿出对应的errors
getFieldsError: (fields?: FieldKey[]) => { [key in FieldKey]?: FieldError<FieldValue>; }

// 获取单个error信息
getFieldError (field: FieldKey): FieldError<FieldValue> | null  

// 获取一组form里的值
getFieldsValue(fields?: FieldKey[]) => Partial<FormData>

// 这个函数可以执行callback,也可以变为promisify
validate(被promisefy包裹)( fieldsOrCallback: FieldKey[] | ((errors?: ValidateFieldsErrors<FieldValue, FieldKey>, values?: FormData) => void), cb: (errors?: ValidateFieldsErrors<FieldValue, FieldKey>, values?: FormData) => void): Promise<any> 

// 清空所有filed的值,注意,touch也会被重置
clearFields

// 获取所有被操作过的字段,一定是要有field的
getTouchedFields () => FieldKey[] 

// 获取所有error信息  * 也可以根据传入的field 数组 拿出对应的errors
getFieldsError: (fields?: FieldKey[]) => { [key in FieldKey]?: FieldError<FieldValue>; } 

// 获取form表单值
getFields: Partial<FormData>

// 外部调用,设置多个表单控件的值,以及 error,touch 信息
setFields(obj: { [field in FieldKey]?: {    value?: FieldValue;    error?: FieldError<FieldValue>;    touched?: boolean;    warning?: React.ReactNode;}; }) => void 

// 重置数据为initialValue 并且重置touchresetFields(fieldKeys?: FieldKey | FieldKey[]) => void  

// 提交方法,提交的时候会先验证
submit:() => void 

// 这个方法会在form组件里包装,重新赋值,借助的是一个scroll库,在知道dom id的情况下能滑过去
scrollToField

//  因为form已经被form包裹的组件,需要控制store实例里的一些数据
// 但是控制store又的数据的方法又在store实例上,所以就必须暴露出来一些这样的方法
getInnerMethods
暴露的方法有:
'registerField', : 收集注册的FormItem包裹的元素实例 并在组件卸载时移除          
'innerSetInitialValues',
'innerSetInitialValue',
'innerSetCallbacks',
'innerSetFieldValue',
'innerGetStore'

实现 getInstance

好了,有了上面的基础我们看下getInstance方法的实现,这个方法核心是Store类的实现。

export function getFormInstance<FormData extends IFormData>(): FormInstance<FormData> {
  const store = new Store<FormData>();
  return {
    getFieldsValue: store.getFieldsValue,
    getFieldValue: store.getFieldValue,
    getFieldError: store.getFieldError,
    getFieldsError: store.getFieldsError,
    getTouchedFields: store.getTouchedFields,
    getFields: store.getFields,
    setFieldValue: store.setFieldValue,
    setFieldsValue: store.setFieldsValue,
    setFields: store.setFields,
    resetFields: store.resetFields,
    clearFields: store.clearFields,
    submit: store.submit,
    validate: store.validate,
    scrollToField: () => {},
    getInnerMethods: (inner?: boolean): InnerMethodsReturnType<FormData> | {} => {
      const methods = {} as InnerMethodsReturnType<FormData> | {};
      const InnerMethodsList: InnerMethodsTuple = [
        'registerField',
        'innerSetInitialValues',
        'innerSetInitialValue',
        'innerSetCallbacks',
        'innerSetFieldValue',
        'innerGetStore',
      ];
      if (inner) {
        InnerMethodsList.map((key) => {
          methods[key] = store[key];
        });
      }
      return methods;
    },
  };
}

实现Store类

实现之前,我们需要解决几个难点:

  • 我们看看平时怎么用Form和FormItem去做表单功能的:
import { Form, Input, Button, InputNumber } from 'UI组件库';

const FormItem = Form.Item;

function Demo() {
  const [form] = Form.useForm();

  return (
    <Form
      form={form}
    >
      <FormItem label='Username' field='name'>
        <Input placeholder='please enter your username' />
      </FormItem>
      <FormItem
        label='Age'
        field='age'
      >
        <InputNumber placeholder='please enter your age' />
      </FormItem>
      <FormItem>
        <Button type='primary' htmlType='submit'>
          Submit
        </Button>
        <Button
          onClick={() => {
            form.resetFields();
          }}
        >
          Reset
        </Button>
        <Button
          onClick={() => {
            form.setFieldsValue({ name: 'admin', age: 11 });
          }}
        >
          Fill Form
        </Button>
      </FormItem>
    </Form>
  );
}

ReactDOM.render(
  <Demo/>,
  CONTAINER
);

这里面FormItem包裹的元素,实际上都注册到了Store里,比如上面的Input组件和InputNumber组件,啥意思呢,Store里面有一个属性叫registerFields,是一个数组

在FormItem的生命周期componentDidMount时,会把自己的this,传给这个registerFields数组,而且Store类有一个store属性,其实就是我们最终得到的表单数据,比如你Input组件输入'张三',那么store属性就是

{
    name: '张三'
}

也就是说Stroe是个大boss,他保存了所有需要收集数据的组件实例,也就是FormItem包裹谁,谁就被添加到registerFields里面了(这么说不严谨,但刚开始完全可以这么理解)。

而且Store还包含了表单当前保存的数据,这些注册的组件,比如Input,InputNumber值一变,Store里的存表单数据的store属性就跟着变了。

最后Store里还有一些例如重置表单数据、设置表单数据等等方法,其中一些方法通过之前讲的formInstance暴露给Form和FormItem组件用了,我只想说的是Store是控制整个表单的终极Boss(还有更大的Boss,但目前完全可以认为它是最大的Boss)

所以我们在这个认知的基础上看这个类就要稍微减轻一些负担:

先看属性部分

class Store<FormData extends IFormData> {
  /**
   * 收集Control元素的实例元素实例,以此控制表单元素的更新
   */
  private registerFields: Control<FormData>[] = [];

  /**
   * 和formControl 的 touched属性不一样。 只要被改过的字段,这里就会存储。并且不会跟随formControl被卸载而清除
   * reset 的时候清除
   */
  private touchedFields: Record<string, unknown> = {};

  /**
   * 存储form表单的数据
   */
  private store: Partial<FormData> = {};

  /**
   * 初始化数据
   */
  private initialValues: Partial<FormData> = {};

  /**
   * 注册一些回调函数,类型在innerCallbackType上(跟值变化和提交的事件)
   */
  private callbacks: ICallBack<FormData> = {};

}

上面的callbacks属性,存在的原因是什么,我们知道react是以组件来组装页面的,我们没办法直接把参数传给Store类的实例,它只是一个类而已,不是组件,所以就必须借助组件,把参数给Store类的实例。

在form组件上,我们可以传入一些props,其中有一些属性就传给了Store类的实例,比如onChange方法onValuesChange方法等。

这些方法都有个特点就是外界想在表单值变化,或者提交等重要的生命周期节点能够接收到form内部的一些状态,比如form表单的值啊,变化的值等等

我们顺便瞅瞅这个callback的ts类型,知道有哪些方法会有

type innerCallbackType = 'onValuesChange' | 'onSubmit' | 'onChange' | 'onSubmitFailed' | 'onValidateFail';

这些值是什么意思,我们解释一下:

  • onValuesChange | 任意表单项值改变时候触发。第一个参数是被改变表单项的值,第二个参数是所有的表单项值
  • onChange | 表单项值改变时候触发。和 onValuesChange 不同的是只会在用户操作表单项时触发
  • onSubmit | 数据验证成功后回调事件
  • onSubmitFailed | 数据验证失败后回调事件
  • onValidateFail | 这个忽略,官方文档都没写,估计被废弃了

接下来Store类的方法太多了,代码都有注释,感兴趣的同学可以看,确实挺枯燥的,如果真的讲解这个组件话,最好开一个视频,文字的话我估计起码3篇文章起步才能说的完。

import get from 'lodash/get';
import setWith from 'lodash/setWith';
import has from 'lodash/has';
import omit from 'lodash/omit';
import { cloneDeep, set, iterativelyGetKeys, isNotEmptyObject, string2Array } from './utils';
import { isArray, isObject } from '../_util/is';
import Control from './control';
import { FieldError, ValidateFieldsErrors, FormValidateFn } from './interface/form';
import promisify from './promisify';
import {
  IFormData,
  IFieldValue,
  ICallBack,
  INotifyType,
  IStoreChangeInfo,
} from './interface/store';

class Store<FormData extends IFormData> {
  /**
   * 触发在form上注册的onChange事件
   * 需要注意value的属性是字符串,比如'name', 'list.1.name'...
   */
  private triggerTouchChange(value: Record<string, any>) {
    if (isNotEmptyObject(value)) {
      this.callbacks?.onChange?.(value, this.getFields());
    }
  }

  /**
   * 注册callbacks,主要是注册在form上传入的值变化和提交事件
   */
  public innerSetCallbacks = (values: ICallBack<FormData>) => {
    this.callbacks = values;
  };

  /**
   * 收集所有control字段,并在组件卸载时移除
   */
  public registerField = (item: Control<FormData>) => {
    this.registerFields.push(item);
    return () => {
      this.registerFields = this.registerFields.filter((x) => x !== item);
    };
  };

  /**
   *  registerFields: 获得全部注册的FormItem包裹的元素实例
   *  hasField为true时,只返回传入field属性的control实例
   */
  private getRegisteredFields = (hasField?: boolean): Control<FormData>[] => {
    if (hasField) {
      return this.registerFields.filter(
        (control) => control.hasFieldProps() && !control.context?.isFormList
      );
    }
    return this.registerFields;
  };

  /**
   * registerFields: 获得单个注册的FormItem包裹的元素实例
   * 获取props.field === field 的control组件
   */
  public getRegisteredField = (field?: string) => {
    return this.registerFields.find((x) => x.context.field === field);
  };

  /**
   *  做两件事,一是把变化过的field标记为touch
   *  第二通知所有的formItem进行更新。有以下三种类型会触发
   *  setFieldValue: 外部调用setFieldsValue (setFieldValue等)方法触发更新
   *  innerSetValue: 控件例如Input,通过onChange事件触发的更新
   *  reset: 重置
   */
  private notify = (type: INotifyType, info: IStoreChangeInfo<string>) => {
    if (type === 'setFieldValue' || (type === 'innerSetValue' && !info.ignore)) {
      /**
       * 将field标记touch过
       */
      this._pushTouchField(
        info.changeValues
          ? /**
             * info.changeValues 假如是 { a: { b : 2 } } => ['a.b']
             */
            iterativelyGetKeys(info.changeValues)
          : this._getIterativelyKeysByField(info.field)
      );
    }
    this.registerFields.forEach((item) => {
      item.onStoreChange?.(type, {
        ...info,
        current: this.store,
      });
    });
  };

  /**
   * initialValue初始化,只是把值给了store,并没有onStoreChange给FormItem包* 裹的表单元素同步数据
   */
  public innerSetInitialValues = (values: Partial<FormData>) => {
    if (!values) return;
    this.initialValues = cloneDeep(values);

    Object.keys(values).forEach((field) => {
      set(this.store, field, values[field]);
    });
  };

  /**
   * 更改InitialValue,改单个值
   */
  public innerSetInitialValue = (field: string, value: any) => {
    if (!field) return;
    set(this.initialValues, field, value);
    // 组件在创建的时候,判断这个field是否touch过。只要没有被操作过(touchedFields里不存在),就生效
    if (!this._inTouchFields(field)) {
      set(this.store, field, get(this.initialValues, field));
    }
  };

  private _getIterativelyKeysByField(field: string | string[]) {
    if (!field) return [];
    const keys = string2Array(field)
      .map((item) => iterativelyGetKeys(set({}, item, undefined)))
      .reduce((total, next) => {
        return total.concat(next);
      }, []);
    return [field, ...keys];
  }

  /**
   * 判断这个field是否touch过
   */
  private _inTouchFields(field?: string) {
    const keys = this._getIterativelyKeysByField(field);
    return keys.some((item) => has(this.touchedFields, item));
  }

  /**
   * 将touch过的field移除
   */
  private _popTouchField(field?: string | string[]) {
    if (field === undefined) {
      this.touchedFields = {};
    }
    const keys = this._getIterativelyKeysByField(field);
    this.touchedFields = omit(this.touchedFields, keys);
  }

  /**
   * 将field标记touch过,touchField都要经过iterativelyGetKeys的改装
   */
  private _pushTouchField(field: string | string[]) {
    string2Array(field).forEach((key) => {
      setWith(this.touchedFields, key, undefined, Object);
    });
  }

  /**
   * 内部使用,更新value,会同时触发onChange 和 onValuesChange
   * 并且强制更新field对应的组件包括其子组件
   */
  public innerSetFieldValue = (
    field: string,
    value: any,
    options?: { isFormList?: boolean; ignore?: boolean }
  ) => {
    if (!field) return;
    const prev = cloneDeep(this.store);
    const changeValues = { [field]: value };

    set(this.store, field, value);
    this.triggerValuesChange(changeValues);
    this.triggerTouchChange(changeValues);
    this.notify('innerSetValue', { prev, field, ...options, changeValues });
  };

  /**
   * 获取内部的form表单值, 注意这里没有克隆store,是拿的引用
   */
  public innerGetStore = () => {
    return this.store;
  };

  /**
   * 获取所有被操作过的字段,并且是FormItem上有field的字段的才行
   */
  public getTouchedFields = (): string[] => {
    return this.getRegisteredFields(true)
      .filter((item) => {
        return item.isTouched();
      })
      .map((x) => x.context.field);
  };

  /**
   * 外部调用设置单个表单字段值
   * */
  public setFieldValue = (field: string, value: any) => {
    if (!field) return;
    this.setFields({
      [field]: { value },
    });
  };

  /**
   * 外部调用,设置多个表单控件的值
   */
  public setFieldsValue = (values: Record<string, any>) => {
    if (isObject(values)) {
      const fields = Object.keys(values);
      const obj = {} as {
        [field in string]: {
          value?: IFieldValue<FormData>;
          error?: FieldError<IFieldValue<FormData>>;
        };
      };
      fields.forEach((field) => {
        obj[field] = {
          value: values[field],
        };
      });
      this.setFields(obj);
    }
  };

  /**
   * 外部调用,设置多个表单控件的值,以及 error,touch 信息。
   * 触发notify的setFieldValue事件,并且有changeValues
   * 这里面有有可能obj本身的key是路径字符串,比如'a.c.v',而且有可能值是对象,所以要处理值
   * 并且触发valuesChange,但没有触发onChange
   * 这里如果传入waring,errors这些参数,会把这些信息传递给Formitem去显示
   */
  public setFields = (obj: {
    [field in string]?: {
      value?: any;
      error?: FieldError<any>;
      touched?: boolean;
      warning?: React.ReactNode;
    };
  }) => {
    const fields = Object.keys(obj);
    const changeValues: Record<string, any> = {};
    fields.forEach((field) => {
      const item = obj[field];
      const prev = cloneDeep(this.store);
      if (item) {
        /**
         * info 格式
         * errors?: FieldError<any>;
         * warnings?: React.ReactNode;
         * touched?: boolean;
         */
        const info: IStoreChangeInfo<string>['data'] = {};
        if ('error' in item) {
          info.errors = item.error;
        }
        if ('warning' in item) {
          info.warnings = item.warning;
        }
        if ('touched' in item) {
          info.touched = item.touched;
        }
        if ('value' in item) {
          set(this.store, field, item.value);
          changeValues[field] = item.value;
        }
        this.notify('setFieldValue', {
          data: info,
          prev,
          field,
          changeValues: { [field]: item.value },
        });
      }
    });
    this.triggerValuesChange(changeValues);
  };

  /**
   * 获取单个值
   * */
  public getFieldValue = (field: string) => {
    return get(this.store, field);
  };

  /**
   * 获取单个字段的错误信息。
   * */
  public getFieldError = (field: string): FieldError<any> | null => {
    const item = this.getRegisteredField(field);
    return item ? item.getErrors() : null;
  };

  /**
   * 获取传入字段/全部的错误信息
   */
  public getFieldsError = (fields?: string[]) => {
    const errors = {} as { [key in string]?: FieldError<IFieldValue<FormData>> };
    if (isArray(fields)) {
      fields.map((field) => {
        const error = this.getFieldError(field);
        if (error) {
          errors[field] = error;
        }
      });
    } else {
      this.getRegisteredFields(true).forEach((item) => {
        if (item.getErrors()) {
          errors[item.context.field] = item.getErrors();
        }
      });
    }
    return errors;
  };

  /**
   * 获取form表单值
   * */
  public getFields = (): Partial<FormData> => {
    return cloneDeep(this.store);
  };

  /**
   * 获取一组form里的数据,也可以获取传入fields的form数据
   * */
  public getFieldsValue = (fields?: string[]): Partial<FormData> => {
    const values = {};

    if (isArray(fields)) {
      fields.forEach((key) => {
        set(values, key, this.getFieldValue(key));
      });
      return values;
    }
    this.getRegisteredFields(true).forEach(({ context: { field } }) => {
      const value = get(this.store, field);
      set(values, field, value);
    });
    return values;
  };

  /**
   * 很简单,就是做几件事
   * set数据重置
   * notify通知FormItem表单数据更新
   * 触发valueChange事件
   * 更新相应表单的touch属性
   */
  public resetFields = (fieldKeys?: string | string[]) => {
    const prev = cloneDeep(this.store);
    const fields = string2Array(fieldKeys);

    if (fields && isArray(fields)) {
      const changeValues = {};
      /* 把值统一重置 */
      fields.forEach((field) => {
        /* store重置 */
        set(this.store, field, get(this.initialValues, field));
        changeValues[field] = get(this.store, field);
      });

      /* 触发valueChange事件 */
      this.triggerValuesChange(changeValues);
      /* 触发reset事件给每一个onStoreChange */
      this.notify('reset', { prev, field: fields });
      /* 只有reset事件会重置touch */
      this._popTouchField(fields);
    } else {
      const newValues = {};
      const changeValues = cloneDeep(this.store);
      /*  利用initialValue 重置value */
      Object.keys(this.initialValues).forEach((field) => {
        set(newValues, field, get(this.initialValues, field));
      });
      this.store = newValues;
      this.getRegisteredFields(true).forEach((item) => {
        const key = item.context.field;
        set(changeValues, key, get(this.store, key));
      });

      this.triggerValuesChange(changeValues);
      this._popTouchField();

      this.notify('reset', { prev, field: Object.keys(changeValues) });
    }
  };

  /**
   * 校验并获取表单输入域的值与 Errors,如果不设置 fields 的话,会验证所有的 fields。这个promisiFy感觉写的过于繁琐
   */
  public validate: FormValidateFn<FormData> = promisify<FormData>(
    (
      fieldsOrCallback?:
        | string[]
        | ((errors?: ValidateFieldsErrors<FormData>, values?: FormData) => void),
      cb?: (errors?: ValidateFieldsErrors<FormData>, values?: FormData) => void
    ) => {
      let callback: (
        errors?: ValidateFieldsErrors<FormData>,
        values?: Partial<FormData>
      ) => void = () => {};

      let controlItems = this.getRegisteredFields(true);

      if (isArray(fieldsOrCallback) && fieldsOrCallback.length > 0) {
        controlItems = controlItems.filter((x) => fieldsOrCallback.indexOf(x.context.field) > -1);
        callback = cb || callback;
      } else if (typeof fieldsOrCallback === 'function') {
        /* 如果是function就校验全部 */
        callback = fieldsOrCallback;
      }

      const promises = controlItems.map((x) => x.validateField());
      /* 校验完毕后处理 */
      Promise.all(promises).then((result) => {
        let errors = {} as ValidateFieldsErrors<FormData>;
        const values = {} as Partial<FormData>;

        result.map((x) => {
          if (x.error) {
            errors = { ...errors, ...x.error };
          }
          set(values, x.field, x.value);
        });
        /* 错误信息导出给callback和onValidateFail */
        if (Object.keys(errors).length) {
          const { onValidateFail } = this.callbacks;
          onValidateFail?.(errors);
          callback?.(errors, cloneDeep(values));
        } else {
          callback?.(null, cloneDeep(values));
        }
      });
    }
  );

  /**
   * 提交方法,提交的时候会先验证
   */
  public submit = () => {
    this.validate((errors, values) => {
      if (!errors) {
        const { onSubmit } = this.callbacks;
        onSubmit?.(values);
      } else {
        const { onSubmitFailed } = this.callbacks;
        onSubmitFailed?.(errors);
      }
    });
  };

  /**
   * 清除表单控件的值
   * 很简单,就是做几件事
   * set数据重置
   * notify通知FormItem表单数据更新
   * 触发valueChange事件
   * 更新相应表单的touch属性
   */
  public clearFields = (fieldKeys?: string | string[]) => {
    const prev = cloneDeep(this.store);
    const fields = string2Array(fieldKeys);
    if (fields && isArray(fields)) {
      const changeValues = {};
      fields.forEach((field) => {
        set(this.store, field, undefined);
        changeValues[field] = get(this.store, field);
      });

      this.triggerValuesChange(changeValues);

      this.notify('setFieldValue', { prev, field: fields });
      /**
       * 清空值也会让touch重置
       */
      this._popTouchField(fields);
    } else {
      const changeValues = {};
      this.store = {};
      this.getRegisteredFields(true).forEach((item) => {
        const key = item.context.field;
        set(changeValues, key, undefined);
      });

      this.triggerValuesChange(changeValues);
      this._popTouchField();

      this.notify('setFieldValue', {
        prev,
        field: Object.keys(changeValues),
      });
    }
  };
}

export default Store;

然后是Form组件,相对代码量少一些

import React, {
  useImperativeHandle,
  useEffect,
  forwardRef,
  PropsWithChildren,
  useContext,
  useRef,
} from 'react';
import scrollIntoView, { Options as ScrollIntoViewOptions } from 'scroll-into-view-if-needed';
import cs from '../_util/classNames';
import useForm from './useForm';
import { FormProps, FormInstance, FieldError, FormContextProps } from './interface/form';
import ConfigProvider, { ConfigContext } from '../ConfigProvider';
import { FormContext, FormProviderContext } from './context';
import { isObject } from '../_util/is';
import useMergeProps from '../_util/hooks/useMergeProps';
import { getFormElementId, getId, ID_SUFFIX } from './utils';
import { IFieldKey, IFormData } from './interface/store';

const defaultProps = {
  layout: 'horizontal' as const,
  labelCol: { span: 5, offset: 0 },
  labelAlign: 'right' as const,
  wrapperCol: { span: 19, offset: 0 },
  requiredSymbol: true,
  wrapper: 'form' as FormProps<FormData>['wrapper'],
  validateTrigger: 'onChange',
};

const Form = <FormData extends IFormData>(
  baseProps: PropsWithChildren<FormProps<FormData>>,
  ref: React.Ref<FormInstance<FormData>>
) => {
  /**
   * 获取根context上注册的信息
   * 每个组件都会从这里拿去一些根的配置信息
   */
  const ctx = useContext(ConfigContext);
  /**
   * 包裹Form组件的provider,共享一些方法
   * 主要的方法就是register,把formInstance注册上去的
   * onFormValuesChange 包裹的任意 Form 组件的值改变时,该方法会被调用
   * onFormSubmit 包裹的任意 Form 组件触发提交时,该方法会被调用
   */
  const formProviderCtx = useContext(FormProviderContext);

  /**
   * 包裹表单dom元素引用
   */
  const wrapperRef = useRef<HTMLElement>(null);
  /**
   * 将useform产生的Store实例拿出赋予formInstance
   */
  const [formInstance] = useForm<FormData>(baseProps.form);
  /**
   * 记录是否componentDidMount
   * 有人会说为啥不用useEffect去模拟componentDidMount
   * 是因为需要在render执行,useEffect做不到
   */
  const isMount = useRef<boolean>();
  /* 老规矩上来合并props,每个组件都有这货 */
  const props = useMergeProps<FormProps<FormData>>(
    baseProps,
    defaultProps,
    ctx.componentConfig?.Form
  );

  const {
    layout,
    labelCol,
    wrapperCol,
    wrapper: Wrapper,
    id,
    requiredSymbol,
    labelAlign,
    disabled,
    colon,
    className,
    validateTrigger,
    size: formSize,
  } = props;

  const prefixCls = ctx.getPrefixCls('form');
  const size = formSize || ctx.size;
  const innerMethods = formInstance.getInnerMethods(true);
  /**
   * 收敛外部传入给form的参数,当做provider给下面的组件
   * 这是在form上统一设置的,formItem也就对应的,可以覆盖这里设置的
   */
  const contextProps: FormContextProps = {
    requiredSymbol,
    labelAlign,
    disabled,
    colon,
    labelCol,
    wrapperCol,
    layout,
    store: formInstance,
    prefixCls,
    validateTrigger,
    getFormElementId: (field: string) => getId({ getFormElementId, id, field, ID_SUFFIX }),
  };
  if (!isMount.current) {
    innerMethods.innerSetInitialValues(props.initialValues);
  }
  useEffect(() => {
    isMount.current = true;
  }, []);

  useEffect(() => {
    let unregister;
    if (formProviderCtx.register) {
      unregister = formProviderCtx.register(props.id, formInstance);
    }
    return unregister;
  }, [props.id, formInstance]);

  useImperativeHandle(ref, () => {
    return formInstance;
  });

  // 滚动到目标表单字段位置
  formInstance.scrollToField = (field: IFieldKey<FormData>, options?: ScrollIntoViewOptions) => {
    /**
     * 获取到dom元素
     */
    const node = wrapperRef.current;
    /**
     * 外界传的id, 作为获取dom的prefix
     */
    const id = props.id;
    if (!node) {
      return;
    }
    /**
     * formItem会把这个id放到dom上,好让scroll插件滚动到对应位置
     */
    const fieldNode = node.querySelector(`#${getId({ getFormElementId, id, field, ID_SUFFIX })}`);
    fieldNode &&
      scrollIntoView(fieldNode, {
        behavior: 'smooth',
        block: 'nearest',
        scrollMode: 'if-needed',
        ...options,
      });
  };

  /**
   * 赋给store实例上的callback属性,也就把给from的自定义方法传给store,也就是注册到store上
   * onValuesChange 在两处触发,一个是formProviderCtx上注册的,一个是form上的onValuesChange
   * onChange form上注册的onChange
   * onValidateFail 没给外面暴露
   * onSubmitFailed	数据验证失败后回调事件
   * onSubmit	数据验证成功后回调事件
   */
  innerMethods.innerSetCallbacks({
    onValuesChange: (value, values) => {
      props.onValuesChange && props.onValuesChange(value, values);
      formProviderCtx.onFormValuesChange && formProviderCtx.onFormValuesChange(props.id, value);
    },
    onChange: props.onChange,
    onValidateFail: (errors: { [key in string]: FieldError<any> }) => {
      if (props.scrollToFirstError) {
        const options = isObject(props.scrollToFirstError) ? props.scrollToFirstError : {};
        formInstance.scrollToField(Object.keys(errors)[0], options);
      }
    },
    onSubmitFailed: props.onSubmitFailed,
    onSubmit: (values) => {
      props.onSubmit && props.onSubmit(values);
      formProviderCtx.onFormSubmit && formProviderCtx.onFormSubmit(props.id, values);
    },
  });

  return (
    <ConfigProvider {...ctx} size={size}>
      <FormContext.Provider value={contextProps}>
        <Wrapper
          ref={wrapperRef}
          {...props.wrapperProps}
          /**
           * layout和size在这里修改
           */
          className={cs(
            prefixCls,
            `${prefixCls}-${layout}`,
            `${prefixCls}-size-${size}`,
            className
          )}
          style={props.style}
          onSubmit={(e) => {
            e.preventDefault();
            e.stopPropagation();
            /* 调用store的submit */
            formInstance.submit();
          }}
          id={id}
        >
          {props.children}
        </Wrapper>
      </FormContext.Provider>
    </ConfigProvider>
  );
};

const FormComponent = forwardRef(Form);

FormComponent.displayName = 'Form';

export default FormComponent as <FormData extends IFormData>(
  props: React.PropsWithChildren<FormProps<FormData>> & {
    ref?: React.Ref<FormInstance<FormData>>;
  }
) => React.ReactElement;

formItem组件代码注释

import React, {
  cloneElement,
  ReactElement,
  forwardRef,
  useContext,
  PropsWithChildren,
  useState,
  useEffect,
  useMemo,
  ReactNode,
  useRef,
} from 'react';
import { CSSTransition } from 'react-transition-group';
import cs from '../_util/classNames';
import { isArray, isFunction, isUndefined, isObject } from '../_util/is';
import Grid from '../Grid';
import { FormItemProps, FieldError, VALIDATE_STATUS } from './interface/form';
import Control from './control';
import { FormItemContext, FormContext } from './context';
import FormItemLabel from './form-label';
import { IFormData } from './interface/store';

const Row = Grid.Row;
const Col = Grid.Col;

interface FormItemTipProps extends Pick<FormItemProps, 'prefixCls' | 'help'> {
  errors: FieldError[];
  warnings: ReactNode[];
}

/**
 * 错误提示文字
 */
const FormItemTip: React.FC<FormItemTipProps> = ({
  prefixCls,
  help,
  errors: propsErrors,
  warnings = [],
}) => {
  /**
   * error信息聚合
   */
  const errorTip = propsErrors.map((item, i) => {
    if (item) {
      return <div key={i}>{item.message}</div>;
    }
  });
  const warningTip = [];
  /**
   * waring信息聚合
   */
  warnings.map((item, i) => {
    warningTip.push(
      <div key={i} className={`${prefixCls}-message-help-warning`}>
        {item}
      </div>
    );
  });
  /**
   *  自定义校验文案存在或者warnings存在,则isHelpTip为true
   */
  const isHelpTip = !isUndefined(help) || !!warningTip.length;
  /**
   * 是否显示的条件其实是:是否有自定义文案,或者warning或者errors
   */
  const visible = isHelpTip || !!errorTip.length;

  return (
    visible && (
      <CSSTransition in={visible} appear classNames="formblink" timeout={300} unmountOnExit>
        <div
          className={cs(`${prefixCls}-message`, {
            [`${prefixCls}-message-help`]: isHelpTip,
          })}
        >
          {!isUndefined(help) ? (
            help
          ) : (
            <>
              {errorTip.length > 0 && errorTip}
              {warningTip.length > 0 && warningTip}
            </>
          )}
        </div>
      </CSSTransition>
    )
  );
};

const Item = <FormData extends IFormData>(
  props: PropsWithChildren<FormItemProps<FormData>>,
  ref: React.Ref<typeof Row>
) => {
  /**
   * formItem的context只是比formContext多了一个updateFormItem
   */
  const topFormItemContext = useContext(FormItemContext);
  const [errors, setErrors] = useState<{
    [key: string]: FieldError;
  }>(null);
  const [warnings, setWarnings] = useState<{
    [key: string]: ReactNode[];
  }>(null);

  /**
   * 获取外部formContext的传入
   */
  const formContext = useContext(FormContext);
  const prefixCls = formContext.prefixCls;
  /* 收敛layout属性 */
  const formLayout = props.layout || formContext.layout;
  /* 收敛label布局属性 */
  const labelAlign = props.labelAlign || formContext.labelAlign;
  /* 收敛disabled属性 */
  const disabled = 'disabled' in props ? props.disabled : formContext.disabled;
  const errorInfo = errors ? Object.values(errors) : [];
  const warningInfo = warnings
    ? Object.values(warnings).reduce((total, next) => total.concat(next), [])
    : [];

  /**
   * rest还有
   * style initialValue field labelCol wrapperCol colon disabled rules trigger
   * triggerPropName getValueFromEvent validateTrigger noStyle required hasFeedback help
   * normalize formatter shouldUpdate labelAlign requiredSymbol
   */
  const { label, extra, className, style, validateStatus, hidden } = props;
  /**
   * 是否这个组件已经卸载了
   */
  const isDestroyed = useRef(false);

  /**
   * 把error和warning数据同步到UI
   */
  const updateInnerFormItem = (
    field: string,
    params: {
      errors?: FieldError;
      warnings?: ReactNode[];
    } = {}
  ) => {
    if (isDestroyed.current) {
      return;
    }
    const { errors, warnings } = params || {};

    setErrors((preErrors) => {
      const newErrors = { ...(preErrors || {}) };
      if (errors) {
        newErrors[field] = errors;
      } else {
        delete newErrors[field];
      }
      return newErrors;
    });
    setWarnings((preWarnings) => {
      const newVal = { ...(preWarnings || {}) };
      if (warnings && warnings.length) {
        newVal[field] = warnings;
      } else {
        delete newVal[field];
      }
      return newVal;
    });
  };

  const updateFormItem =
    isObject(props.noStyle) && props.noStyle.showErrorTip && topFormItemContext.updateFormItem
      ? topFormItemContext.updateFormItem
      : updateInnerFormItem;

  useEffect(() => {
    return () => {
      isDestroyed.current = true;
      setErrors(null);
      setWarnings(null);
    };
  }, []);

  /**
   * 传给control的数据
   */
  const contextProps = {
    ...formContext,
    prefixCls,
    updateFormItem,
    disabled,
    field: isArray(props.children) ? undefined : props.field,
    shouldUpdate: props.shouldUpdate,
    trigger: props.trigger,
    normalize: props.normalize,
    getValueFromEvent: props.getValueFromEvent,
    children: props.children,
    rules: props.rules,
    validateTrigger: props.validateTrigger || formContext.validateTrigger || 'onChange',
    triggerPropName: props.triggerPropName,
    validateStatus: props.validateStatus,
    formatter: props.formatter,
    noStyle: props.noStyle,
    isFormList: props.isFormList,
    hasFeedback: props.hasFeedback,
    initialValue: props.initialValue,
  };
  const labelClassNames = cs(`${prefixCls}-label-item`, {
    [`${prefixCls}-label-item-left`]: labelAlign === 'left',
  });

  /**
   * 收敛状态 自定义validateStatus必须跟feedback一起用在control的右边才有icon
   */
  const itemStatus = useMemo(() => {
    if (validateStatus) {
      return validateStatus;
    }
    if (errorInfo.length) {
      return VALIDATE_STATUS.error;
    }
    if (warningInfo.length) {
      return VALIDATE_STATUS.warning;
    }
    return undefined;
  }, [errors, warnings, validateStatus]);

  const hasHelp = useMemo(() => {
    return !isUndefined(props.help) || warningInfo.length > 0;
  }, [props.help, warnings]);

  const classNames = cs(
    // width: 100%;
    // margin-bottom: 20px;
    // display: flex;
    // justify-content: flex-start;
    // align-items: flex-start;
    `${prefixCls}-item`,
    {
      /* margin-bottom: 0 */
      [`${prefixCls}-item-error`]:
        hasHelp || (!validateStatus && itemStatus === VALIDATE_STATUS.error),
      // 让下面的control组件定义backgroundcolor
      [`${prefixCls}-item-status-${itemStatus}`]: itemStatus,
      // 无样式
      [`${prefixCls}-item-has-help`]: hasHelp,
      // display: none
      [`${prefixCls}-item-hidden`]: hidden,
      /* 让control下的组件定义 padding-right: 28px; */
      [`${prefixCls}-item-has-feedback`]: itemStatus && props.hasFeedback,
    },
    /* 无样式 */
    `${prefixCls}-layout-${formLayout}`,
    className
  );

  const cloneElementWithDisabled = () => {
    const { field, children } = props;
    if (isFunction(children)) {
      return <Control {...(field ? { key: field } : {})}>{(...rest) => children(...rest)}</Control>;
    }
    if (isArray(children)) {
      const childrenDom = React.Children.map(children, (child, i) => {
        const key = (isObject(child) && (child as ReactElement).key) || i;
        return isObject(child) ? cloneElement(child as ReactElement, { key }) : child;
      });
      return <Control>{childrenDom}</Control>;
    }
    if (React.Children.count(children) === 1) {
      if (field) {
        return <Control key={field}>{children}</Control>;
      }
      if (isObject(children)) {
        return <Control>{children}</Control>;
      }
    }

    return children;
  };
  return (
    <FormItemContext.Provider value={contextProps}>
      {props.noStyle ? (
        cloneElementWithDisabled()
      ) : (
        <Row ref={ref} className={classNames} div={formLayout !== 'horizontal'} style={style}>
          {label ? (
            <Col
              {...(props.labelCol || formContext.labelCol)}
              className={cs(
                labelClassNames,
                props.labelCol?.className,
                formContext.labelCol?.className,
                {
                  [`${prefixCls}-label-item-flex`]: !props.labelCol && !formContext.labelCol,
                }
              )}
            >
              <FormItemLabel
                htmlFor={props.field && formContext.getFormElementId(props.field)}
                label={label}
                prefix={prefixCls}
                requiredSymbol={
                  'requiredSymbol' in props ? props.requiredSymbol : formContext.requiredSymbol
                }
                required={props.required}
                rules={props.rules}
                showColon={'colon' in props ? props.colon : formContext.colon}
              />
            </Col>
          ) : null}
          <Col
            className={cs(`${prefixCls}-item-wrapper`, {
              [`${prefixCls}-item-wrapper-flex`]: !props.wrapperCol && !formContext.wrapperCol,
            })}
            {...(props.wrapperCol || formContext.wrapperCol)}
          >
            {cloneElementWithDisabled()}
            <FormItemTip
              prefixCls={prefixCls}
              help={props.help}
              errors={errorInfo}
              warnings={warningInfo}
            />
            {extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
          </Col>
        </Row>
      )}
    </FormItemContext.Provider>
  );
};

const ItemComponent = forwardRef(Item);

ItemComponent.defaultProps = {
  trigger: 'onChange',
  triggerPropName: 'value',
};

ItemComponent.displayName = 'FormItem';

export default ItemComponent as <FormData = any>(
  props: React.PropsWithChildren<FormItemProps<FormData>> & {
    ref?: React.Ref<typeof Row['prototype']>;
  }
) => React.ReactElement;

control组件代码注释

import React, { Component, ReactElement } from 'react';
import isEqualWith from 'lodash/isEqualWith';
import has from 'lodash/has';
import set from 'lodash/set';
import get from 'lodash/get';
import setWith from 'lodash/setWith';
import { FormControlProps, FieldError, FormItemContextProps } from './interface/form';
import { FormItemContext } from './context';
import { isArray, isFunction } from '../_util/is';
import warn from '../_util/warning';
import IconExclamationCircleFill from '../../icon/react-icon/IconExclamationCircleFill';
import IconCloseCircleFill from '../../icon/react-icon/IconCloseCircleFill';
import IconCheckCircleFill from '../../icon/react-icon/IconCheckCircleFill';
import IconLoading from '../../icon/react-icon/IconLoading';
import { isSyntheticEvent, schemaValidate } from './utils';
import {
  IFormData,
  IFieldValue,
  IFieldKey,
  IStoreChangeInfo,
  INotifyType,
} from './interface/store';

/**
 * 是否要更新表单UI的路径在已有filed的control上
 */
function isFieldMath(field, fields) {
  const fieldObj = setWith({}, field, undefined, Object);

  return fields.some((item) => has(fieldObj, item));
}

export default class Control<FormData extends IFormData> extends Component<
  FormControlProps<FormData>
> {
  /**
   * 这个属性感觉删了也没啥
   */
  static isFormControl = true;

  /**
   * 引用上面传的context,这里是FormItem的
   */
  static contextType = FormItemContext;

  context: FormItemContextProps<FormData>;

  /**
   * 这里的errors和下面的warning都是setFieldsValue传的,然后通过updateFormItem更新UI到上面
   */
  private errors: FieldError<IFieldValue<FormData>> = null;

  private warnings: React.ReactNode[] = null;

  private isDestroyed = false;

  private touched: boolean;

  /**
   * 卸载这个control
   */
  private removeRegisterField: () => void;

  constructor(props: FormControlProps<FormData>, context: FormItemContextProps<FormData>) {
    super(props);
    /**
     * setInitialValue
     */
    if (context.initialValue !== undefined && this.hasFieldProps(context)) {
      const innerMethods = context.store.getInnerMethods(true);
      innerMethods.innerSetInitialValue(context.field, context.initialValue);
    }
  }

  /**
   * 把自己注册到stroe上
   */
  componentDidMount() {
    const { store } = this.context;
    if (store) {
      const innerMethods = store.getInnerMethods(true);
      this.removeRegisterField = innerMethods.registerField(this);
    }
  }

  /**
   * 从store上删除引用
   */
  componentWillUnmount() {
    this.removeRegisterField?.();

    this.removeRegisterField = null;

    /**
     * 把errors和warnings也删除
     */
    const { updateFormItem } = this.context;
    updateFormItem?.(this.context.field as string, { errors: null, warnings: null });
    this.isDestroyed = true;
  }

  getErrors = (): FieldError<IFieldValue<FormData>> | null => {
    return this.errors;
  };

  isTouched = (): boolean => {
    return this.touched;
  };

  public hasFieldProps = (context?: FormItemContextProps<FormData>): boolean => {
    return !!(context || this.context).field;
  };

  /**
   * 强制render,把store的数据映射到最新的UI上,并且更新error和warnings的UI
   */
  private updateFormItem = () => {
    if (this.isDestroyed) return;
    this.forceUpdate();
    const { updateFormItem } = this.context;
    updateFormItem &&
      updateFormItem(this.context.field as string, {
        errors: this.errors,
        warnings: this.warnings,
      });
  };

  /**
   * store notify的时候,会触发FormItem包裹的表单更新
   * type为rest:touched errors warning 重置 组件重新render,重新render是因为store新的值反应在表单UI上
   * type为innerSetValue 这个触发时机是表单自身值变化,比如onChange事件,结果是更改touch为true,组件重新render
   * type为setFieldValue 是外界主动调用setFieldValue触发
   */
  public onStoreChange = (type: INotifyType, info: IStoreChangeInfo<string> & { current: any }) => {
    /**
     * fields统一为数组格式
     */
    const fields = isArray(info.field) ? info.field : [info.field];
    const { field, shouldUpdate } = this.context;

    // isInner: the value is changed by innerSetValue
    // 如果你的FromItem属性设置shouldUpdate这里会校验并执行
    const shouldUpdateItem = (extra?: { isInner?: boolean; isFormList?: boolean }) => {
      if (shouldUpdate) {
        let shouldRender = false;
        if (isFunction(shouldUpdate)) {
          shouldRender = shouldUpdate(info.prev, info.current, {
            field: info.field,
            ...extra,
          });
        } else {
          shouldRender = !isEqualWith(info.prev, info.current);
        }
        if (shouldRender) {
          this.updateFormItem();
        }
      }
    };

    switch (type) {
      case 'reset':
        this.touched = false;
        this.errors = null;
        this.warnings = null;
        this.updateFormItem();
        break;
      case 'innerSetValue':
        if (isFieldMath(field, fields)) {
          this.touched = true;
          this.updateFormItem();
          return;
        }
        shouldUpdateItem({
          isInner: true,
          isFormList: info.isFormList,
        });
        break;
      case 'setFieldValue':
        if (isFieldMath(field, fields)) {
          this.touched = true;
          if (info.data && 'touched' in info.data) {
            this.touched = info.data.touched;
          }
          if (info.data && 'warnings' in info.data) {
            this.warnings = [].concat(info.data.warnings);
          }
          if (info.data && 'errors' in info.data) {
            this.errors = info.data.errors;
          } else if (!isEqualWith(get(info.prev, field), get(info.current, field))) {
            this.errors = null;
          }
          this.updateFormItem();
          return;
        }
        shouldUpdateItem();
        break;
      default:
        break;
    }
  };

  /**
   * form表单本身值变化,比如onChange事件触发了内部的setValue
   */
  innerSetFieldValue = (field: string, value: IFieldValue<FormData>) => {
    if (!field) return;
    const { store } = this.context;
    const methods = store.getInnerMethods(true);
    methods.innerSetFieldValue(field, value);

    const changedValue = {} as Partial<FormData>;
    set(changedValue, field, value);
  };

  /**
   * 处理表单自身变化事件,主要要stopPropagation一下,别冒泡上去
   * 值的处理 原始value => getValueFromEvent => normalize => children?.props?.[trigger]
   */
  handleTrigger = (_value, ...args) => {
    const { store, field, trigger, normalize, getValueFromEvent } = this.context;
    const value = isFunction(getValueFromEvent) ? getValueFromEvent(_value, ...args) : _value;
    const children = this.context.children as ReactElement;
    let normalizeValue = value;
    // break if value is instance of SyntheticEvent, 'cos value is missing
    if (isSyntheticEvent(value)) {
      warn(
        true,
        'changed value missed, please check whether extra elements is outta input/select controled by Form.Item'
      );
      value.stopPropagation();
      return;
    }

    if (typeof normalize === 'function') {
      normalizeValue = normalize(value, store.getFieldValue(field), {
        ...store.getFieldsValue(),
      });
    }
    this.touched = true;
    this.innerSetFieldValue(field, normalizeValue);

    this.validateField(trigger);
    children?.props?.[trigger]?.(normalizeValue, ...args);
  };

  /**
   * 首先筛选出触发当前事件并且rules有注明改事件的所有rules
   * 然后用schemaValidate去校验,并且返回结果赋给this.errors和warnings
   */
  validateField = (
    triggerType?: string
  ): Promise<{
    error: FieldError<IFieldValue<FormData>> | null;
    value: IFieldValue<FormData>;
    field: IFieldKey<FormData>;
  }> => {
    const { store, field, rules, validateTrigger } = this.context;
    const value = store.getFieldValue(field);
    const _rules = !triggerType
      ? rules
      : (rules || []).filter((rule) => {
          const triggers = [].concat(rule.validateTrigger || validateTrigger);
          return triggers.indexOf(triggerType) > -1;
        });
    if (_rules && _rules.length && field) {
      return schemaValidate(field, value, _rules).then(({ error, warning }) => {
        this.errors = error ? error[field] : null;
        this.warnings = warning || null;
        this.updateFormItem();
        return Promise.resolve({ error, value, field });
      });
    }
    if (this.errors) {
      this.errors = null;
      this.warnings = null;
      this.updateFormItem();
    }
    return Promise.resolve({ error: null, value, field });
  };

  /**
   * 收集rules里的validateTrigger字段
   */
  getValidateTrigger(): string[] {
    const _validateTrigger = this.context.validateTrigger;
    const rules = this.context.rules || [];

    const result = rules.reduce((acc, curr) => {
      acc.push(curr.validateTrigger || _validateTrigger);
      return acc;
    }, []);
    return Array.from(new Set(result));
  }

  /**
   * 将validate相关事件绑定到children
   * 将值变化的事件,默认onChange绑定到children
   * 将disabled属性绑定到children
   * 将从store取出的value用formatter再次加工给表单
   */
  renderControl(children: React.ReactNode, id) {
    // trigger context上默认 'onChange',
    // triggerPropName context上默认 'value',
    const { field, trigger, triggerPropName, validateStatus, formatter } = this.context;
    const { store, disabled } = this.context;
    const child = React.Children.only(children) as ReactElement;
    const childProps: any = {
      id,
    };

    this.getValidateTrigger().forEach((validateTriggerName) => {
      childProps[validateTriggerName] = (e) => {
        this.validateField(validateTriggerName);
        child.props?.[validateTriggerName](e);
      };
    });

    childProps[trigger] = this.handleTrigger;

    if (disabled !== undefined) {
      childProps.disabled = disabled;
    }
    let _value = store.getFieldValue(field);

    if (isFunction(formatter)) {
      _value = formatter(_value);
    }

    childProps[triggerPropName] = _value;
    if (!validateStatus && this.errors) {
      childProps.error = true;
    }

    return React.cloneElement(child, childProps);
  }

  getChild = () => {
    const { children } = this.context;
    const { store } = this.context;
    if (isFunction(children)) {
      return children(store.getFields(), {
        ...store,
      });
    }
    return children;
  };

  render() {
    const { noStyle, field, isFormList, hasFeedback } = this.context;
    const validateStatus = this.context.validateStatus || (this.errors ? 'error' : '');
    const { prefixCls, getFormElementId } = this.context;
    let child = this.getChild();
    const id = this.hasFieldProps() ? getFormElementId(field) : undefined;
    if (this.hasFieldProps() && !isFormList && React.Children.count(child) === 1) {
      child = this.renderControl(child, id);
    }

    if (noStyle) {
      return child;
    }
    return (
      <div className={`${prefixCls}-item-control-wrapper`}>
        <div className={`${prefixCls}-item-control`} id={id}>
          <div className={`${prefixCls}-item-control-children`}>
            {child}

            {validateStatus && hasFeedback && (
              <div className={`${prefixCls}-item-feedback`}>
                {validateStatus === 'warning' && <IconExclamationCircleFill />}
                {validateStatus === 'success' && <IconCheckCircleFill />}
                {validateStatus === 'error' && <IconCloseCircleFill />}
                {validateStatus === 'validating' && <IconLoading />}
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }
}