鸿蒙动态表单实战:用 @hmkit/validator 处理条件必填与错误联动

表单刚开始通常只有几项规则:姓名必填,手机号检查格式,点击提交时统一校验。

后来需求变成了这样:

  • 选择个人身份,公司名称可以不填。
  • 切换到企业身份,公司名称变成必填。
  • 企业身份下已经出现的必填提示,切回个人后要及时更新。
  • 手机号希望等用户输入完成、失去焦点后再提示。

这时,页面需要处理字段之间的关系,以及错误状态在交互中的变化。

我一直在维护一个鸿蒙 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。对一个校验组件来说,能在实际页面中少写一些重复逻辑、把错误及时展示到正确位置,就是很有价值的反馈。

相关推荐
行者-全栈开发2 小时前
【共创稿事节】HarmonyOS 三方库工程化集成:从 ohpm 引入到三方库鸿蒙化的完整闭环
harmonyos·鸿蒙·har·三方库·ohpm·框架适配
网络豆3 小时前
开源鸿蒙平台 CMP 三方库「tanstack-table-kmp」适配全流程
华为·开源·harmonyos
贾伟康4 小时前
【共创稿事节】食材百科页的懒加载改造:LazyForEach、IDataSource 与缓存窗口的三次迭代
性能优化·harmonyos·arkts·lazyforeach·列表加载
SZYSK4 小时前
一套人员主数据,三套事务:通行、消费、人证比对如何分事件类型
物联网·算法·harmonyos
贾伟康5 小时前
【共创稿事节】一个 UserDataManager 管八类数据:Preferences 持久化的克制用法
java·服务器·前端·harmonyos·arkts·鸿蒙
万物智能信息科技6 小时前
六轴传感器Motion驱动框架开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
驱动开发·华为·开源·harmonyos·鸿蒙
熊猫钓鱼>_>6 小时前
端侧 NPU 轻量大模型推理接入笔记【鸿蒙心迹】
开发语言·笔记·华为·ai编程·harmonyos·鸿蒙
2601_949847756 小时前
开源鸿蒙平台 CMP 三方库「compose-remember-setting」适配全流程
华为·开源·harmonyos
resh_people7 小时前
开源鸿蒙平台 CMP 三方库「snowfall-compose」适配全流程
华为·开源·harmonyos