
Angular 表单动态校验指南深入理解 tanstack/angular-form 的 onDynamic 与 revalidateLogic【免费下载链接】form Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form导读在实际业务中表单校验规则很少是固定不变的——校验规则往往需要根据表单状态如是否首次提交动态切换。tanstack/angular-form提供的onDynamic校验函数与revalidateLogic()校验策略正是为了解决根据表单状态动态调整校验规则这一经典痛点而设计。本文将以 docs/framework/angular/guides/dynamic-validation.md 为核心脉络结合 ValidationLogic.ts 与 DynamicValidation.spec.ts 等源码与测试带你完整掌握onDynamic的触发机制、revalidateLogic的参数语义、错误读取方式以及它与字段级校验、异步校验、Zod/Valibot 标准 Schema 校验的组合用法。一、为什么需要动态校验默认情况下TanStack Form 的校验规则是静态挂载的onChange、onBlur、onSubmit在表单生命周期内的触发时机是固定的。但在大量真实场景中我们希望同一个字段在不同阶段应用不同的校验强度最典型的例子就是用户首次提交之前尽量少打扰只在提交时校验用户首次提交之后随着输入实时给出反馈每次变更都校验。这种根据表单是否已提交而动态切换校验模式的能力正是onDynamic校验函数存在的意义。正如 ValidationLogic.ts 源码注释所描述的revalidateLogic的定位是像 React Hook Form 一样驱动校验它只运行onDynamic校验器配合默认校验器并根据表单自身状态切换校验类型。需要特别强调的是默认情况下onDynamic根本不会被调用。必须通过validationLogic: revalidateLogic()选项显式启用这一点在 ValidationLogic.ts 与本文档中均有明确说明。二、第一个 onDynamic 示例首次提交才校验必填先看最基础的用法。下面的组件通过injectForm创建表单并配置了validationLogic: revalidateLogic()与onDynamic校验器实现提交前不校验、提交后每次变更都校验的必填逻辑import { Component } from angular/core import { TanStackField, injectForm, revalidateLogic, } from tanstack/angular-form Component({ selector: app-root, standalone: true, imports: [TanStackField], template: !-- Your form template here -- , }) export class AppComponent { form injectForm({ defaultValues: { firstName: , lastName: , }, // 如果省略该项onDynamic 将不会被调用 validationLogic: revalidateLogic(), validators: { onDynamic: ({ value }) { if (!value.firstName) { return { firstName: A first name is required } } return undefined }, }, }) }从源码角度拆解这个示例的执行路径injectForm只是 inject-form.ts 中new FormApi(opts)的轻量封装并额外通过injectStore订阅了isSubmitting状态用于驱动 Angular 变更检测。校验发生时FormApi.ts 会调用getSyncValidatorArray并把this.options.validationLogic || defaultValidationLogic传入注意formOptions默认校验策略是defaultValidationLogic只有显式传入revalidateLogic时onDynamic才会被纳入校验。revalidateLogic内部ValidationLogic.ts会读取submissionAttempts提交尝试次数来决定当前采用mode还是modeAfterSubmission并将onDynamic/onDynamicAsync以cause: dynamic的身份追加到校验器列表中。对应测试DynamicValidation.spec.ts 中 rhf validation should work as-expected 用例完整验证了这条行为链提交前setValue(Jo)不产生onDynamic错误 → 首次handleSubmit后出现校验错误 → 修正值后onDynamic错误自动清除。三、revalidateLogic 的重校验选项mode 与 modeAfterSubmissionrevalidateLogic允许你精确指定何时执行校验并根据表单当前提交状态动态切换校验规则。它接收两个参数参数可选值默认值语义modechange/blur/submitsubmit首次提交前的校验模式。change每次变更校验blur失焦校验submit仅在提交时校验modeAfterSubmissionchange/blur/submitchange首次提交之后的校验模式。默认在每次变更时重新校验例如希望首次提交前只在提交时校验首次提交后改为失焦校验可以这样配置Component({ selector: app-root, standalone: true, imports: [TanStackField], template: !-- Your form template here -- , }) export class AppComponent { form injectForm({ // ... validationLogic: revalidateLogic({ mode: submit, modeAfterSubmission: blur, }), // ... }) }底层判定逻辑ValidationLogic.ts 中的实现如下读取submissionAttempts当校验对象是FormGroupApi时读取该 group 自身的state.meta.submissionAttempts否则读取父表单的state.submissionAttempts这使得 group 的onDynamic会在 group 自身提交后才切换到modeAfterSubmissionmodeToWatch submissionAttempts 0 ? mode : modeAfterSubmission仅当event.type命中[modeToWatch, submit]之一时dynamicValidator才会被加入校验队列——这意味着submit事件永远会触发onDynamic而change/blur事件只有在匹配当前模式时才会触发。因此默认配置mode: submit、modeAfterSubmission: change下的完整行为是提交前change/blur不触发onDynamic提交时触发提交后每次change都会触发。这正是 React Hook Form 风格的modereValidateMode语义。四、读取 onDynamic 错误formErrorMap 与 injectStore读取onDynamic的错误与其他校验类型onChange、onBlur完全一致——通过表单的错误映射errorMap获取。在 Angular 中用injectStore订阅即可获得响应式的错误状态import { Component } from angular/core import { TanStackField, injectForm, injectStore, revalidateLogic, } from tanstack/angular-form Component({ selector: app-root, standalone: true, imports: [TanStackField], template: p{{ formErrorMap().onDynamic?.firstName }}/p , }) export class AppComponent { form injectForm({ // ... validationLogic: revalidateLogic(), validators: { onDynamic: ({ value }) { if (!value.firstName) { return { firstName: A first name is required } } return undefined }, }, }) formErrorMap injectStore(this.form, (state) state.errorMap) }injectStore(this.form, (state) state.errorMap)订阅表单状态中的errorMap当onDynamic校验产生/清除错误时formErrorMap().onDynamic会随之更新表单级onDynamic校验器返回的是按字段名分组的错误对象如{ firstName: ... }因此模板中通过onDynamic?.firstName取对应字段的错误错误被写入errorMap的哪个键由 FormApi.ts 的getErrorMapKey决定cause dynamic映射到onDynamic键。错误来源的字段级写入在 FormApi.ts 的validateSync中表单级校验返回的fieldErrors会被逐字段分发到fieldMeta.errorMap[errorMapKey]同时通过errorSourceMap标记错误来源为form。这也是字段实例的state.meta.errorMap.onDynamic能被同步填充的原因——测试 DynamicValidation.spec.ts 中大量断言正是读取field.state.meta.errorMap.onDynamic。五、与其他校验逻辑onChange / onBlur组合使用onDynamic并不是孤立的它可以与onChange、onBlur等其他校验逻辑共存。下面的示例同时启用了onChange必填 firstName与onDynamic必填 lastName并按各自来源分别渲染错误import { Component } from angular/core import { TanStackField, injectForm, injectStore, revalidateLogic, } from tanstack/angular-form Component({ selector: app-root, standalone: true, imports: [TanStackField], template: div p{{ formErrorMap().onChange?.firstName }}/p p{{ formErrorMap().onDynamic?.lastName }}/p /div , }) export class AppComponent { form injectForm({ defaultValues: { firstName: , lastName: , }, validationLogic: revalidateLogic(), validators: { onChange: ({ value }) { if (!value.firstName) { return { firstName: A first name is required } } return undefined }, onDynamic: ({ value }) { if (!value.lastName) { return { lastName: A last name is required } } return undefined }, }, }) formErrorMap injectStore(this.form, (state) state.errorMap) }这正是 ValidationLogic.ts 中revalidateLogic的实现细节它通过defaultValidationLogic计算出默认校验器集合再把dynamicValidator追加在默认校验器之后[...defaultValidators, ...validatorsToAdd]。也就是说revalidateLogic并不会替代默认校验逻辑而是在其之上叠加onDynamic。测试 DynamicValidation.spec.ts 的 rhf validation should handle default validators as well 用例证明了这一点配置了revalidateLogic后onChange校验依然照常工作。与字段Fields一起使用onDynamic同样可以用于字段级校验方式与onChange等其他校验逻辑完全一致。通过TanStackField指令的[validators]传入并在age.api.state.meta.errorMap.onDynamic中读取import { Component } from angular/core import { TanStackField, injectForm, revalidateLogic, } from tanstack/angular-form import type { FieldValidateFn } from tanstack/angular-form Component({ selector: app-root, standalone: true, imports: [TanStackField], template: form (submit)handleSubmit($event) ng-container [tanstackField]form nameage [validators]{ onDynamic: ageValidator, } #agefield input typenumber [value]age.api.state.value (blur)age.api.handleBlur() (input)age.api.handleChange($any($event).target.valueAsNumber) / if (age.api.state.meta.errorMap.onDynamic) { p stylecolor: red {{ age.api.state.meta.errorMap.onDynamic }} /p } /ng-container button typesubmitSubmit/button /form , }) export class AppComponent { ageValidator: FieldValidateFnany, any, any, any, number ({ value }) value 18 ? undefined : Age must be greater than 18 form injectForm({ defaultValues: { name: , age: 0, }, validationLogic: revalidateLogic(), onSubmit({ value }) { alert(JSON.stringify(value)) }, }) handleSubmit(event: SubmitEvent) { event.preventDefault() event.stopPropagation() this.form.handleSubmit() } }要点说明字段级onDynamic返回的是字符串错误而非字段分组对象直接渲染在errorMap.onDynamic中字段级的onDynamic选项定义在 FieldApi.ts类型为RejectPromiseValidatorTOnDynamic与onChange/onBlur并列handleSubmit中先preventDefault()再调用this.form.handleSubmit()避免原生表单提交打断 TanStack Form 的提交与校验流程。六、异步动态校验与防抖onDynamicAsync异步校验同样可以与onDynamic配合使用并且支持通过onDynamicAsyncDebounceMs做防抖避免高频输入触发过多请求如用户名查重import { Component } from angular/core import { TanStackField, injectForm, revalidateLogic, } from tanstack/angular-form Component({ selector: app-root, standalone: true, imports: [TanStackField], template: !-- Your form template here -- , }) export class AppComponent { form injectForm({ defaultValues: { username: , }, validationLogic: revalidateLogic(), validators: { onDynamicAsyncDebounceMs: 500, // 将异步校验防抖 500ms onDynamicAsync: async ({ value }) { if (!value.username) { return { username: Username is required } } // 模拟一个异步校验例如向服务端查询用户名是否已被占用 const isValid await validateUsername(value.username) return isValid ? undefined : { username: Username is already taken } }, }, }) }底层防抖实现异步校验器数组的生成在 utils.ts 的getAsyncValidatorArray中默认防抖值为asyncDebounceMs ?? 0即不防抖校验器的cause为dynamic时优先使用onDynamicAsyncDebounceMs否则回退到默认值submit事件下的防抖会被强制置为 0utils.ts保证提交时异步校验立即执行不因防抖而挂起。revalidateLogic在异步事件下会选择onDynamicAsync而非onDynamicValidationLogic.ts因此异步与同步动态校验可以同时配置、按需触发。对应异步场景的测试见 DynamicValidation.spec.ts。七、与标准 Schema 校验库Zod / Valibot结合onDynamic还支持直接传入符合 Standard Schema 规范的校验库如 Zod、Valibot的 schema 对象从而把随表单状态动态切换的规则定义在声明式的 schema 中import { Component } from angular/core import { TanStackField, injectForm, revalidateLogic, } from tanstack/angular-form import { z } from zod Component({ selector: app-root, standalone: true, imports: [TanStackField], template: !-- Your form template here -- , }) export class AppComponent { schema z.object({ firstName: z.string().min(1, A first name is required), lastName: z.string().min(1, A last name is required), }) form injectForm({ defaultValues: { firstName: , lastName: , }, validationLogic: revalidateLogic(), validators: { onDynamic: this.schema, }, }) }这里的关键机制onDynamic的合法取值既可以是校验函数也可以是 Standard Schema 对象——FormValidators类型允许TOnDynamic为FormValidateOrFnTFormData | TStandardSchemaValidatorIssue这类联合形态见 types.tsschema 在运行时由 standardSchemaValidator.ts 统一适配解析因此 Zod、Valibot 等符合 Standard Schema 标准的库都能直接使用组合价值在于你可以为未提交与已提交两个阶段分别准备不同严格程度的 schema通过revalidateLogic的模式切换实现渐进式校验体验。测试 DynamicValidation.spec.ts 正是用z.object({ name: z.string().min(3, ...) })作为onDynamic校验器验证了 schema 形态下提交前后行为完全符合预期。八、补充FormGroup 与默认策略边界最后补充两个容易踩坑的边界场景FormGroup 的提交状态隔离当校验对象是FormGroupApi时revalidateLogic会读取 group 自身的state.meta.submissionAttempts来决定是否切换为modeAfterSubmissionValidationLogic.ts。这意味着一个 group 的onDynamic只会在该 group 自身提交后才切换到提交后模式父表单的提交不会意外改变子 group 的校验行为。未配置validationLogic时默认的defaultValidationLogicValidationLogic.ts在mount/submit/blur/change事件下只装配onMount/onChange/onBlur/onSubmit/onServer等校验器从不装配onDynamic。所以如果你的onDynamic校验莫名不生效请首先检查validationLogic: revalidateLogic()是否已配置。总结onDynamicrevalidateLogic为tanstack/angular-form提供了随表单状态动态切换校验规则的能力其行为链路清晰且可测试验证启用前提必须配置validationLogic: revalidateLogic()否则onDynamic不会被调用模式切换mode默认submit控制首次提交前的触发时机modeAfterSubmission默认change控制提交后的触发时机错误读取表单级错误通过injectStore(form, s s.errorMap)读取onDynamic?.fieldName字段级错误读取age.api.state.meta.errorMap.onDynamic组合能力可与onChange/onBlur共存支持字段级校验、onDynamicAsync异步校验含onDynamicAsyncDebounceMs防抖、以及 Zod / Valibot 等 Standard Schema 声明式校验。如需进一步查看完整实现与测试证据可深入阅读 packages/form-core/src/ValidationLogic.ts、packages/form-core/src/utils.ts、packages/form-core/src/FormApi.ts 与 packages/form-core/tests/DynamicValidation.spec.ts。【免费下载链接】form Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考