TanStack Form Angular 校验完全指南:字段级/表单级、同步/异步与 Schema 验证实战

TanStack Form Angular 校验完全指南:字段级/表单级、同步/异步与 Schema 验证实战 TanStack Form Angular 校验完全指南字段级/表单级、同步/异步与 Schema 验证实战【免费下载链接】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 Form 的 Angular 适配器为对象系统讲解表单校验Validation这一核心能力你可以完全掌控校验时机change / input / blur / submit、在字段级[tanstackField]指令或表单级injectForm()定义规则、选择同步或异步校验如调用后端 API并通过field.api.state.meta.errors与errorMap精准呈现错误。读完本文你将掌握 Angular 中基于 TanStack Form 的完整校验方案包括内置防抖、内置去抖、Standard Schema 库集成以及提交拦截的最佳实践。一、校验是 TanStack Form 的核心设计TanStack Form 把校验validation视为框架的核心能力并围绕高度可定制这一原则设计。从 FieldApi.ts 的FieldValidators类型可以看出每个字段可配置的校验回调包含onMount、onChange、onChangeAsync、onBlur、onBlurAsync、onSubmit、onSubmitAsync、onDynamic、onDynamicAsync等。这些设计在 Angular 适配器中被完整暴露为[tanstackField]指令的validators输入属性见 tanstack-field.ts。核心能力可以概括为三点校验时机可控onChange每次值变化、onBlur失焦、onSubmit提交、onMount挂载等由你决定在哪个生命周期触发校验层级可选规则可以定义在字段级每个[tanstackField]也可以定义在表单级injectForm()同步/异步皆可同步函数直接返回错误信息异步函数如 API 调用返回Promise两者可共存于同一字段。在 Angular 中校验函数通过[validators]...传入[tanstackField]指令指令内部会基于这些配置创建并驱动一个FieldApi实例源码见 app-field.ts因此模板绑定、变更检测与校验状态更新可以无缝衔接。二、何时执行校验由回调函数决定2.1 onChange每次输入都校验把校验函数放在validators.onChange上每次字段值变化每次击键都会执行。校验函数接收{ value, fieldApi }返回错误信息字符串即代表校验失败返回undefined表示通过。Component({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onChange: ageValidator, } #agefield label [for]age.api.nameAge:/label input [id]age.api.name [name]age.api.name [value]age.api.state.value typenumber (input)age.api.handleChange($any($event).target.valueAsNumber) / if (age.api.state.meta.errors) { em rolealert{{ age.api.state.meta.errors.join(, ) }}/em } /ng-container , }) export class AppComponent { ageValidator: FieldValidateFnany, any, any, any, number ({ value }) value 13 ? You must be 13 to make an account : undefined // ... }要点通过#agefield导出指令实例exportAs: field见 tanstack-field.ts模板中用age.api访问FieldApiage.api.handleChange(...)必须由你显式绑定到输入事件上TanStack Form 才能收到值变化并触发校验校验结果读取age.api.state.meta.errors它是一个错误数组ValidationError[]。2.2 onBlur失焦时才校验如果希望校验在字段失焦时才执行把规则放到validators.onBlur并在模板中监听(blur)事件调用age.api.handleBlur()Component({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onBlur: ageValidator, } #agefield label [for]age.api.nameAge:/label !-- We always need to implement onChange, so that TanStack Form receives the changes -- !-- Listen to the onBlur event on the field -- input [id]age.api.name [name]age.api.name [value]age.api.state.value typenumber (blur)age.api.handleBlur() (input)age.api.handleChange($any($event).target.valueAsNumber) / if (age.api.state.meta.errors) { em rolealert{{ age.api.state.meta.errors.join(, ) }}/em } /ng-container , }) export class AppComponent { ageValidator: FieldValidateFnany, any, any, any, number ({ value }) value 13 ? You must be 13 to make an account : undefined // ... }注释中特别强调onChange必须始终实现handleChange必须绑定这样 TanStack Form 才能收到值变化onBlur只是决定校验的执行时机。2.3 同一字段、不同时机、不同规则你可以为同一字段在不同时机配置不同的校验规则例如击键时检查数值是否为非负数失焦时再检查年龄下限Component({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onChange: ageValidator, onBlur: minimumAgeValidator, } #agefield label [for]age.api.nameAge:/label !-- We always need to implement onChange, so that TanStack Form receives the changes -- !-- Listen to the onBlur event on the field -- input [id]age.api.name [name]age.api.name [value]age.api.state.value typenumber (blur)age.api.handleBlur() (input)age.api.handleChange($any($event).target.valueAsNumber) / if (!age.api.state.meta.isValid) { em rolealert{{ age.api.state.meta.errors.join(, ) }}/em } /ng-container , }) export class AppComponent { ageValidator: FieldValidateFnany, any, any, any, number ({ value }) value 13 ? You must be 13 to make an account : undefined minimumAgeValidator: FieldValidateFnany, any, any, any, number ({ value, }) (value 0 ? Invalid value : undefined) // ... }由于field.state.meta.errors是数组同一时刻所有相关错误都会被收集并展示这里还演示了field.state.meta.isValid布尔标志的用法!isValid即存在错误。2.4 errorMap按校验来源精确取错field.state.meta.errorMap按校验来源onChange、onBlur等分别存放错误适合只展示某个特定来源的错误if (age.api.state.meta.errorMap[onChange]) { em rolealert{{ age.api.state.meta.errorMap[onChange] }}/em }在 form-core 内部每个校验来源对应一个errorMapKeygetErrorMapKey(cause)见 FieldApi.ts校验结果会写入meta.errorMap[errorMapKey]见 FieldApi.ts。2.5 errors 数组与 errorMap 的返回类型对齐值得强调的是errors数组和errorMap的值与校验函数返回的类型完全一致——校验函数可以返回任意类型不限于字符串模板中即可直接访问其属性。例如返回一个对象{isOldEnough: false}Component({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onChange: ageValidator } #agefield !-- ... -- !-- errorMap.onChange is type {isOldEnough: false} | undefined -- !-- meta.errors is type Array{isOldEnough: false} | undefined -- if (!age.api.state.meta.errorMap[onChange]?.isOldEnough) { em rolealertThe user is not old enough/em } /ng-container , }) export class AppComponent { ageValidator: FieldValidateFnany, any, any, any, number ({ value }) value 13 ? You must be 13 to make an account : undefined // ... }由于类型被完整保留Angular 模板中的类型检查可以为你捕获访问不存在属性的错误这也是 TanStack Form type-safe 的体现之一。三、字段级校验与表单级校验3.1 表单级校验injectForm validators除了给每个[tanstackField]配置validators外也可以在injectForm()中传入同样的回调onChange、onBlur、onSubmitAsync等来定义表单级校验。表单级校验函数接收整个表单的value。Component({ selector: app-root, standalone: true, imports: [TanStackField], template: div ng-container [tanstackField]form nameage #agefield !-- ... -- if (formErrorMap().onChange) { div em There was an error on the form: {{ formErrorMap().onChange }}/em /div } !-- ... -- /ng-container /div , }) export class AppComponent { form injectForm({ defaultValues: { age: 0, }, onSubmit({ value }) { console.log(value) }, validators: { // Add validators to the form the same way you would add them to a field onChange({ value }) { if (value.age 13) { return Must be 13 or older to sign } return undefined }, }, }) // Subscribe to the forms error map so that updates to it will render formErrorMap injectStore(this.form, (state) state.errorMap) }注意这里的两个关键 API见 inject-form.tsinjectForm()在内部创建FormApi实例并注入 storeinjectStore(this.form, (state) state.errorMap)用于订阅表单的errorMap让模板对它的变更响应式地重新渲染。3.2 从表单校验器设置字段级错误表单级校验的一个典型场景是在提交时通过onSubmitAsync调用单个 API 端点一次性校验所有字段然后把错误回写到具体字段。表单校验器可以返回{ form?, fields? }结构Component({ selector: app-root, imports: [TanStackField], template: form (submit)handleSubmit($event) div ng-container [tanstackField]form nameage #ageFieldfield label [for]ageField.api.nameAge:/label input typenumber [name]ageField.api.name [value]ageField.api.state.value (blur)ageField.api.handleBlur() (input) ageField.api.handleChange($any($event).target.valueAsNumber) / if (ageField.api.state.meta.errors.length 0) { em rolealert{{ ageField.api.state.meta.errors.join(, ) }}/em } /ng-container /div button typesubmitSubmit/button /form , }) export class AppComponent { form injectForm({ defaultValues: { age: 0, socials: [], details: { email: , }, }, validators: { onSubmitAsync: async ({ value }) { // Validate the value on the server const hasErrors await verifyDataOnServer(value) if (hasErrors) { return { form: Invalid data, // The form key is optional fields: { age: Must be 13 or older to sign, // Set errors on nested fields with the fields name socials[0].url: The provided URL does not exist, details.email: An email is required, }, } } return null }, }, }) handleSubmit(event: SubmitEvent) { event.preventDefault() event.stopPropagation() this.form.handleSubmit() } }核心规则form键可选存放表单级错误信息fields键按字段路径支持嵌套如socials[0].url、details.email回写错误字段会立即在各自meta.errors中看到返回null表示校验通过。3.3 字段级错误会覆盖表单级错误需要注意一个覆盖行为如果字段自身配置了校验器那么字段级校验返回的错误会覆盖表单级校验为该字段产生的错误。例如Component({ selector: app-root, standalone: true, imports: [TanStackField], template: div ng-container [tanstackField]form nameage #ageFieldfield [validators]{ onChange: fieldValidator, } input typenumber [value]ageField.api.state.value (input) ageField.api.handleChange($any($event).target.valueAsNumber) / if (ageField.api.state.meta.errors.length 0) { em rolealert{{ ageField.api.state.meta.errors.join(, ) }}/em } /ng-container /div , }) export class AppComponent { form injectForm({ defaultValues: { age: 0, }, validators: { onChange: ({ value }) { return { fields: { age: value.age 12 ? Too young! : undefined, }, } }, }, }) fieldValidator: FieldValidateFnany, any, number ({ value }) value % 2 0 ? Must be odd! : undefined }在上述配置中即使表单级校验返回了Too young!最终界面上也只会显示字段级校验器的Must be odd!——因为字段级错误优先。这一行为在 form-core 的异步校验逻辑中通过determineFieldLevelErrorSourceAndValue实现见 FieldApi.ts当字段级错误存在时会优先采用字段级错误作为最终展示值。四、异步校验与内置防抖4.1 onChangeAsync / onBlurAsync 系列需要网络请求或其他异步操作时使用专门的异步校验器onChangeAsync、onBlurAsync、onSubmitAsync等。异步校验函数签名与同步类似但可以返回Promise其类型为FieldValidateAsyncFn接收{ value, fieldApi, signal }见 FieldApi.tsComponent({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onChangeAsync: ageValidator } #agefield label [for]age.api.nameLast Name:/label input [id]age.api.name [name]age.api.name [value]age.api.state.value typenumber (input)age.api.handleChange($any($event).target.valueAsNumber) / if (age.api.state.meta.errors) { em rolealert{{ age.api.state.meta.errors.join(, ) }}/em } /ng-container , }) export class AppComponent { ageValidator: FieldValidateAsyncFnany, string, number async ({ value, }) { await new Promise((resolve) setTimeout(resolve, 1000)) return value 13 ? You must be 13 to make an account : undefined } // ... }4.2 同步与异步校验共存同步和异步校验可以同时存在例如同一字段同时配置onBlur与onBlurAsyncComponent({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onBlur: ensureAge13, onBlurAsync: ensureOlderAge } #agefield label [for]age.api.nameLast Name:/label input [id]age.api.name [name]age.api.name [value]age.api.state.value typenumber (blur)age.api.handleBlur() (input)age.api.handleChange($any($event).target.value) / if (age.api.state.meta.errors) { em rolealert{{ age.api.state.meta.errors.join(, ) }}/em } /ng-container , }) export class AppComponent { ensureAge13: FieldValidateFnany, any, any, any, number ({ value }) value 13 ? You must be at least 13 : undefined ensureOlderAge: FieldValidateAsyncFnany, string, number async ({ value, }) { const currentAge await fetchCurrentAgeOnProfile() return value currentAge ? You can only increase the age : undefined } // ... }执行顺序的默认约定是同步校验先跑异步校验只在同步校验通过后才运行。如果想改变这一行为把asyncAlways选项设为true异步校验将无视同步校验结果始终执行。在 FieldApi.ts 中可以看到该逻辑的实现同步校验出错hasErrored且未设置asyncAlways时异步校验不会启动。4.3 内置防抖asyncDebounceMs每次击键都发起网络请求会压垮数据库因此 TanStack Form 内置了防抖debounce能力——只需添加一个属性asyncDebounceMsng-container [tanstackField]form nameage asyncDebounceMs{500} [validators]{ onChangeAsync: someValidator } #agefield !-- ... -- /ng-container注意{500}是 Angular 的插值写法等价于[asyncDebounceMs]500在 tanstack-field.ts 中该输入属性通过numberAttribute转换因此也可以直接写asyncDebounceMs500。它会对所有异步校验统一防抖 500ms。也可以针对某个校验单独覆盖防抖时间通过xxxAsyncDebounceMs属性该系列选项在 FieldApi.ts 中定义FormApi同样支持表单级配置见 FormApi.tsng-container [tanstackField]form nameage [validators]{ onChangeAsyncDebounceMs: 1500, onChangeAsync: someValidator, onBlurAsync: otherValidator, } #agefield !-- ... -- /ng-container效果onChangeAsync每 1500ms 执行一次而onBlurAsync使用默认的 500ms。form-core 在异步校验执行时正是通过setTimeout(..., validateObj.debounceMs)实现防抖并配合AbortController取消上一次未完成的请求见 FieldApi.ts确保过期的校验结果不会覆盖新结果。五、通过 Schema 库进行校验函数式校验足够灵活但略显冗长。TanStack Form 原生支持遵循 Standard Schema 规范 的所有库最常用的是ZodValibotArkType注意请使用这些库的最新版本旧版本可能尚未支持 Standard Schema。另请注意校验不会为你提供变换transformed后的值相关处理请参见 提交处理指南。5.1 把 Schema 当作校验器直接传入Schema 可以直接放在validators中用法与自定义函数完全一致import { z } from zod Component({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onChange: z.number().gte(13, You must be 13 to make an account), } #agefield !-- ... -- /ng-container , }) export class AppComponent { form injectForm({ // ... }) z z // ... }将z z暴露为组件属性是 Angular 模板中访问类字段的标准做法Angular 模板不能直接访问导入的顶层变量。5.2 Schema 也支持异步校验表单级与字段级的异步 Schema 校验同样受支持Component({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onChange: z.number().gte(13, You must be 13 to make an account), onChangeAsyncDebounceMs: 500, onChangeAsync: increaseAge, } #agefield !-- ... -- /ng-container , }) export class AppComponent { increaseAge z.number().refine( async (value) { const currentAge await fetchCurrentAgeOnProfile() return value currentAge }, { message: You can only increase the age, }, ) // ... }5.3 在回调函数内手动解析 Schema如果需要对 Standard Schema 校验做更精细的控制可以在校验回调中调用fieldApi.parseValueWithSchema()手动解析对应的异步版本是parseValueWithSchemaAsync()这两个方法只解析不写状态见 FieldApi.tsComponent({ selector: app-root, standalone: true, imports: [TanStackField], template: ng-container [tanstackField]form nameage [validators]{ onChangeAsync: ageValidator } #agefield !-- ... -- /ng-container , }) export class AppComponent { ageValidator: FieldValidateAsyncFnany, string, number async ({ value, fieldApi, }) { const errors fieldApi.parseValueWithSchema( z.number().gte(13, You must be 13 to make an account), ) if (errors) return errors // continue with your validation } // ... }5.4 底层实现Standard Schema 适配器form-core 的 standardSchemaValidator.ts 是 Schema 支持的核心它通过isStandardSchemaValidator()检测对象是否实现了~standard接口即 Standard Schema 规范再由standardSchemaValidators.validate / validateAsync统一执行解析。当校验来源为form时返回的错误会通过transformFormIssues按字段路径支持数组下标如socials[0]重组成{ form, fields }结构——这正是前文从表单校验器设置字段级错误中fields键的来源也解释了为何把整个表单的 Schema 传给表单级validators时错误会自动分发到对应字段。六、阻止无效表单提交onChange、onBlur等校验回调在表单提交时同样会被执行表单无效时提交会被阻断。表单状态对象中的canSubmit标志当任何字段无效且表单已被触碰touched时canSubmit为false在表单被触碰之前即使某些字段技术上按onChange/onBlur规则无效canSubmit仍为true。通过injectStore订阅它即可在无效时禁用提交按钮Component({ selector: app-root, standalone: true, imports: [TanStackField], template: !-- ... -- button typesubmit [disabled]!canSubmit() {{ isSubmitting() ? ... : Submit }} /button , }) export class AppComponent { canSubmit injectStore(this.form, (state) state.canSubmit) isSubmitting injectStore(this.form, (state) state.isSubmitting) // ... }无障碍提示实践中disabled按钮对屏幕阅读器不可访问更推荐使用aria-disabled表达禁用语义。如果希望在用户交互之前就完全禁止提交可以把canSubmit与isPristine未触碰标志组合使用!canSubmit || isPristine这一条件可以在用户做出任何修改之前有效禁用提交。七、结语本文围绕 TanStack Form Angular 的校验体系从何时校验onChange/onBlur回调、在哪校验字段级[tanstackField]与表单级injectForm()、如何异步onChangeAsync系列 内置防抖 asyncAlways、如何用 SchemaZod / Valibot / ArkType 等 Standard Schema 库到如何拦截无效提交canSubmit/isPristine进行了完整梳理。这些能力背后有清晰的源码支撑Angular 适配器通过 tanstack-field.ts 暴露validators、asyncDebounceMs、asyncAlways等输入通过 inject-form.ts 创建FormApi并注入响应式 store而校验的时机分派、同步优先/异步兜底、防抖与请求取消等底层逻辑统一收敛在 FieldApi.ts 与 ValidationLogic.ts 中defaultValidationLogic定义了各事件触发的校验器组合revalidateLogic则提供了类似 React Hook Form 的提交后按需重验策略。若需进一步深入可继续阅读同目录下的 动态校验 与 提交处理 两篇指南它们分别覆盖基于字段值动态切换校验规则、以及提交与 Server Action 集成的完整流程。【免费下载链接】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),仅供参考