form 实现思路
前言
在中后台这个领域,form是一个离不开老生常谈的话题,有很多优秀的form解决方案, 比如react-form-hook,formik,formaily等,在使用这些比较成熟的解决方案的时候有想过怎么实现的呢, 我们今天一起来揭开这层神秘的面纱, 一起探讨一下实现一个form的思路以及核心编码在开始今天的话题之前,需要了解 react 的一个比较新的api(useSyncExternalStore),我会根据设计方案的制定以及实现思路来带着大家进行对编码的实现
设计方案
设计理念
- 状态管理:
FormStore
主要负责管理表单的状态,包括表单的值、初始值、错误信息、校验器和事件监听器。通过集中管理这些状态,可以简化表单逻辑并提高代码可维护性。 - 响应式更新: 当表单值发生变化时,
FormStore
会通知所有注册的监听器。这种设计允许视图组件能够自动更新,从而实现响应式 UI。 - 表单校验:
FormStore
提供了校验机制,确保表单提交时的有效性。通过注册校验器,可以对表单的不同字段进行单独的校验。 - 解耦逻辑:
FormStore
将表单的状态管理与 UI 逻辑解耦,使得表单逻辑更加模块化和可测试。
要解决的问题
- 状态同步: 确保表单的值与初始值之间的同步,支持表单的重置操作。
- 值更新: 提供方法更新表单字段的值,并确保这些变化能够被及时监听和响应。
- 错误处理: 在表单校验失败时,能够准确地记录并处理错误信息。
- 提交逻辑: 管理表单的提交行为,确保在提交之前进行必要的校验,并根据结果触发相应的回调函数。
在编程语言中,单一职则应该是耳目共熟了吧,在这一思想的指导下我们来写一下我们的form表单的整体设计思路(UML图,流程图)
UML图
- form组件:负责表单的提交以及表单组件的赋值
- form-item组件:负责控制常见的表单组件,负责收集校验规则
- 表单组件需要提供onchange和value事件
- 采用面向对象来管理相关的事件
根据以上思路整体的设计思路如下
流程图
- formItem组件触发onchange通知form组件收集校验规则
- form组件setValue/setVlaues通知form-item去set表单组件
- Input组件发生改变触发onchane并通知form组件进行set
流程图大致如下
编码实现
Form组件实现思路
- form 组件主要承担了表单的最后提交
- form组件负责管理Formstore类, formStore承担了所有的业务逻辑
- form组件提供了上下文供form-item组件使用
- form继承了原生form元素的能力
根据以上form承担的责任以及结合设计思路因此我们的FormConext大致实现思路如下
FormContext
xport interface FormContext {
values?: Record<string, unknown>; // 用于存储表单数据
onValuesChange?: (key: string, value: unknown) => void; // 用于监听表单数据变化
registerValidator?: (key: string, validator: () => void) => void // 注册校验规则
}
// 表单context
export const FormProvider = createContext<FormContext>({});
FormStore
实现了FormConetx以后我们来看一下我来设计一下我的FormStore类
interface Errors {
[key: string]: Error;
}
interface Validators {
[key: string]: () => void;
}
interface Listeners {
(): void;
}
interface HandleSubmit {
onFinish?: (values: FormValues) => void;
onFinishFailed?: (errors: FormErrors) => void;
}
interface IFormStore {
setValues(values: Record<string, unknown>): void;
setValue(key: string, value: unknown): void;
setInitialValues (values: Record<string, unknown>): void;
resetFields (): void;
onValuesChange (key: string, value: unknown): void;
emitChange(): void;
getFieldValue (key: string): unknown;
getFieldsValue (): Record<string, unknown>;
subscribe (callback: () => void): void;
handleSubmit (e: React.FormEvent<HTMLFormElement>,
options: HandleSubmit): Promise<void>;
registerValidator (key: string, validator: () => void);
}
interface HandleSubmitParams implements IFormStore {
onFinish?: (values: FormValues) => void;
onFinishFailed?: (errors: FormErrors) => void;
}
class FormStore {
private initialValues: Record<string, unknown>;
private values : Record<string, unknown>;
private errors: Record<string, Error>;
private validators: Record<string, () => void>;
private listeners: Array<() => void>;
constructor(initialValues: Record<string, unknown> ) {
this.initialValues = initialValues;
this.values = {};
this.errors = {};
this.validators = {};
this.listeners = [];
}
setValues(values: Record<string, unknown>) {
this.values = values;
}
setValue(key: string, value: unknown) {
this.values = {
...this.values,
[key]: value
};
}
setInitialValues (values: Record<string, unknown>) {
this.initialValues = values;
this.setValues(values);
}
resetFields () {
this.setValues(this.initialValues);
}
onValuesChange (key: string, value: unknown) {
if (key) {
this.setValues({
...this.values,
[key]: value,
});
this.emitChange()
}
}
emitChange() {
for (const listener of this.listeners) {
listener();
}
}
getFieldValue (key: string) {
return this.values[key];
}
getFieldsValue () {
return this.values;
}
subscribe (callback: () => void) {
this.listeners = [...this.listeners, callback];
return () => {
this.listeners = this.listeners.filter((listener) => listener !== callback);
}
}
// 表单提交
async handleSubmit (e: React.FormEvent<HTMLFormElement>,
{ onFinish, onFinishFailed }: HandleSubmit) {
e.preventDefault();
for (const key of Object.keys(this.validators)) {
try {
await this.validators[key]();
} catch (e) {
this.errors[key] = e as Error;
}
}
if (Object.keys(this.errors).length) {
onFinishFailed?.(this.errors);
} else {
onFinish?.(this.values);
}
}
registerValidator (key: string, validator: () => void) {
this.validators[key] = validator;
}
}
export default FormStore;
useForm
有了FormStore以后,我们根据Formstore来实现 useForm hook来管理
import { useRef } from "react";
import FormStore from "./store";
function useForm(values: Record<string, unknown> = {}) {
// 表单数据
const formStore = useRef<FormStore>(new FormStore(values || {}));
return [formStore.current];
}
export default useForm;
Form 组件
最后一步结合FormContext,FormStore,useForm 封装出我们的Form组件
export interface FormProps extends HtmlHTMLAttributes<HTMLFormElement>, PropsWithChildren {
className?: string; // 表单类名
style?: CSSProperties; // 表单样式
onFinish?: (values: Record<string, unknown>) => void; // 表单提交回调
onFinishFailed?: (values: Record<string, unknown>) => void; // 表单提交失败回调
initialValues?: Record<string, unknown>; // 表单初始值
form?: FormStore;
watch?: Record<string, Watch>;
}
export interface Watch {
immediate?: boolean;
handler: (value: unknown) => void;
}
const Form: FC<FormProps> = ({
className,
style, children,
onFinish,
onFinishFailed,
initialValues,
form,
watch = {},
...resetProps
}
) => {
// 表单数据
const [innerFormStore] = useForm(initialValues);
const formStore = form ?? innerFormStore;
const onValuesChange = (key: string, value: unknown) => {
watch?.[key]?.handler(value);
formStore.onValuesChange(key, value);
};
// 表单提交
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
formStore.handleSubmit(e, { onFinish, onFinishFailed });
};
const registerValidator = (key: string, validator: () => void) => {
formStore.registerValidator(key, validator);
};
const getFieldsValue = () => {
return formStore.getFieldsValue();
}
const subscribe = (callback: () => void) => {
return formStore.subscribe(callback);
}
const values = useSyncExternalStore(subscribe, getFieldsValue);
useEffect(() => {
for (const key of Object.keys(watch)) {
const { immediate, handler } = watch[key];
if (immediate) {
handler(formStore.getFieldValue(key));
}
}
}, [watch, formStore])
return (
<FormProvider.Provider value={{
values,
onValuesChange,
registerValidator,
}}>
<form className={className} style={style} {...resetProps} onSubmit={handleSubmit}>
{children}
</form>
</FormProvider.Provider>
);
};
export default Form;
Form-item实现
我们上面大致实现了Form组件的核心逻辑,接下来我们实现form-item组件,采用async-validator来进行数据校验
- form-item负责注入向其子组件value和onchange事件
- form-item负责收集校验规则
- form-item组件负责
大致代码逻辑实现如下
import { CSSProperties, ChangeEvent, Children, ReactElement, cloneElement, useContext, useState } from "react";
import { FormProvider } from ".";
import Schema, { Rule, ValidateError } from 'async-validator';
export interface FormItmProps {
className?: string; // 表单项类名
style?: CSSProperties; // 表单项样式
name: string; // 表单项名称
label?: string; // 表单项标签
rules?: Rule; // 表单项校验规则
children?: ReactElement; // 表单项内容
valuePropName?: string; // 表单项值属性名
onChange?: (e: ChangeEvent<HTMLInputElement>) => void; // 表单项值变化回调
value?: string | number | boolean; // 表单项值
}
const FormItem = ({
className,
style,
name, label,
rules,
valuePropName = 'value',
children,
...resetProps
}: FormItmProps) => {
const { onValuesChange, values = {}, registerValidator } = useContext(FormProvider);
const [value, setValue] = useState<string | number | boolean>();
const [error, setError] = useState<string | undefined>('');
if (name && value != values[name]) {
setValue(values[name] as string | number | boolean);
}
const validate = (value: unknown) => {
if (!rules) {
return
}
const newRules = Array.isArray(rules) ? rules : [rules];
const validator = new Schema({
[name]: newRules.map((rule) => ({ ...rule, })),
});
return validator.validate({ [name]: value }, {}, (errors: ValidateError[] | null) => {
if (errors && errors.length) {
setError(errors[0].message);
} else {
setError('');
}
});
};
const onChange = async (e: ChangeEvent<HTMLInputElement>) => {
const value = getValueFormEvent(e);
if (name) {
registerValidator?.(name, () => validate(value));
}
try {
const hasValidate = await validate(value);
if (name && hasValidate) {
setValue(value);
}
} catch (e) {
setError(e.message);
} finally {
if (name) {
onValuesChange?.(name, value);
}
}
resetProps.onChange?.(e);
}
const getValueFormEvent = (e: ChangeEvent<HTMLInputElement>) => {
const { target } = e;
return target.type === 'checkbox' ? target.checked : target.value;
}
const childEle = Children.toArray(children).length > 1 ? children : cloneElement(children as ReactElement,
{
[valuePropName]: name ? value : resetProps.value,
onChange,
})
if (!name) {
return children;
}
return (
<div className={className} style={style}>
<label htmlFor={name}>{label}</label>
{childEle}
{error && <div>{error}</div>}
</div>
);
};
export default FormItem;
转载自:https://juejin.cn/post/7385583777732083746