写个极简表单校验模型createCheckModel

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

安装

bash 复制代码
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:一般检查重名等可能需要提前请求,手动写检查方法另外判断即可,这种情况比较少,基本就一个字段需要

typescript 复制代码
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']
typescript 复制代码
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)
typescript 复制代码
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取值

typescript 复制代码
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]是为了方便多次引用,避免多次写很长的代码

  • 模板使用
html 复制代码
<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>
typescript 复制代码
// 这个是利用data.name的响应式触发,一般是外部样式class: error的地方用
checkModel.name?.(data.name);

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

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

typescript 复制代码
const nameError = computed(() => checkModel.name(data.name));

检查所有字段

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

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

如下只校验name字段

typescript 复制代码
const hasError = checkModel._validate(data, { name: true });
  • 第三个参数extra
typescript 复制代码
const hasError = checkModel._validate(data, undefined, extra);
相关推荐
前端Hardy16 分钟前
HTML&CSS:有趣的漂流瓶
前端·javascript·css
前端Hardy18 分钟前
HTML&CSS :惊艳 UI 必备!卡片堆叠动画
前端·javascript·css
无羡仙42 分钟前
替代 Object.freeze 的精准只读模式
前端·javascript
小菜全1 小时前
uniapp新增页面及跳转配置方法
开发语言·前端·javascript·vue.js·前端框架
白水清风2 小时前
关于Js和Ts中类(class)的知识
前端·javascript·面试
前端Hardy2 小时前
只用2行CSS实现响应式布局,比媒体查询更优雅的布局方案
javascript·css·html
车口2 小时前
滚动加载更多内容的通用解决方案
javascript
艾小码2 小时前
手把手教你实现一个EventEmitter,彻底告别复杂事件管理!
前端·javascript·node.js
拜无忧4 小时前
2025最新React项目架构指南:从零到一,为前端小白打造
前端·react.js·typescript
冰冷的bin4 小时前
【React Native】点赞特效动画组件FlowLikeView
react native·react.js·typescript