写个极简表单校验模型createCheckModel

一个Form表单极简的字段校验模型,框架无关,Typescript支持,实际引用大小不到2kb

安装

npm i @zr-lib/check-model

实现 createCheckModel

  • 在字段校验方法内
    • 建议value的取值在source[key]取值之前,这样在模板调用的时候只要传value就可以了,防止在外面页面渲染调用时其他字段改动而触发校验
    • value如果是undefined才会使用到source
    • 如果同一个字段在不同tab有不同的规则,此时字段校验方法可以传入第三个参数extras,类型手动指定;然后注意调用_validate方法的时候也要传入
  • createCheckModel的参数会根据Source自动推导校验字段对应的方法参数类型深层key校验可以看下文DeepKeyType
  • 使用createCheckModel创建checkModel定义或者调用字段校验方法都会有类型提示类型校验
  • 校验方法支持返回boolean/string,方便直接显示错误;注意不要返回true
  • 内部定义的_state对象:
    • 记录上次的字段校验状态
    • 存储一些上次调用checkModel[key](data[key])校验的结果;
    • _statekeycheckModelkey一致
  • 内部定义的_validate(source: S, checkConfig?: Partial<Record<keyof S, boolean>>, extras?: any)方法:
    • 校验是否存在错误,打印所有校验错误的提示
    • checkConfig不传时,默认触发全部字段的校验方法
    • 支持多个tab但是字段不一样的选定字段校验,checkConfig传入不同tab需要校验的key,此时只会触发对应字段的校验方法,就可以在一个 createCheckModel下定义所有字段的校验方法了
    • 如果同一个字段在不同tab有不同的规则,此时字段校验方法可以传入第三个参数extras,此处也需要传入;这个参数最好是一个对象,其他字段在_validate方法传入的是一样的,方便不同取值

关于Promise:一般检查重名等可能需要提前请求,手动写检查方法另外判断即可,这种情况比较少,基本就一个字段需要

type HasErrorFn<V = any, S = any, Extra = any> = (
  value: V,
  source?: S,
  extras?: Extra
) => string | boolean;
type ErrorModel<S, Extra> = {
  [K in keyof S]?: HasErrorFn<K extends keyof S ? S[K] : any, S, Extra>;
};

/**
 * 返回字段错误校验Model(范型传递)
 * @description
 * - 校验方法返回错误原因;false/''则视为无错误;
 * - 如果要显示错误原因就不要返回true
 */
export function createCheckModel<
  S extends Record<string, any>,
  Extra extends Record<string, any> = any
>(model: ErrorModel<S, Extra>) {
  /** 存储一些上次调用model校验之后的提示信息 */
  const _state: Partial<Record<keyof S, string | boolean>> = {};

  const keys = Object.keys(model) as (keyof S)[];
  keys.forEach((k) => {
    const check = model[k];
    model[k] = function (...args) {
      _state[k] = check?.apply(this, args);
      return _state[k] ?? '';
    };
  });

  /**
   * 校验是否存在错误
   * @param checkConfig {} 传入需要校验的key,默认全部校验
   * - 如果有`多个tab`但是字段不一样的情况,此时只写一个`createCheckModel`就可以了
   */
  function _validate(source: S, checkConfig?: Partial<Record<keyof S, boolean>>, extras?: Extra) {
    const arr = [] as string[];
    keys.forEach((k) => {
      if (checkConfig && !checkConfig[k]) {
        delete _state[k];
        return;
      }
      const check = model[k];
      const result = check?.(undefined as never, source, extras);
      if (result) arr.push((k as string) + ': ' + result);
    });
    // 打印所有校验错误的提示数组
    if (arr.length) console.warn('[checkModel]#####_state', _state);
    return !!arr.length;
  }

  return { ...model, _state, _validate };
}

/** 返回当前应该取值的数据来源 */
export function getCurrentValue<K extends keyof S, S extends Record<string, any>>(
  value: S[K],
  source: S | undefined,
  key: K
) {
  if (value !== undefined) return value;
  if (source !== undefined) return source[key];
}

使用 createCheckModel

表单数据与Source

定义当前Source表单类型与数据data

注意:

  • 有时候一个字段的数据是一个对象,然后里面也有多个内部字段需要校验,
  • 一般会扁平展开只设置一层如content.length/content.type
    • 因为本身是不存在content.length这个key的,
    • 需要在定义Source的时候手动加上类型补充DeepKeyType所有字段都可选-绕过data类型定义;
    • 调用字段校验方法时,传入正确的数据data.content.length,而不是data['content.length']
type DataType = {
  name: string;
  id: number;
  content: {
    length: number;
    type: 'string' | 'boolean';
  };
};
type DeepKeyType = {
  // 这里的深层key实际上data数据里面是不存在的,注意传递相应的数据
  'content.length'?: DataType['content']['length'];
  'content.type'?: DataType['content']['type'];
};

type Source = DataType & DeepKeyType;

const data = reactive<Source>({
  name: '',
  id: 0,
  content: {
    length: 1,
    type: 'boolean'
  }
});

创建 checkModel

创建后,导出供外部代码调用校验

checkModelkey跟对应的方法,在编写校验代码调用校验方法时都会有自动补全和类型校验

字段校验方法内注意未传值undefined的处理

  • 单个字段时,字段校验方法的source参数不需要传,为undefined
  • 调用_validate方法时,字段校验方法的value参数为undefined
  • 如id字段的校验,使用到了第三个参数extra
    • 一般可能多个字段都需要extra,此时最好传对象,这样多个字段可以按需取值
    • 在调用单个字段校验方法时传参如:checkModel.id(data.id, undefined, extra)

type Extra = { appType: AppType };

enum AppType {
  six = 1,
  eight = 2
}

export function checkId(type: AppType, id: number) {
  if (!/^[0-9+]+$/.test(id + '')) return '请输入数字整数';
  if (type === AppType.six) return `${id}`.length > 6 ? 'id不能超过6位数字' : '';
  if (type === AppType.eight) return `${id}`.length > 8 ? 'id不能超过8位数字' : '';
  return '';
}

const checkModel = createCheckModel<Source, Extra>({
  name: (value, source) => {
    if (value !== undefined && !value) return '不能为空';
    if (source !== undefined && !source?.name) return '不能为空';
    return '不能为空';
  },
  id: (value, source, extra) => {
    const { appType } = extra || {};
    const currentValue = getCurrentValue(value, source, 'id');
    return !currentValue ? '不能为空' : checkId(appType!, currentValue);
  },
  content: (value, source) => {
    if (value !== undefined && !value) return '不能为空';
    if (source !== undefined && !source?.content) return '不能为空';
    return '不能为空';
  },
  'content.length': (value, source) => {
    if (value !== undefined && !value) return '不能为空';
    if (source !== undefined && !source?.content.length) return '不能为空';
    return '不能为空';
  },
  'content.type': (value, source) => {
    if (value !== undefined && !value) return '不能为空';
    if (source !== undefined && !source?.content.type) return '不能为空';
    return '不能为空';
  }
});
  • getCurrentValue 辅助函数

使用 getCurrentValue如下,只需要一个currentValue,不用管从value还是source取值

const checkModel = createCheckModel<Source, Extra>({
  id: (value, source, extra) => {
    const { appType } = extra || {};
    const currentValue = getCurrentValue(value, source, 'id');
    return !currentValue ? '不能为空' : checkId(appType!, currentValue);
    // if (value !== undefined) return checkId(appType!, value);
    // if (source !== undefined) return checkId(appType!, source.id);
    // return '不能为空';
  },
  // ...
}

使用 checkModel

检查单个字段

  • 外部样式class: error调用checkModel下面的方法(key对应传入的数据key),传入响应式的数据
  • 内部显示错误信息直接引用_state下面的属性即可

这种情况不传第二个参数source,就不会在页面引用的地方被其他字段触发重新渲染了

调用checkModel[key](data[key])方法是为了响应式,调用checkModel._state[key]是为了方便多次引用,避免多次写很长的代码

  • 模板使用
<div
  class="edit-wrap"
  :class="{ error: saveInfo.clicked && checkModel.appId?.(currentConfig.appId || '') }"
  >
  <Input v-model:value="currentConfig.appId" placeholder="必填" />
  <div class="status-text error" v-if="saveInfo.clicked && checkModel._state.appId">
    {{ checkModel._state.appId }}
  </div>
</div>
// 这个是利用data.name的响应式触发,一般是外部样式class: error的地方用
checkModel.name?.(data.name);

// 这个是内部显示错误提示用,需要在响应式触发后使用,或者定义的时候使用reactive等包起来
checkModel._state.name; 

如果需要多处调用,使用computed/useMemo即可:

const nameError = computed(() => checkModel.name(data.name));

检查所有字段

同时会打印所有校验结果到控制台

  • 默认触发所有字段校验方法
const hasError = checkModel._validate(data);
  • 只触发部分字段校验方法
    • 适用于多个tab切换,只写一个createCheckModel就可以校验所有字段
    • 不同tab传入对应的checkConfig即可

如下只校验name字段

const hasError = checkModel._validate(data, { name: true });
  • 第三个参数extra
const hasError = checkModel._validate(data, undefined, extra);

原文链接:https://juejin.cn/post/7346510823772667958 作者:五分熟的咸鱼

(0)
上一篇 2024年3月17日 上午11:09
下一篇 2024年3月17日 下午4:00

相关推荐

发表回复

登录后才能评论