前言
本篇文章是基于字节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类有啥用。
首先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上
- formProviderCtx是可以控制多个Form表单的,所以就包含了所有form组件的引用
- 我们会向form组件传一些方法,比如 onValuesChange、onChange,来监听form表单值变化的事件,这些事件为啥要挂载到Store的实例上呢,你想想onValuesChange,当值变化的时候,是不是store变化呢,所以当store变化的时候是不是要调用onValuesChange
- 虽然从scrollToField的有啥用呢,就是当某个值validate校验失败了,就要滚动到第一个失败的表单那里,他就是实现的函数。然后form组件把接收到的参数以及formInstance实例传给了下面的FormItem
formItem
- formItem 组件把error和warning的展示信息放到了这里,因为formItem组件下面还有Control组件,这个Control组件包裹表单元素,比如Input元素,如下图:
- formItem自己把formContext上的信息+一个updateFormItem方法形成FormItem Context传给下面的Control组件
Control组件
-
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来讲,其他的原理是一样的。
更新截断的逻辑一言以蔽之:
- 先更新存储表单store的数据变化,然后Store类的notify方法通知Control组件进行UI变化,这样数据和UI就统一了。
详细代码注释
接下来的代码,偏向枯燥的代码讲解,适合自己业务写一个form组件的同学,并再次基础上进行拓展业务定制功能。比如低代码平台的form内核完全可以用这个,我为什么没有细讲呢,因为今年是要写一套组件库出来的,加上平时业务和学习和必要的休息,实在没啥时间了,所以就以代码注释作为讲解。
组件库每个组件和都会出文章,是一个系列,下面这个是已经写完的开发和打包cli工具,欢迎大家给star
文件目录大概是这样
- 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 并且重置touch
+ resetFields(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>
);
}
}
今天的文章实现一个比ant-design更好form组件,可用于生产环境!分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14376.html