TanStack Form Angular 校验完全指南:字段级/表单级、同步/异步与 Schema 验证实战
2026/9/17 10:41:31 网站建设 项目流程

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.errorserrorMap精准呈现错误。读完本文,你将掌握 Angular 中基于 TanStack Form 的完整校验方案,包括内置防抖、内置去抖、Standard Schema 库集成以及提交拦截的最佳实践。


一、校验是 TanStack Form 的核心设计

TanStack Form 把校验(validation)视为框架的核心能力,并围绕"高度可定制"这一原则设计。从 FieldApi.ts 的FieldValidators类型可以看出,每个字段可配置的校验回调包含:onMountonChangeonChangeAsynconBluronBlurAsynconSubmitonSubmitAsynconDynamiconDynamicAsync等。这些设计在 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" name="age" [validators]="{ onChange: ageValidator, }" #age="field" > <label [for]="age.api.name">Age:</label> <input [id]="age.api.name" [name]="age.api.name" [value]="age.api.state.value" type="number" (input)="age.api.handleChange($any($event).target.valueAsNumber)" /> @if (age.api.state.meta.errors) { <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em> } </ng-container> `, }) export class AppComponent { ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) => value < 13 ? 'You must be 13 to make an account' : undefined // ... }

要点:

  • 通过#age="field"导出指令实例(exportAs: 'field',见 tanstack-field.ts),模板中用age.api访问FieldApi
  • age.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" name="age" [validators]="{ onBlur: ageValidator, }" #age="field" > <label [for]="age.api.name">Age:</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" type="number" (blur)="age.api.handleBlur()" (input)="age.api.handleChange($any($event).target.valueAsNumber)" /> @if (age.api.state.meta.errors) { <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em> } </ng-container> `, }) export class AppComponent { ageValidator: FieldValidateFn<any, 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" name="age" [validators]="{ onChange: ageValidator, onBlur: minimumAgeValidator, }" #age="field" > <label [for]="age.api.name">Age:</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" type="number" (blur)="age.api.handleBlur()" (input)="age.api.handleChange($any($event).target.valueAsNumber)" /> @if (!age.api.state.meta.isValid) { <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em> } </ng-container> `, }) export class AppComponent { ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) => value < 13 ? 'You must be 13 to make an account' : undefined minimumAgeValidator: FieldValidateFn<any, 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按"校验来源"(onChangeonBlur等)分别存放错误,适合只展示某个特定来源的错误:

@if (age.api.state.meta.errorMap['onChange']) { <em role="alert">{{ 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" name="age" [validators]="{ onChange: ageValidator }" #age="field" > <!-- ... --> <!-- errorMap.onChange is type `{isOldEnough: false} | undefined` --> <!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` --> @if (!age.api.state.meta.errorMap['onChange']?.isOldEnough) { <em role="alert">The user is not old enough</em> } </ng-container> `, }) export class AppComponent { ageValidator: FieldValidateFn<any, 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()中传入同样的回调(onChangeonBluronSubmitAsync等)来定义表单级校验。表单级校验函数接收整个表单的value

@Component({ selector: 'app-root', standalone: true, imports: [TanStackField], template: ` <div> <ng-container [tanstackField]="form" name="age" #age="field"> <!-- ... --> @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 form's error map so that updates to it will render formErrorMap = injectStore(this.form, (state) => state.errorMap) }

注意这里的两个关键 API(见 inject-form.ts):

  • injectForm()在内部创建FormApi实例并注入 store;
  • injectStore(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" name="age" #ageField="field"> <label [for]="ageField.api.name">Age:</label> <input type="number" [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 role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em> } </ng-container> </div> <button type="submit">Submit</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 field's 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" name="age" #ageField="field" [validators]="{ onChange: fieldValidator, }" > <input type="number" [value]="ageField.api.state.value" (input)=" ageField.api.handleChange($any($event).target.valueAsNumber) " /> @if (ageField.api.state.meta.errors.length > 0) { <em role="alert">{{ 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: FieldValidateFn<any, any, number> = ({ value }) => value % 2 === 0 ? 'Must be odd!' : undefined }

在上述配置中,即使表单级校验返回了'Too young!',最终界面上也只会显示字段级校验器的'Must be odd!'——因为字段级错误优先。这一行为在 form-core 的异步校验逻辑中通过determineFieldLevelErrorSourceAndValue实现(见 FieldApi.ts),当字段级错误存在时会优先采用字段级错误作为最终展示值。


四、异步校验与内置防抖

4.1 onChangeAsync / onBlurAsync 系列

需要网络请求或其他异步操作时,使用专门的异步校验器:onChangeAsynconBlurAsynconSubmitAsync等。异步校验函数签名与同步类似,但可以返回Promise(其类型为FieldValidateAsyncFn,接收{ value, fieldApi, signal },见 FieldApi.ts):

@Component({ selector: 'app-root', standalone: true, imports: [TanStackField], template: ` <ng-container [tanstackField]="form" name="age" [validators]="{ onChangeAsync: ageValidator }" #age="field" > <label [for]="age.api.name">Last Name:</label> <input [id]="age.api.name" [name]="age.api.name" [value]="age.api.state.value" type="number" (input)="age.api.handleChange($any($event).target.valueAsNumber)" /> @if (age.api.state.meta.errors) { <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em> } </ng-container> `, }) export class AppComponent { ageValidator: FieldValidateAsyncFn<any, 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 同步与异步校验共存

同步和异步校验可以同时存在,例如同一字段同时配置onBluronBlurAsync

@Component({ selector: 'app-root', standalone: true, imports: [TanStackField], template: ` <ng-container [tanstackField]="form" name="age" [validators]="{ onBlur: ensureAge13, onBlurAsync: ensureOlderAge }" #age="field" > <label [for]="age.api.name">Last Name:</label> <input [id]="age.api.name" [name]="age.api.name" [value]="age.api.state.value" type="number" (blur)="age.api.handleBlur()" (input)="age.api.handleChange($any($event).target.value)" /> @if (age.api.state.meta.errors) { <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em> } </ng-container> `, }) export class AppComponent { ensureAge13: FieldValidateFn<any, any, any, any, number> = ({ value }) => value < 13 ? 'You must be at least 13' : undefined ensureOlderAge: FieldValidateAsyncFn<any, 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)能力——只需添加一个属性asyncDebounceMs

<ng-container [tanstackField]="form" name="age" asyncDebounceMs="{500}" [validators]="{ onChangeAsync: someValidator }" #age="field" > <!-- ... --> </ng-container>

注意{500}是 Angular 的插值写法(等价于[asyncDebounceMs]="500"),在 tanstack-field.ts 中该输入属性通过numberAttribute转换,因此也可以直接写asyncDebounceMs="500"。它会对所有异步校验统一防抖 500ms。

也可以针对某个校验单独覆盖防抖时间,通过xxxAsyncDebounceMs属性(该系列选项在 FieldApi.ts 中定义,FormApi同样支持表单级配置,见 FormApi.ts):

<ng-container [tanstackField]="form" name="age" [validators]="{ onChangeAsyncDebounceMs: 1500, onChangeAsync: someValidator, onBlurAsync: otherValidator, }" #age="field" > <!-- ... --> </ng-container>

效果:onChangeAsync每 1500ms 执行一次,而onBlurAsync使用默认的 500ms。form-core 在异步校验执行时正是通过setTimeout(..., validateObj.debounceMs)实现防抖,并配合AbortController取消上一次未完成的请求(见 FieldApi.ts),确保过期的校验结果不会覆盖新结果。


五、通过 Schema 库进行校验

函数式校验足够灵活,但略显冗长。TanStack Form 原生支持遵循 Standard Schema 规范 的所有库,最常用的是:

  • Zod
  • Valibot
  • ArkType

注意:请使用这些库的最新版本,旧版本可能尚未支持 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" name="age" [validators]="{ onChange: z.number().gte(13, 'You must be 13 to make an account'), }" #age="field" > <!-- ... --> </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" name="age" [validators]="{ onChange: z.number().gte(13, 'You must be 13 to make an account'), onChangeAsyncDebounceMs: 500, onChangeAsync: increaseAge, }" #age="field" > <!-- ... --> </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.ts):

@Component({ selector: 'app-root', standalone: true, imports: [TanStackField], template: ` <ng-container [tanstackField]="form" name="age" [validators]="{ onChangeAsync: ageValidator }" #age="field" > <!-- ... --> </ng-container> `, }) export class AppComponent { ageValidator: FieldValidateAsyncFn<any, 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时,错误会自动分发到对应字段。


六、阻止无效表单提交

onChangeonBlur等校验回调在表单提交时同样会被执行,表单无效时提交会被阻断

表单状态对象中的canSubmit标志:当任何字段无效且表单已被触碰(touched)时,canSubmitfalse(在表单被触碰之前,即使某些字段"技术上"按onChange/onBlur规则无效,canSubmit仍为true)。

通过injectStore订阅它,即可在无效时禁用提交按钮:

@Component({ selector: 'app-root', standalone: true, imports: [TanStackField], template: ` <!-- ... --> <button type="submit" [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表达禁用语义。

如果希望在用户交互之前就完全禁止提交,可以把canSubmitisPristine(未触碰标志)组合使用:!canSubmit || isPristine这一条件可以在用户做出任何修改之前有效禁用提交。


七、结语

本文围绕 TanStack Form Angular 的校验体系,从"何时校验"(onChange/onBlur回调)、"在哪校验"(字段级[tanstackField]与表单级injectForm())、"如何异步"(onChangeAsync系列 + 内置防抖 +asyncAlways)、"如何用 Schema"(Zod / Valibot / ArkType 等 Standard Schema 库)到"如何拦截无效提交"(canSubmit/isPristine)进行了完整梳理。

这些能力背后有清晰的源码支撑:Angular 适配器通过 tanstack-field.ts 暴露validatorsasyncDebounceMsasyncAlways等输入,通过 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询