表单刚开始通常只有几项规则:姓名必填,手机号检查格式,点击提交时统一校验。
后来需求变成了这样:
- 选择个人身份,公司名称可以不填。
- 切换到企业身份,公司名称变成必填。
- 企业身份下已经出现的必填提示,切回个人后要及时更新。
- 手机号希望等用户输入完成、失去焦点后再提示。
这时,页面需要处理字段之间的关系,以及错误状态在交互中的变化。
我一直在维护一个鸿蒙 ArkTS 开源校验库 @hmkit/validator 。这篇用"个人/企业身份切换"演示它的条件必填和表单联动能力,代码以 1.4.0 为例。
这些能力在此前版本已经提供,下面重点讲实际接入方式。
1. 先把业务规则写清楚
本文的表单只有三个字段:
| 字段 | 规则 | 交互要求 |
|---|---|---|
accountType |
只能为 personal 或 company |
身份切换后更新关联错误 |
companyName |
企业身份必填;填写后至少两个字符 | 跟随身份重新校验 |
phone |
必填且符合手机号格式 | 失焦时校验,提交时再次检查 |
先安装依赖:
bash
ohpm install @hmkit/validator@1.4.0
然后创建表单控制器:
typescript
import { v, FormTrigger, FormValidator } from '@hmkit/validator';
function createProfileValidator(): FormValidator {
return new FormValidator({
'accountType': v.enumOf<string>(['personal', 'company'])
.required('请选择身份'),
'companyName': v.string()
.requiredWhen(
'accountType',
(type: Object | null | undefined): boolean => type === 'company',
'企业身份需要填写公司名称'
)
.min(2, '公司名称至少两个字符'),
'phone': v.string()
.required('请输入手机号')
.phone('手机号格式不正确')
}, {
'triggers': [FormTrigger.CHANGE, FormTrigger.BLUR],
'fields': {
'companyName': { 'dependencies': ['accountType'] },
'phone': { 'triggers': [FormTrigger.BLUR] }
}
});
}
这里把校验规则和交互策略集中到了一处。页面负责传递当前输入,控制器负责执行规则和记录错误。
使用创建函数,也方便每个页面获得自己的控制器和规则实例。
2. 条件必填和字段依赖分别解决什么问题
这两个配置需要配合起来理解:
typescript
// 条件:企业身份下,公司名称为空时返回必填错误。
v.string().requiredWhen(
'accountType',
(type: Object | null | undefined): boolean => type === 'company',
'企业身份需要填写公司名称'
);
requiredWhen() 在校验时读取同级字段,判断当前是否必须填写。
而 companyName 的配置 dependencies: ['accountType'] 表示:公司名称依赖身份字段。身份发生 change 事件后,重新校验公司名称。
所以依赖声明写在 companyName 上。
条件规则定义"什么情况下报错",字段依赖定义"什么变化会触发重新检查"。如果只在提交时统一校验,条件规则已经可以发挥作用;要让切换过程中的错误同步更新,还需要把事件和依赖接起来。
3. 跑一遍身份切换,观察错误如何变化
下面的代码接在创建函数之后,可以先独立验证表单逻辑:
typescript
async function demoIdentitySwitch(): Promise<void> {
const validator = createProfileValidator();
try {
const personal: Record<string, Object> = {
'accountType': 'personal',
'companyName': '',
'phone': '13800138000'
};
console.info(JSON.stringify(validator.validateAll(personal)));
// {}
const company: Record<string, Object> = {
'accountType': 'company',
'companyName': '',
'phone': '13800138000'
};
await validator.onChange('accountType', 'company', company);
console.info(JSON.stringify(validator.getState().errors));
// { "companyName": "企业身份需要填写公司名称" }
await validator.onChange('accountType', 'personal', personal);
console.info(JSON.stringify(validator.getState().errors));
// {}
} finally {
validator.dispose();
}
}
这里没有在切换时手动删除 companyName 的错误。身份变化触发了关联字段重校验,公司名称仍为空,但个人身份下不再必填,于是控制器更新了错误状态。
每次事件都传入一份完整的当前值快照。条件规则需要读取身份,关联校验需要取得公司名称,这些信息都来自第三个参数。
如果只传字段名和新值,控制器就缺少这次联动所需的整组数据。
4. 接到页面时,读取整表错误快照
身份变化可能同时影响多个字段。下面这个事件处理函数返回所有当前错误,页面可以用它更新显示状态:
typescript
async function handleAccountTypeChange(
validator: FormValidator,
currentValues: Record<string, Object>
): Promise<Record<string, string>> {
await validator.onChange(
'accountType',
currentValues['accountType'],
currentValues
);
return validator.getState().errors;
}
在 ArkUI 组件中,先更新身份值,再把包含新身份的完整数据传给这个函数,最后把返回结果赋给组件负责显示错误的 @State 属性。
onChange() 的返回值只代表当前源字段的错误;依赖字段的变化需要从 getState().errors 读取。
getState() 返回的是状态快照。控制器内部错误发生变化后,页面仍需要主动读取并更新自己的响应式状态,界面才会展示最新结果。
5. 手机号可以失焦校验,提交时统一检查
前面的配置让手机号只在 blur 时触发直接字段校验。页面中可以分别接入:
- 手机号输入事件:更新值,并调用
onChange('phone', value, values),记录输入变化、使旧任务失效。 - 手机号失焦事件:调用
onBlur('phone', value, values),执行校验,再读取错误快照。 - 点击提交:调用
submit(values, handler),对整张表进行异步校验。
提交逻辑可以这样写:
typescript
async function demoSubmit(): Promise<void> {
const validator = createProfileValidator();
try {
const values: Record<string, Object> = {
'accountType': 'company',
'companyName': '示例科技',
'phone': '13800138000'
};
const errors = await validator.submit(
values,
async (submitted: Record<string, Object>): Promise<void> => {
console.info(JSON.stringify(submitted));
// 在这里调用项目自己的提交接口。
}
);
console.info(JSON.stringify(errors));
// {}
} finally {
validator.dispose();
}
}
即使手机号配置为失焦触发,submit() 仍会检查它。校验通过后才会执行回调;回调中的接口异常会继续交给调用方处理。
本文只有同步字段规则,仍然使用表单控制器的异步事件与提交入口,便于后续接入远程校验。
示例回调拿到的是提交时传入的值。如果 Schema 中还有类型转换或默认值处理,需要另外通过 parse()/parseAsync() 取得处理后的数据,再交给业务接口。
6. 几个容易混淆的边界
非必填,填写后仍然需要合法
requiredWhen() 控制空值是否报必填错误。个人身份下,如果公司名称保留了一个字符,例如 'A',仍会受到 .min(2) 的约束。
可以用这张表检查需求是否一致:
| 身份 | 公司名称 | 本文规则的结果 |
|---|---|---|
| 个人 | 空字符串 | 通过 |
| 企业 | 空字符串 | 返回条件必填错误 |
| 个人或企业 | 'A' |
返回长度错误 |
| 个人或企业 | '示例科技' |
通过 |
如果业务要求切回个人后,公司字段完全不参与校验,需要进一步设计分支 Schema 或调整提交数据,不能仅靠隐藏输入框实现。
字段隐藏、数据保留和错误展示由页面决定
校验库不会自动隐藏组件,也不会自动清空公司名称。页面需要明确:切换身份时是否保留已填内容,隐藏字段是否提交,以及什么时候展示错误。
源字段变化可以触发尚未操作过的依赖字段校验。如果产品希望用户操作后才显示提示,可以结合 getFieldState(name) 的 touched、dirty 和提交状态决定展示时机。
依赖重新校验当前声明的直接关联字段
本文的关系是 accountType → companyName。如果业务还存在更长的关联链,应显式定义需要重校验的直接关系,不能把它当成自动遍历任意依赖图的引擎。
页面退出时调用 dispose() 清理控制器任务;需要重置并继续使用时调用 reset()。表单输入值本身仍由页面维护和重置。
7. 从一个有联动需求的表单开始接入
@hmkit/validator 既可以单独校验一个值,也可以组合对象、数组和条件规则。除了本文场景,还提供手机号、身份证、银行卡等本地化规则,以及异步校验、结构化错误、类型转换等能力。
如果项目已经有现成页面,可以先接入一个条件字段,确认"规则、触发、错误展示、提交"这几步都符合需求,再逐步扩展。
项目使用 MIT 开源协议,安装入口、源码和示例都在这里:
我会继续维护这个库。欢迎试用,也欢迎带着具体表单场景、输入数据和预期结果提 Issue。对一个校验组件来说,能在实际页面中少写一些重复逻辑、把错误及时展示到正确位置,就是很有价值的反馈。