formily 自定义字段实现校验
实现思路
- 1、通过顶层的状态管理,触发组件校验
- 2、自定义组件内部实现错误状态的显示及校验逻辑
- 3、通过定义高阶组件监听状态,触发组件内部校验
- 4、校验触发后有错误,则通过 field.setSelfErrors(['']) 进行错误设置,不过此处设置的错误信息为空,只是利用 formily 自带校验器抛出错误阻止表单提交,不触发 formily 的 errorMsg 显示;errorMsg 的显示及状态均由高阶组件实现
核心代码
高阶组件实现代码 factoryCustomField.tsx
import { useField } from "@formily/react";
import { isEmpty } from "lodash-es";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useValidatorContext } from "./useValidatorContext";
interface PropsType {
requiredMsg?: string;
required?: boolean;
onChange?: (val: any) => void;
[key: string]: any;
}
interface ValidatorMessage {
message: string;
type: string;
}
type ValidatorResult = ValidatorMessage | null | undefined;
/**
* 自定义字段校验器
* @param Comp 自定义组件
* @param options 参数
* @returns 包装后的自定义组件
*
* 该组件完全自定义字段的必填显示、及errorMsg显示
*
* 必填设置需要通过 props 进行设置
* props: {
required: true,
requiredMsg: '必填'
},
*/
export const validatorCustomField = (
Comp: any,
options: {
// label 内容
labelClassName?: string,
label: string,
labelWidth?: number,
errorClassName?: string,
isCustomError?: boolean, // 是否自定义错误内容
}
): any => {
const CustomComp = (props: PropsType) => {
const { requiredMsg, required, onChange, ...extra } = props || {};
const { label, labelWidth, errorClassName, isCustomError, labelClassName } =
options || {};
// 组件状态
const [status, setStatus] = (useState < "error") | ("" > "");
// 错误信息
const [errorMsg, setErrorMsg] = useState("");
const field: any = useField();
const { state } = useValidatorContext();
const compRef =
useRef <
{ validator: null | Function } >
{
validator: null,
};
// 字段触发 change 事件时清除 error
const handleChange = useCallback(
(val: any) => {
if (status === "error") {
field.setSelfErrors([]);
setStatus("");
setErrorMsg("");
}
onChange && onChange(val);
},
[status]
);
// isValidate 为 true 时触发校验
useEffect(() => {
if (state.isValidate) {
// exclude 不需要校验的字段
if (state.validatorOption?.exclude?.includes(field.componentType)) {
return;
}
// 校验必填
if (
required &&
isEmpty(
typeof field.value === "number" ? `${field.value}` : field.value
)
) {
const errorMsg: string = !requiredMsg
? "该字段是必填字段"
: requiredMsg;
setStatus("error");
setErrorMsg(errorMsg);
// 设置form字段错误,msg 使用自定义组件显示,所以置为空字符串
field.setSelfErrors([""]);
return;
}
// 检查是否存在校验器
if (!compRef.current?.validator) return;
// 校验组件内部自定义规则函数, 返回值:message 错误信息,type 错误类型
const validatorResult = () => {
return (
new Promise() <
ValidatorMessage >
(async (resolve) => {
resolve(await compRef.current.validator());
})
);
};
validatorResult().then((result) => {
if (result && result.type === "error") {
setStatus("error");
setErrorMsg(result.message);
field.setSelfErrors([""]);
}
});
}
}, [state, field, required, requiredMsg]);
return (
<div className="flex">
{label && (
<div
className={`text-right ${labelClassName}`}
style={{
width: labelWidth || "auto",
}}
>
<span
className={`text-error mr-1 font-[SimSun,sans-serif] ${
!required ? "opacity-0" : ""
}`}
>
*
</span>
{label || ""}:
</div>
)}
<div className="flex flex-col flex-1">
<Comp
{...extra}
required={required}
status={status}
onChange={handleChange}
ref={compRef}
/>
{status === "error" && !isCustomError && (
<div className={`text-error ${errorClassName}`}>
{errorMsg || "该字段是必填字段"}
</div>
)}
</div>
</div>
);
};
return CustomComp;
};
错误状态管理实现代码 useValidatorContext.tsx
import React, { useContext, useReducer } from "react";
interface ReducerType {
isValidate?: boolean; // 是否进行校验
validatorOption?: {
exclude?: string[], // 排除不需要校验的字段
callback?: () => void,
};
}
interface InitData {
state: ReducerType;
actions: {
validator: (option?: { exclude?: string[], callback?: () => void }) => void,
};
}
interface PropsType {
children?: any;
[key: string]: any;
}
const ValidatorContext = (React.createContext < InitData) | (null > null);
const reducerData: ReducerType = {
isValidate: false,
};
const reducer = (
state: ReducerType,
action: { type: string, payload: ReducerType }
) => {
switch (action.type) {
case "info":
return Object.assign({}, state, action.payload);
default:
return state;
}
};
const ValidatorProvider = (props: PropsType) => {
const [state, dispatch] = useReducer(reducer, reducerData);
const dispatchFun = (payload: ReducerType) =>
dispatch({ type: "info", payload });
// 触发组件内部校验函数,触发之后将 isValidate 置为 false
const validator = (option?: {
exclude?: string[],
callback?: () => void,
}) => {
dispatchFun({ isValidate: true, validatorOption: option });
setTimeout(() => dispatchFun({ isValidate: false }));
};
const ValidatorContextValue = {
state,
actions: {
validator,
},
};
return (
<ValidatorContext.Provider value={ValidatorContextValue}>
{props.children}
</ValidatorContext.Provider>
);
};
const withProvider = (Comp: any, options?: PropsType) => {
return React.forwardRef((props: any, ref: any) => {
return (
<ValidatorProvider {...(options || {})}>
<Comp {...props} ref={ref || null} />
</ValidatorProvider>
);
});
};
const useValidatorContext = () => {
const context = useContext(ValidatorContext);
if (!context) {
throw new Error("useValidator must use in ValidatorContext");
}
return context;
};
export { useValidatorContext, ValidatorProvider, withProvider };
具体用法
form 中的具体使用 DemoForm.tsx
核心 Api - validatorActions.validator()
// 引入状态管理器context,在表单提交时触发 validatorActions.validator() 进行校验
const { actions: validatorActions } = useValidatorContext();
validatorActions.validator();
使用示例
import { useValidatorContext, withProvider } from "./hooks/useValidatorContext";
const DemoForm = (props: PropsType) => {
const { actions: validatorActions } = useValidatorContext();
return (
<>
<Button
type="primary"
onClick={() => {
// 触发自定义字段校验
validatorActions.validator();
// 触发表单其他字段校验
form.submit((val: any) => {
// 调用接口
fetchForm(val);
});
}}
htmlType="submit"
>
提交
</Button>
</>
);
};
export default withProvider(DemoForm);
自定义字段包装
核心 Api - validatorCustomField
// 调用高阶函数进行包装
validatorCustomField(WorkTime, {
label: '营业时间',
labelWidth: 140
})
// 设置必填,配置 schema x-component-props
props: {
required: true,
requiredMsg: '营业时间不能为空!'
},
使用示例
import { FormItem } from "@formily/antd-v5";
import { createSchemaField } from "@formily/react";
import React from "react";
import { validatorCustomField } from "./hooks/factoryCustomField";
const SchemaField = createSchemaField({
components: {
WorkTime: validatorCustomField(WorkTime, {
label: "营业时间",
labelWidth: 140,
}),
},
});
const DemoSchema = () => {
const schema: any = {
type: "object",
properties: {},
};
const fieldList = [
{
name: "workTime",
props: {
required: true,
requiredMsg: "营业时间不能为空!",
},
component: "WorkTime",
},
];
fieldList.forEach((field: any) => {
const {
validator,
name,
type,
title,
required,
component,
props,
decoratorProps,
...extra
} = field;
schema.properties[field.name] = {
type,
title,
required,
"x-decorator": "FormItem",
"x-decorator-props": {
className: "!mb-6",
...(decoratorProps || {}),
},
"x-component": component,
"x-component-props": props,
"x-validator": validator,
...extra,
};
});
return <SchemaField schema={schema} />;
};
export default DemoSchema;
使用示例-自定义组件
import React, { useCallback, useEffect, useState } from "react";
import { Button, Modal, Spin } from "antd";
import { CheckOutlined } from "@ant-design/icons";
import { debounce } from "lodash-es";
import FormatInput from "@/components/FormatInput";
import { useDataContext } from "../hooks/useDataContext";
const OrderUserMobile = (props: any, ref: any) => {
const validator = useCallback(() => {
const reg = /^(?:(?:\+|00)86)?1[3-9]\d{9}$/;
if (!reg.test(props?.value)) {
return { message: "手机号格式错误", type: "error" };
}
return null;
}, [props]);
useEffect(() => {
if (ref && ref.current) ref.current = { validator };
}, [ref, validator]);
return (
<Spin spinning={loading}>
<div className="flex">
<FormatInput
{...(props || {})}
suffix={
isSuccess === true ? (
<CheckOutlined className="text-success text-[14px]" />
) : null
}
status=""
/>
</div>
</Spin>
);
};
export default React.forwardRef(OrderUserMobile);