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);
powered by Gitbook该文件修订时间: 2024-04-02 10:21:09

results matching ""

    No results matching ""