(题图与内容无关,这是我最喜欢的电影之一《爱乐之城》,夹带私货了嘿嘿)
写在前面
大家好,我是早晚会起风。不小心🕊(鸽)了这篇文章,本来是想要上周末更新的,结果由于周末的床吸引力太大了,导致没更出来,在此对读者们表示歉意。经过好几天的良心不安(并没有)之后,我终于搞出来这篇文章了!
在上篇文章 手写一个 Antd4 Form 吧(上篇):源码分析 中,我们对 And4 Form 的核心逻辑进行了逐步分析。这篇文章就是依次实现上篇内容分析的内容。
Q: 为什么还要再动手实现一遍呢?这不是重复造轮子吗?
A: 我们手写源码主要目的是深入了解好的思想,拓展我们的视野,当我们见到的好的代码之后,我们才能写出更好的代码。另外,了解源码的实现逻辑到自己能够实现还是有很大距离的。只有当你自己动手实现的时候,你才会发现,你有很多的细节还没有理解(我就是这样的,这也是我🕊的罪魁祸首)。动手实现的过程就是对你学习到的知识的检验过程。
Tips1:本篇文章实现的代码我也同步到 Github 仓库了,并且按照 commit 逐步提交,让读者可以清晰地看到每个模块都实现了哪些东西。戳这里跳转到 Github 仓库,如果觉得不错,大家可以点个👍
Tips2:在引用代码块的时候,我会在开头使用
//file:
标记出当前修改的文件,方便大家定位。我们实现的源码都存放在 Github 项目仓库的./src/rc-field-form
中
在开始之前,我再次贴一下总结的思维导图,方便大家参考。
实现状态管理
通过上一篇文章的源码解析,我们已经知道 Form 的状态管理分为三步,
- 创建 FormStore 实例维护表单状态
- 通过 Context 跨组件传递 store
- 子组件消费 Context,关联 store
我们就按这个顺序来,实现状态管理部分的逻辑。
创建 FormStore 实例
FormStore 的实例是通过 useForm
这个自定义 Hook 创建的,所以第一步我们先来实现它。代码如下,
// file: ./src/rc-field-form/useForm.tsx
class FormStore { ... }
export function useForm(formInstance?: FormInstance) {
// 创建一个 ref 用于保存实例,直到组件被销毁
const formRef = useRef<FormInstance>();
// 如果 store 还没被初始化
if (!formRef.current) {
// 如果外界传入了实例,直接使用
if (formInstance) {
formRef.current = formInstance;
} else {
// 否则,创建一个实例
formRef.current = new FormStore().getForm();
}
}
return [formRef.current];
}
useForm 做的事情很简单,就是通过 new FormStore()
创建一个 form 实例并返回。接着我们来实现 FormStore,
// file: ./src/rc-field-form/useForm.tsx
class FormStore {
private store: Store = {};
// 对外暴露 API
public getForm = (): FormInstance => ({
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
setFieldValue: this.setFieldValue,
setFieldsValue: this.setFieldsValue,
});
private getFieldValue = (name: NamePath) => {
return this.store[name];
};
private getFieldsValue = () => {
return { ...this.store };
};
private setFieldValue = (name: NamePath, value: any) => {
this.store[name] = value;
};
private setFieldsValue = (newValues: any) => {
return { ...this.store, ...newValues };
};
}
这里我们先实现四个基础 API: getFieldValue
getFieldsValue
setFieldValue
setFieldsValue
,之后使用的时候再来完善。
由于我们实现的是 TypeScript 版本的 Form 组件,所以别忘了补充类型定义。目前我们需要的类型定义如下,
// file: ./src/rc-field-form/interface.ts
export type NamePath = string | number;
export type StoreValue = any;
export interface FormInstance {
getFieldValue: (name: NamePath) => StoreValue;
getFieldsValue: () => any;
setFieldValue: (name: NamePath, value: any) => void;
setFieldsValue: (values: any) => void;
}
到这里,我们已经实现了第一部分——创建表单实例。简单总结一下,实现分为两个部分,
- useForm 自定义 Hook 的定义
- FormStore 的定义
通过 Context 跨组件传递 store
接着,我们来实现第二步,使用 Context 来传递 form 实例。通过上一篇文章的学习,我们已经知道,Context 是在我们使用 <Form ...>{children}</Form>
这个组件的时候,为子元素 children
包裹 Context,传入之前创建好的 form 实例,形式如 <Context.Provider value={store}>{children}</Context.Provider>
。
所以,这一小节我们需要实现两个部分,
- 创建 Context 对象并调用
- 实现 Form 组件
首先我们来创建 Context 对象,叫做 FieldContext
。这一部分比较简单,我就直接贴代码了,
// file: ./src/rc-field-form/FieldContext.tsx
import { createContext } from "react";
const FieldContext = createContext({});
export default FieldContext;
创建 Context 之后,我们来实现 Form 组件,并调用 Context 传入 form 实例。代码如下,
// file: ./src/rc-field-form/Form.tsx
import React from "react";
import FieldContext from "./FieldContext";
import { FormInstance, ValidateErrorEntity } from "./interface";
import { useForm } from "./useForm";
export interface FormProps {
form?: FormInstance;
children?: React.ReactNode;
}
export function Form({ form, children, ...restProps }: FormProps) {
// 1. 创建实例
const [formInstance] = useForm(form);
// 2. 使用 Context 将实例包裹 children
const wrapperNode = (
<FieldContext.Provider value={formInstance}>
{children}
</FieldContext.Provider>
);
return (
<form
{...restProps}
onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
// 阻止 form 默认行为和事件传播
event.preventDefault();
event.stopPropagation();
// 触发提交操作
formInstance.**submit**();
}}
>
{wrapperNode}
</form>
);
}
代码中除了使用 Context 传递实例得到 wrapperNode
外,return 的 <form>
也有处理。这里我们禁止掉了 form 自身的行为(在提交 form 表单时默认会刷新页面)和事件的传播。
当表单提交时,需要触发实例上的 submit
方法。这一部分我们还没实现,之后再补充。
子组件消费 Context
现在我们已经通过 Context 传递了 form 实例,接下来就是去 Field 中消费它。理所当然,我们就要实现 Field
组件。代码如下,
// file: ./src/rc-field-form/Field.tsx
import React from "react";
import FieldContext from "./FieldContext";
import { FormInstance } from "./interface";
interface InternalFieldProps {
children?: React.ReactElement;
name: string;
fieldContext: FormInstance;
}
interface ChildProps {
[name: string]: any;
}
class Field extends React.Component<InternalFieldProps> {
public static contextType = FieldContext;
public getControlled = (props: ChildProps) => {
const { fieldContext, name } = this.props;
const { getFieldValue, setFieldValue, dispatch } = fieldContext;
return {
value: getFieldValue(name),
onChange: (e) => {
// 获取输入值
const newValue = e.target?.value;
// 更新 store 中存储的值
dispatch({
type: 'updateValue',
namePath: name,
value: newValue
})
},
};
};
public render() {
const { children } = this.props;
// clone 子元素,注入 value 和 onChange 事件 (这里目前只考虑正常情况)
const returnChild = React.cloneElement(
children as React.ReactElement,
this.getControlled((children as React.ReactElement).props)
);
return returnChild;
}
}
function WrapperField({ name, ...restProps }: any) {
// 获取 fieldContext 并传递下去
const fieldContext = React.useContext(FieldContext);
return (
<Field key={name} name={name} {...restProps} fieldContext={fieldContext} />
);
}
export default WrapperField;
这里的 WrapperField
的作用就是通过 useContext
这个 hook 拿到 FieldContext
传下来的 form 实例,并传给真正的 Field
组件。
Field
组件做的事情就是通过 React.cloneElement
克隆子组件(比如最常见的 Input 组件)并调用 getControlled
方法为其挂载 value
和 onChange
方法。
好了,到这里我们已经实现了 Form 的状态管理流程。不过还有一些小问题,我们为子组件挂载的 onChange
中调用了 dispatch
方法,我们现在还没有定义。不要着急,我们接下来就来实现它。
实现 Field 组件的状态更新
这里,我再展示一遍 Field 组件更新的流程,方便大家理清思路,
- 输入内容,触发挂载到子组件(例如 Input)上的 onChange 方法,通过
dispatch
告诉 store 值需要更新。 - 将新的状态存储到 store 中。
- store 通知 Field 组件进行重新渲染。
第一步我们在上一节已经实现了,接下来我们看剩余的两个步骤,
将新的状态存储到 store 中
现在我们来实现 dispatch
方法,我们先看实现代码,
// file: ./src/rc-field-form/useForm.tsx
interface UpdateAction {
type: "updateValue";
namePath: NamePath;
value: StoreValue;
}
interface ValidateAction {
type: 'validateField';
namePath: NamePath;
triggerName: string;
}
export type ReducerAction = UpdateAction | ValidateAction;
...
class FormStore {
...
public getForm = (): FormInstance => ({
...
dispatch: this.dispatch, // 记得对外暴露 dispatch 方法
});
// 更新 store
private updateStore = (nextStore: Store) => {
this.store = nextStore;
};
private dispatch = (action: ReducerAction) => {
switch (action.type) {
case "updateValue": {
const { namePath, value } = action;
this.updateValue(namePath, value);
break;
}
case "validateField": {
break;
}
default:
}
};
// 更新值
private updateValue = (name: NamePath, value: StoreValue) => {
const prevStore = this.store;
// 1. 更新 store 对象
this.updateStore({
...this.store,
[name]: value, // 这里要注意 key 值为 [name] 而不是 'name'
});
// 2. 触发对应组件的更新
this.notifyObservers(prevStore, name, {
type: "valueUpdate",
});
};
}
我们已经知道触发 dispatch
的两种方式有 updateValue(值更新)
和 validateField(校验)
。这里我们只把后者列出来,方便我们梳理思路,具体的实现放在第三部分。
更新值没什么好说的,就是替换 [this.store](http://this.store)
对象。在 updateValue
中我们除了实现值的更新外,还需要通知对应组件的更新,这就是下一步我们需要做的事情。
到这里我们已经实现了值的更新,即 dispatch
方法的调用流程。还有一个细节别忘了,更新 FormInstance
这个类的定义。
// file: ./src/rc-field-form/interface.ts
export interface FormInstance {
...
// 增加 dispatch 类型定义
dispatch: (action: ReducerAction) => void;
}
通知 Field 组件重新渲染
通知 Field 组件,我们首先需要在 store 中注册 Field 组件吧,不然怎么调用组件上的 onStoreChange
方法呢(不生 🐔 怎么下得出 🥚 呢)。好,我们来生🐔…… 错了错了,是注册 Field 组件。
1. FormStore 新增组件注册方法
第一步,我们需要为 FormStore 新增注册组件的部分。这一步很简单,就是创建 fieldEntities
属性存放注册的 Field 组件,并新增 registerField
方法注册组件。
// file: ./src/rc-field-form/useForm.tsx
class FormStore {
...
// 定义变量存放注册的 fields
private fieldEntities: FieldEntity[] = [];
private registerField = (entity: FieldEntity) => {
this.fieldEntities.push(entity);
};
public getForm = (): FormInstance => ({
...
registerField: this.registerField, // 对外暴露 registerField 方法
});
}
这里定义了一个新的 TS 类型 FieldEntity
,我们补充一下,
// file: ./src/rc-field-form/interface.ts
export type Store = Record<string, StoreValue>;
export interface FieldEntity {
// 目前只用到 onStoreChange 方法
onStoreChange: (
store: Store,
namePathList: NamePath,
info: any, // info 的类型我们之后再补充
) => void;
}
export interface FormInstance {
...
registerField: (entity: FieldEntity) => void; // 同样补充 registerField 的定义
}
2. 注册 Field 组件
定义好注册方法之后,我们来进行第二步——注册 Field 组件到 store 中。
// file: ./src/rc-field-form/Field.tsx
class Field extends React.Component<InternalFieldProps> {
public static contextType = FieldContext;
private mounted = false;
componentDidMount() {
this.mounted = true;
const { fieldContext } = this.props;
const { registerField } = fieldContext;
registerField(this);
}
...
}
这一步就很简单了,就是在组件挂在到页面上(componentDidMount
)时调用注册方法,把自己传进去。
3. 通知 Field 组件更新
我们做好了前置条件的准备,终于可以进入正题了。这一步我们就要实现 store updateValue
方法中调用的 notifyObservers
方法了。
// file: ./src/rc-field-form/useForm.tsx
...
interface ValueUpdateInfo {
type: "valueUpdate";
}
export type NotifyInfo = ValueUpdateInfo;
export type ValuedNotifyInfo = NotifyInfo & {
store: Store;
};
class FormStore {
...
private getFieldEntities = () => {
return this.fieldEntities;
}
private notifyObservers = (
prevStore: Store,
name: NamePath,
info: NotifyInfo
) => {
// info 与 store 合并,传给 onStoreChange 方法
const mergedInfo: ValuedNotifyInfo = {
...info,
store: this.getFieldsValue(),
};
// 遍历每个注册的 Field 组件更新
this.getFieldEntities().forEach(({ onStoreChange }) => {
onStoreChange(prevStore, name, mergedInfo);
});
};
...
}
记得更新一下 onStoreChange
方法的类型定义,
// file: ./src/rc-field-form/interface.ts
export interface FieldEntity {
onStoreChange: (
store: Store,
namePathList: NamePath,
info: ValuedNotifyInfo, // +
) => void;
}
4. 需要更新的 Field 组件重新渲染
到现在,我们实现了从 dispatch 到更新 store 状态到触发组件 onStoreChange 的过程,只差最后一步——组件重渲染。
// file: ./src/rc-field-form/Field.tsx
class Field extends React.Component<InternalFieldProps> {
...
public reRender() {
if (!this.mounted) return;
this.forceUpdate();
}
public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePath, info) => {
// 确定当前 Field 是否需要更新
const namePathMatch = namePath && namePath === this.props.name;
switch (info.type) {
default:
if (namePathMatch) {
this.reRender();
return;
}
}
}
...
}
这里的重点在于 namePathMatch
,由于我们实现的比较简单,这里目前直接通过 namePath === this.props.name
来判断当前 Field 组件是否需要更新。如果需要,就调用 reRender
方法。
至此,我们已经实现了 Form 表单最基本的功能。在进行剩余部分之前,建议读者先写一个例子验证一下。需要做到能够输入并渲染值。我在这里给一个基本的例子以供测试,
// file: ./src/App.tsx
import Form, { useForm, Field } from "./rc-field-form";
import Input from "./rc-field-form/Input";
export default () => {
const [form] = useForm();
return (
<Form
form={form}
>
<Field name="name" rules={[nameRules]}>
<Input placeholder="Username" />
</Field>
<Field name="name1" rules={[nameRules]}>
<Input placeholder="Username1" />
</Field>
</Form>
);
};
实现表单的校验逻辑
到这里,我们就实现了最基本的表单状态管理和状态更新逻辑啦,我们继续来实现表单的校验逻辑。
触发表单校验
默认情况下,当我们输入表单值的时候就会触发表单校验逻辑。这里为了简单起见,我们直接在 onChange
方法中执行 validateField
类型的 dispatch 触发表单校验。代码如下,
// file: ./src/rc-field-form/Field.tsx
public getControlled = (props: ChildProps) => {
const { fieldContext, name } = this.props;
const { getFieldValue, dispatch } = fieldContext;
return {
value: getFieldValue(name),
onChange: (e) => {
// 获取输入值
// ...
// 更新 store 中存储的值
// ...
// 进行表单校验
dispatch({
type: "validateField",
namePath: name,
triggerName: "onChange",
});
},
};
};
实现表单校验流程(前半部分)
接下来我们来实现校验流程,从这一部分开始稍微有点难理解,还请大家耐心一点。代码如下,
// file: ./src/rc-field-form/useForm.tsx
// FormStore 中新增 validateFields 方法
private validateFields = (name?: NamePath, options?: ValidateOptions) => {
const promiseList: Promise<FieldError>[] = [];
this.getFieldEntities().forEach((field) => {
// 1. 如果该 field 没有定义 rules,直接跳过
if (!field.props.rules || !field.props.rules.length) {
return;
}
// 2. 如果触发校验的 field 不是当前 field,直接跳过
if (name !== field.props.name) {
return;
}
// 3. 调用 field 自身的方法进行校验,返回一个 promise
const promise = field.validateRules();
// 剩余步骤我们等实现第 3 步的具体过程后再补充
...
});
};
简单梳理一下这里的校验流程就是,对需要校验的 field
进行校验,并将校验结果收集起来,最后统一通知校验失败的 fields 去重新渲染。
这里的重点是第 3
点,field 自身触发的校验, const promise = field.validateRules()
。
实现 Field 自身的校验逻辑
话不多说,我们先上代码,
// file: ./src/rc-field-form/Field.tsx
import { validateRules } from "./utils/validateUtil";
...
interface ValidateFinishInfo {
type: 'validateFinish';
}
export type NotifyInfo = ValueUpdateInfo | ValidateFinishInfo;
...
class Field extends React.Component<InternalFieldProps> {
...
private errors: string[] = [];
private warnings: string[] = [];
...
public validateRules = (options?: ValidateOptions): Promise<RuleError[]> => {
const { getFieldValue } = this.props.fieldContext;
const currentValue = getFieldValue(this.props.name);
const rootPromise = Promise.resolve().then(() => {
if (!this.mounted) {
return [];
}
// 1. 获取 filterRules 这里其实还过滤出与当前校验的 triggerName 一致的规则。
// 我们这里都是 onChange,所以就不作过滤了。
let filteredRules = this.getRules();
// 2. 调用工具函数 validateRules 进行校验,这一部分我们就不实现了,具体的过程在第一篇文章中已经分析过了。
// 如果忘了,可以再翻回去看看。
const promise = validateRules(
[this.props.name],
currentValue,
filteredRules,
options,
false
);
// 3. 处理校验结果
promise
.catch((e) => e)
.then((ruleErrors: RuleError[] = []) => {
// 将校验结果保存起来
// Get errors & warnings
const nextErrors: string[] = [];
const nextWarnings: string[] = [];
ruleErrors.forEach(({ rule: { warningOnly }, errors = [] }) => {
if (warningOnly) {
nextWarnings.push(...errors);
} else {
nextErrors.push(...errors);
}
});
this.errors = nextErrors;
this.warnings = nextWarnings;
this.reRender();
});
// 4. 返回 promise 对象
// 注意,这里返回的是 promise 对象,而不是 promise.catch(...).then(...) 之后的结果。
// 也就是说,promise 处理了两次。
// 第一次是上边这部分代码,将校验结果保存到了 this.errors 和 this.warnings 中,
// 第二次是返回给 form 实例之后,再次处理。目的是将所有 fields 的校验信息收集起来,统一处理。
return promise;
});
return rootPromise;
};
}
这部分逻辑有两个部分需要注意,
- field 组件自身的校验需要调用工具方法
validateRules
,这一部分由于太多,我这里直接将代码复制过来,放到了./src/rc-field-form/utils
文件夹下边。有兴趣的读者可以自行实现。 - 代码这里的第
4
步返回的结果是promise
对象本身,不是promise.catch(...).then(...)
之后的结果,具体作用我在代码里注释了(怕读者没仔细看,对后面的流程可能会造成一些误解)。
实现表单校验流程(后半部分)
我们说完 field 自身的校验流程之后,再回到 validateFields
方法中看剩余的步骤。
// file: ./src/rc-field-form/useForm.tsx
private validateFields = (name?: NamePath, options?: ValidateOptions) => {
const promiseList: Promise<FieldError>[] = [];
this.getFieldEntities().forEach((field) => {
...
// 3. 调用 field 自身的方法进行校验,返回一个 promise
const promise = field.validateRules();
// 4. 将 promise 存放到 promiseList 中
promiseList.push(
promise
.then<any, RuleError>(() => {
return { name, errors: [], warnings: [] };
})
.catch((ruleErrors: RuleError[]) => {
const mergedErrors: string[] = [];
const mergedWarnings: string[] = [];
ruleErrors.forEach(({ rule: { warningOnly }, errors }) => {
if (warningOnly) {
mergedWarnings.push(...errors);
} else {
mergedErrors.push(...errors);
}
});
if (mergedErrors.length) {
return Promise.reject({
name,
errors: mergedErrors,
warnings: mergedWarnings,
});
}
return {
name,
errors: mergedErrors,
warnings: mergedWarnings,
};
})
);
});
// 5. 这一步很关键,allPromiseFinish 返回一个新的 Promise ,等待 promiseList 中的所有 promise 都完成
const summaryPromise = allPromiseFinish(promiseList);
// Notify fields with rule that validate has finished and need update
summaryPromise
.catch((results) => results)
.then((results: FieldError[]) => {
// 获取到校验失败的 fields 的错误信息
const resultNamePathList: NamePath[] = results.map(({ name }) => name);
// 6. 通知这些 fields 更新
this.notifyObservers(this.store, resultNamePathList, {
type: "validateFinish",
});
});
};
这里第 4
步就是收集 field 校验的结果存放到 promiseList 中。
为什么要这样做呢?比如我们在 onSubmit
提交整个表单的时候会对表单内容全部进行校验,这个时候我们就需要收集校验结果供用户下一步操作。
第 5
步很重要,通过 allPromiseFinish(promiseList)
做到等待所有 field 的校验完成后,再进行第 6
步。我们来看这个函数做了什么,
// file: ./src/rc-field-form/utils/asyncUtil.ts
import type { FieldError } from '../interface';
export function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]> {
// 标记校验结果是否有错误
let hasError = false;
let count = promiseList.length;
const results: FieldError[] = [];
if (!promiseList.length) {
return Promise.resolve([]);
}
// 这里返回的一个新的 Promise 对象
return new Promise((resolve, reject) => {
// 遍历 promisList
promiseList.forEach((promise, index) => {
promise
.catch(e => {
// 将 hasError 标记为 true
hasError = true;
return e;
})
.then(result => {
count -= 1;
results[index] = result;
// 当还有 promise 没处理时,继续下一个 promise 的处理
if (count > 0) {
return;
}
// 如果所有的 promise 都处理完成,并且有错误,则 reject
if (hasError) {
reject(results);
}
// 否则 resolve
resolve(results);
});
});
});
}
具体的逻辑我标记在代码中了。为什么要这样做呢?
最基本的,我们肯定是需要等待所有的 promise 都校验完成后再进行下一步处理。但是由于 promise 的校验可能会有异步的逻辑(比如我们调用一个后端接口进行校验)。通过 allPromiseFinish
,我们就能做到等待最后一个异步的 promise 校验完毕后再 resolve/reject
进行后续的处理环节。
最后一步(第 6
步)就是通知 fields 进行更新重渲染。这一步没什么好说的,需要注意的是 resultNamePathList
变量的类型是 NamePath[]
,我们之前对 notifyObservers
和 onStoreChange
的变量定义都是 NamePath
,需要适配改造一下。
逻辑适配改造 & 补充类型定义
// file: ./src/rc-field-form/useForm.tsx
private notifyObservers = (
prevStore: Store,
// -
// name: NamePath,
// +
name: NamePath[],
info: NotifyInfo
)
...
private updateValue = (name: NamePath, value: StoreValue) => {
...
// -
// this.notifyObservers(prevStore, name, {
// +
this.notifyObservers(prevStore, [name], {
type: "valueUpdate",
});
};
// file: ./src/rc-field-form/interface.ts
export interface FieldEntity {
...
onStoreChange: (
store: Store,
// -
// namePathList: NamePath,
// +
namePathList: NamePath[],
info: ValuedNotifyInfo,
) => void;
...
}
// file: ./src/rc-field-form/Field.tsx
public onStoreChange: FieldEntity["onStoreChange"] = (
prevStore,
namePath,
info
) => {
// 确定当前 Field 是否需要更新
// -
// const namePathMatch = namePath && namePath === this.props.name;
// +
const namePathMatch = namePath && namePath.some(name => name === this.props.name);
...
};
最后,我们补充一下 TS 定义。由于这块定义比较多,我就直接 copy 定义了,大家如果感兴趣可以学习一下 Antd4 Form 的类型是怎样定义的。
// file: ./src/rc-field-form/interface.ts
import type { ReactElement } from 'react';
export type InternalNamePath = (string | number)[];
export type NamePath = string | number | InternalNamePath;
...
export type RuleType =
| 'string'
| 'number'
| 'boolean'
| 'method'
| 'regexp'
| 'integer'
| 'float'
| 'object'
| 'enum'
| 'date'
| 'url'
| 'hex'
| 'email';
type Validator = (
rule: RuleObject,
value: StoreValue,
callback: (error?: string) => void,
) => Promise<void | any> | void;
export interface ValidatorRule {
warningOnly?: boolean;
message?: string | ReactElement;
validator: Validator;
}
interface BaseRule {
warningOnly?: boolean;
enum?: StoreValue[];
len?: number;
max?: number;
message?: string | ReactElement;
min?: number;
pattern?: RegExp;
required?: boolean;
transform?: (value: StoreValue) => StoreValue;
type?: RuleType;
whitespace?: boolean;
/** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */
validateTrigger?: string | string[];
}
type AggregationRule = BaseRule & Partial<ValidatorRule>;
interface ArrayRule extends Omit<AggregationRule, 'type'> {
type: 'array';
defaultField?: RuleObject;
}
export type RuleObject = AggregationRule | ArrayRule;
export interface FieldError {
name: NamePath;
errors: string[];
warnings: string[];
}
export interface RuleError {
errors: string[];
rule: RuleObject;
}
好了,到现在我们已经实现了一个 mini 版的表单校验逻辑了。现在我们只剩最后一个问题了。
实现表单的依赖项更新
话不多说,我们直接来实现。在第一节我们已经实现的更新值的逻辑,当我们更新完值后,就要去查看是否有其他 field 依赖这个 field,去更新被依赖的 field 的状态。
值改变时通知 Field 依赖项更新
所以,我们就要通知依赖项的更新,代码如下,
// file: ./src/rc-field-form/useForm.tsx
...
interface DependenciesUpdateInfo {
type: "dependenciesUpdate";
}
export type NotifyInfo =
| ValueUpdateInfo
| ValidateFinishInfo
| DependenciesUpdateInfo;
...
private updateValue = (name: NamePath, value: StoreValue) => {
const prevStore = this.store;
// 1. 更新 store 对象
...
// 2. 触发对应组件的更新
...
// 3. 触发依赖项更新
this.notifyObservers(prevStore, [name], {
type: "dependenciesUpdate",
});
};
onStoreChange 处理依赖更新
很明显,我们需要稍微对 onStoreChange
方法稍微改造一下,使得它能够判断 dependenciesUpdate
这种情况。代码如下,
// file: ./src/rc-field-form/Field.tsx
interface InternalFieldProps {
...
dependencies?: NamePath[];
}
...
class Field extends React.Component<InternalFieldProps> {
...
public onStoreChange: FieldEntity["onStoreChange"] = (
prevStore,
namePath,
info
) => {
...
switch (info.type) {
case "dependenciesUpdate": {
const { dependencies } = this.props;
if (dependencies) {
const dependenciesMatch = namePath.some((name) =>
dependencies.includes(name)
);
if (dependenciesMatch) {
this.reRender();
return;
}
}
}
default:
...
}
};
...
}
这里的处理也很简单,如果发现发生变化(onChange)的 field 与当前 field 的依赖项匹配,即当前 field 依赖这个发生改变的 field 时,就重新渲染当前 field。很简单有木有~
测试是否成功
我们来看上一篇开头就给出的例子,
// file: ./src/App.tsx
import Form, { useForm, Field } from "./rc-field-form";
import Input from "./rc-field-form/Input";
const nameRules = { required: true, message: "请输入姓名!" };
const passwordRules = { required: true, message: "请输入密码!" };
export default () => {
const [form] = useForm();
return (
<Form
form={form}
// preserve={false}
// onFinish={onFinish}
// onFinishFailed={onFinishFailed}
>
<Field name="name" rules={[nameRules]}>
<Input placeholder="Username" />
</Field>
<Field dependencies={["name"]}>
{() => {
return form.getFieldValue("name") === "1" ? (
<Field name="password" rules={[passwordRules]}>
<Input placeholder="Password" />
</Field>
) : null;
}}
</Field>
<Field dependencies={["password"]}>
{() => {
const password = form.getFieldValue("password");
console.log(">>>", password);
return password ? (
<Field name="password2">
<Input placeholder="Password 2" />
</Field>
) : null;
}}
</Field>
{/* <button onClick={() => form.submit()}>submit</button> */}
</Form>
);
};
仔细的读者已经发现了,我们有依赖项的 Field 组件的子元素都是 function
,但是我们现在还没有处理。在测试之前,我们先来补充处理一下这种情况,
// file: ./src/rc-field-form/Field.tsx
class Field extends React.Component<InternalFieldProps> {
...
public render() {
const { children } = this.props;
// 如果 children 的类型是 function 的话,我们直接执行函数
if (typeof children === "function") {
return (children as Function)();
}
...
}
}
万事大吉,我们来测试一下,
写在最后
恭喜你看到了这里,通过这篇文章的学习,我们已经实现了 And4 Form 的核心逻辑,相信你对它的理解也更深了。希望你从 Form 代码中学到的思想和各种处理方式(比如对 promise 的处理和 Field 的订阅机制)能够触类旁通,写出更好的代码~
当然,我们实现的只是最核心的逻辑。一些配套的功能还没实现,比如说,
- 我们现在连表单都提交不了(这耍个锤子哦)
- onFinish、onFinishFailed 等钩子也还没实现
- Field 之前有链式依赖的情况我们也还没处理
这些我们都留到下篇文章中实现,不过是在核心逻辑的基础上补充完善,最重要的还是我们学习到的代码思想。
最后,如果觉得文章写得还不错,点个 👍 嘿嘿嘿,这是我更文最大的动力!文章不免有错误出现,烦请大家指出~
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。