本文介绍如何在 Piying-View 中使用自定义验证器,包括同步和异步验证。
validation-demo.definition.ts
import * as v from 'valibot';
import { actions, formConfig } from '@piying/view-angular-core';
export const schema = v.object({
password: v.pipe(
v.string(),
actions.attributes.set({ placeholder: '密码(至少 8 位,不含 123)' }),
formConfig({
validators: [
(control) => {
const value = control.value ?? '';
if (value.includes('123')) {
return [
{
kind: 'weakPassword',
message: '密码不能包含连续数字',
},
];
}
if (value.length < 8) {
return [
{
kind: 'tooShort',
message: '密码至少 8 个字符',
},
];
}
return undefined;
},
],
}),
),
confirmPassword: v.pipe(
v.string(),
actions.attributes.set({ placeholder: '确认密码(需与密码一致)' }),
formConfig({
validators: [
(control) => {
const password = control.root.get('password')?.value;
if (control.value && password !== control.value) {
return [
{
kind: 'passwordsNotMatch',
message: '两次密码不一致',
},
];
}
return undefined;
},
],
}),
),
});
export const model = { password: '', confirmPassword: '' };
validators 接受一个验证函数数组,每个验证函数接收 AbstractControl 并返回错误或 null/undefined:
validation-sync.definition.ts
import * as v from 'valibot';
import { formConfig } from '@piying/view-angular-core';
export const schema = v.object({
password: v.pipe(
v.string(),
formConfig({
validators: [
(control) => {
if (control.value?.includes('123')) {
return [
{
kind: 'weakPassword',
message: '密码不能包含连续数字',
},
];
}
return undefined;
},
(control) => {
if (control.value?.length < 8) {
return { tooShort: '密码至少 8 个字符' };
}
return undefined;
},
],
}),
),
});
export const model = { password: 'abc123' };
输入:当前字段控件 control,通过 control.value 读当前值。
输出:
- 返回
null / undefined:验证通过
- 返回
{ [errorKey]: errorValue }:旧格式错误(向后兼容)
- 返回
[{ kind, metadata, message? }]:新格式错误(推荐)
asyncValidators 接受异步验证函数数组,用于网络请求等耗时操作,支持 Promise/Observable/Signal:
validation-async.definition.ts
import * as v from 'valibot';
import { formConfig } from '@piying/view-angular-core';
export const schema = v.object({
username: v.pipe(
v.string(),
formConfig({
asyncValidators: [
async (control) => {
const response = await fetch(
`/api/check-username?name=${control.value}`,
);
const available = await response.json();
if (!available) {
return [
{
kind: 'usernameTaken',
metadata: { value: control.value },
message: '用户名已被占用',
},
];
}
return undefined;
},
],
}),
),
});
export const model = { username: '' };
Piying-View 同时支持 Valibot 内置验证和自定义验证器,两者并行执行:
validation-combined.definition.ts
import * as v from 'valibot';
import { formConfig } from '@piying/view-angular-core';
export const schema = v.object({
email: v.pipe(
v.string(),
v.email('邮箱格式不正确'),
formConfig({
validators: [
(control) => {
if (control.value?.endsWith('.test')) {
return [
{
kind: 'testDomain',
message: '不能使用测试域名',
},
];
}
return undefined;
},
],
}),
),
});
export const model = { email: 'test@test.com' };
Valibot 验证和自定义验证器的错误会合并到 control.errors 中:
// 输入 "test@test.test"
control.errors = [
{ kind: 'valibot', metadata: [...] }, // Valibot 验证失败
{ kind: 'testDomain', message: '...' }, // 自定义验证器失败
];
比较两个字段是否相等(如密码确认):
validation-compare.definition.ts
import * as v from 'valibot';
import { formConfig } from '@piying/view-angular-core';
export const schema = v.pipe(
v.object({
password: v.pipe(v.string()),
confirmPassword: v.pipe(v.string()),
}),
formConfig({
validators: [
(control) => {
const parent = control.root;
if (parent.value?.password !== parent.value?.confirmPassword) {
return [
{
kind: 'passwordsNotMatch',
message: '两次密码不一致',
},
];
}
return undefined;
},
],
}),
);
export const model = { password: '12345678', confirmPassword: '87654321' };
validation-conditional.definition.ts
import * as v from 'valibot';
import { formConfig } from '@piying/view-angular-core';
export const schema = v.pipe(
v.object({
hasAddress: v.boolean(),
address: v.pipe(v.string()),
}),
formConfig({
validators: [
(control) => {
const parent = control.root;
if (parent.value?.hasAddress && !parent.value?.address) {
return [
{
kind: 'conditionalRequired',
message: '需要填写地址',
},
];
}
return undefined;
},
],
}),
);
export const model = { hasAddress: true, address: '' };
validation-dynamic-error.definition.ts
import * as v from 'valibot';
import { formConfig } from '@piying/view-angular-core';
export const schema = v.object({
age: v.pipe(
v.number(),
formConfig({
validators: [
(control) => {
if (control.value < 0) {
return [
{
kind: 'negative',
metadata: { value: control.value },
message: `年龄不能为负数(当前: ${control.value})`,
},
];
}
if (control.value > 150) {
return [
{
kind: 'tooOld',
metadata: { value: control.value },
message: `年龄不合理(当前: ${control.value})`,
},
];
}
return undefined;
},
],
}),
),
});
export const model = { age: 25 };
也可以使用 group 只在专门的组中进行验证,通过 validGroup 组件包裹需要验证的字段:
valid-group.definition.ts
import * as v from 'valibot';
import { setComponent } from '@piying/view-angular-core';
export const schema = v.pipe(
v.object({
k1: v.pipe(v.string()),
k2: v.pipe(
v.string(),
v.check((value) => value === 'k2-value', 'should input k2-value'),
),
}),
setComponent('validGroup'),
);
validGroup 是一个自定义分组组件,仅对组内字段进行验证并集中显示错误。
Piying-View 的 FieldControl 提供标准接口访问验证状态:
| 属性 |
类型 |
说明 |
control.errors |
ValidationErrors2[] | null |
当前所有错误(Valibot + 自定义) |
control.valid |
boolean |
是否所有验证都通过 |
control.status$$() |
'VALID' | 'INVALID' | 'PENDING' |
验证状态(包括异步验证中) |
control.dirty |
boolean |
值是否被修改过 |
control.touched |
boolean |
是否被聚焦过 |
control.pristine |
boolean |
是否未被修改过 |
新格式错误访问示例:
if (control.errors) {
for (const error of control.errors) {
if (error.kind === 'usernameTaken') {
console.log('用户名已占用:', error.metadata?.value);
} else if (error.kind === 'valibot') {
// Valibot 验证失败
const issues = error.metadata;
}
}
}
Piying-View 支持两种验证方式,二者并行执行:
| 验证方式 |
说明 |
特点 |
| Valibot 验证 |
在 Valibot 解析阶段进行 |
只能验证自身,无法以上下文值验证 |
| 控件自带验证 |
由 formConfig.validators 注册,独立执行 |
可访问父级,根据其他字段值判断 |
// Valibot 内置验证
v.pipe(v.string(), v.minLength(5));
// 自定义验证
v.pipe(
v.string(),
v.check((value) => value === 'k2-value', 'should input k2-value'),
);
错误合并:两种方式相互独立执行,不存在「一个失败就跳过另一个」——control.errors 中先是自定义验证器的错误,若 Valibot 验证不通过,再追加一条 { kind: 'valibot', metadata: issues } 错误。
Valibot 提供了预制的国际化支持,可按浏览器语言加载对应语言包:
import '@valibot/i18n/zh-CN';
import { setGlobalConfig } from 'valibot';
const browserLanguage = navigator.language;
if (browserLanguage.startsWith('zh')) {
setGlobalConfig({ lang: 'zh-CN' });
}
控件自带验证的国际化需要在需要显示异常信息的包装器、组件上自行实现。