跳转到内容

formConfig — 表单配置

import * as v from 'valibot';
import { formConfig } from '@piying/view-angular-core';

const schema = v.object({
  // 禁用
  a: v.pipe(v.string(), formConfig({ disabled: true })),
  // 失焦才更新模型
  b: v.pipe(v.string(), formConfig({ updateOn: 'blur' })),
  // 空数组输出 []
  c: v.pipe(v.array(v.string()), formConfig({ emptyValue: [] })),
});

disabled / disabledValue / transformer / pipe / defaultValue / validators / asyncValidators / updateOn / emptyValue / deletionMode / groupMode / groupKeySchema / groupValueSchema / disableOrUpdateActivate,各项含义见下文分节说明。

字段 说明
disabled 禁用此字段
disabledValue 'reserve'(默认)保留值;'delete' 不输出值
formConfig({ disabled: true });
formConfig({ disabled: true, disabledValue: 'delete' });

需要跟随其他字段的值动态禁用时,用 disableWhen

transformer 做同步的值转换,两个方向都可以配置;pipe 对值流套一层 RxJS 管道(去抖、过滤等),目前只支持 toModel 方向。

两个方向的回调都接收「当前值 + 当前控件」,返回转换后的新值:

  • toModel:视图 → 模型
  • toView:模型 → 视图
import { pipe, debounceTime, filter, map } from 'rxjs';

const schema = v.object({
  // toModel:去空格 + 转大写
  code: v.pipe(
    v.string(),
    formConfig({
      transformer: {
        toModel: (value) => value?.replace(/\s/g, '').toUpperCase() ?? '',
      },
    }),
  ),

  // toView:统一以小写展示
  name: v.pipe(
    v.string(),
    formConfig({
      transformer: {
        toView: (value) => value?.toLowerCase() ?? '',
      },
    }),
  ),
});

// 去抖 + 过滤 + 转换
formConfig({
  pipe: {
    toModel: pipe(
      debounceTime(300),
      filter((v) => v.trim().length > 0),
      map((v) => v.toLowerCase()),
    ),
  },
});

validators / asyncValidators 接受验证函数数组。

输入:当前字段控件 control,通过 control.value 读当前值。

输出:返回 undefined 表示通过;需要报错时返回两种格式之一:

  • 旧格式(向后兼容):{ [错误名]: 错误信息 }
  • 新格式(推荐):错误数组,每一项形如 { kind, metadata?, message? }
    • kind:错误类型标识
    • metadata:附加数据(如最小长度、实际值)
    • message:展示用文案
formConfig({
  validators: [
    (control) => {
      if (control.value.length < 6) {
        return [
          {
            kind: 'minLength',
            metadata: { required: 6, actual: control.value.length },
            message: '最少 6 个字符',
          },
        ];
      }
      return undefined;
    },
  ],
  asyncValidators: [
    async (control) => {
      const { available } = await fetch(`/api/check?value=${control.value}`).then((r) => r.json());
      if (!available) {
        return [{ kind: 'duplicate', metadata: { value: control.value }, message: '该值已存在' }];
      }
      return undefined;
    },
  ],
});

异步验证器可返回 Promise / Observable / Signal,与同步验证器并行执行,错误自动合并;验证期间控件状态为 PENDING

更多校验写法见 自定义验证

formConfig({ updateOn: 'change' }); // 默认,输入即更新
formConfig({ updateOn: 'blur' }); // 失焦更新
formConfig({ updateOn: 'submit' }); // 提交时更新
字段 适用 说明
emptyValue array / group / logic group 聚合结果为空(空数组、空对象、无匹配分支)时,最终值取 emptyValue
deletionMode array 'shrink'(默认)删除后缩短;'mark' 该位置置 undefined,长度不变
groupMode group / array 多余键值 / 超出项的处理方式,见下表
groupKeySchema group(record) 键的类型约束
groupValueSchema group / array 值 / 元素的类型约束
formConfig({ emptyValue: [] }); // 数组为空时输出 []
formConfig({ deletionMode: 'mark' });

v.intersect() / v.union() 解析出的逻辑组,在 or 类型下更新值时会自动激活第一个匹配的分支。设为 true 可关闭这一自动激活:

formConfig({ disableOrUpdateActivate: true });

下列字段一般不需要手动设置,写好 schema 即自动得出;手动写入的值会覆盖推导结果。

字段 来源
undefinedable v.optional() / v.undefinedable()
nullable v.nullable() / v.nullish()
required !undefinedable && !nullable
defaultValue v.optional(schema, 默认值) 的第二个参数
groupMode v.object()defaultv.looseObject()loosev.strictObject()strictv.array() / rest 部分 → reset
groupKeySchema v.record() 的键 schema
groupValueSchema v.record() 的值 schema / v.array() 的元素 schema
const schema = v.object({
  a: v.string(), // required: true
  b: v.optional(v.string()), // required: false, undefinedable: true, groupMode: 'default'
  c: v.optional(v.nullable(v.string()), '默认值'), // nullable: true, defaultValue: '默认值'
  d: v.looseObject({ x: v.string() }), // groupMode: 'loose'
});
import * as v from 'valibot';
import { formConfig } from '@piying/view-angular-core';
import { debounceTime, filter, map, pipe } from 'rxjs';

const schema = v.object({
  // 搜索框:去抖 + 过滤 + 转换
  search: v.pipe(
    v.string(),
    formConfig({
      updateOn: 'change',
      pipe: {
        toModel: pipe(
          debounceTime(300),
          filter((v) => v.trim().length > 0),
          map((v) => v.toLowerCase()),
        ),
      },
    }),
  ),

  // 价格:去空格 + 结构化校验
  price: v.pipe(
    v.string(),
    formConfig({
      transformer: {
        toModel: (value) => value?.trim() ?? '',
      },
      validators: [
        (control) => {
          const val = parseFloat(control.value);
          if (val < 0) {
            return [{ kind: 'negative', message: '价格不能为负' }];
          }
          if (val > 999999) {
            return [{ kind: 'tooLarge', message: '价格超出范围' }];
          }
          return undefined;
        },
      ],
    }),
  ),

  // 用户名:同步长度校验 + 异步唯一性校验
  username: v.pipe(
    v.string(),
    formConfig({
      validators: [
        (control) =>
          control.value.length < 3
            ? [{ kind: 'tooShort', message: '用户名至少 3 个字符' }]
            : undefined,
      ],
      asyncValidators: [
        async (control) => {
          const { available } = await fetch(
            `/api/check-username?name=${control.value}`,
          ).then((r) => r.json());
          return available
            ? undefined
            : [
                {
                  kind: 'usernameTaken',
                  metadata: { value: control.value },
                  message: '该用户名已被占用',
                },
              ];
        },
      ],
    }),
  ),

  // 数组:mark 删除模式 + 空值回退
  tags: v.pipe(
    v.array(v.string()),
    formConfig({ deletionMode: 'mark', emptyValue: [] }),
  ),
});