TanStack Form 功能的核心是验证的概念。TanStack Form 使验证高度可定制
这取决于您![tanstackField] 指令接受一些回调作为 props,例如 onChange 或 onBlur。这些回调会传递字段的当前值以及 fieldAPI 对象,以便您可以执行验证。如果您发现验证错误,只需返回错误消息字符串,它将可在 field.api.state.meta.errors 中找到。
这是一个例子
@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
// ...
}
@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
// ...
}
在上面的例子中,验证是在每次击键时完成的 (onChange)。相反,如果我们希望在字段失去焦点时完成验证,我们将像这样更改上面的代码
@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
// ...
}
@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
// ...
}
因此,您可以通过实现所需的回调来控制何时完成验证。您甚至可以在不同的时间执行不同的验证
@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.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
minimumAgeValidator: FieldValidateFn<any, any, any, any, number> = ({
value,
}) => (value < 0 ? 'Invalid value' : undefined)
// ...
}
@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.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
minimumAgeValidator: FieldValidateFn<any, any, any, any, number> = ({
value,
}) => (value < 0 ? 'Invalid value' : undefined)
// ...
}
在上面的例子中,我们在不同的时间(在每次击键和字段失去焦点时)在同一个字段上验证不同的内容。由于 field.state.meta.errors 是一个数组,因此会显示给定时间的所有相关错误。您还可以使用 field.state.meta.errorMap 来获取基于验证完成时间(onChange、onBlur 等)的错误。有关显示错误的更多信息,请参见下文。
一旦您设置好验证,您可以将错误从数组映射到您的 UI 中显示
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container
[tanstackField]="form"
name="age"
[validators]="{
onChange: ageValidator
}"
#age="field"
>
<!-- ... -->
@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
// ...
}
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container
[tanstackField]="form"
name="age"
[validators]="{
onChange: ageValidator
}"
#age="field"
>
<!-- ... -->
@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
// ...
}
或者使用 errorMap 属性来访问您正在查找的特定错误
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container
[tanstackField]="form"
name="age"
[validators]="{
onChange: ageValidator
}"
#age="field"
>
<!-- ... -->
@if (age.api.state.meta.errorMap['onChange']) {
<em role="alert">{{ age.api.state.meta.errorMap['onChange'] }}</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
// ...
}
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container
[tanstackField]="form"
name="age"
[validators]="{
onChange: ageValidator
}"
#age="field"
>
<!-- ... -->
@if (age.api.state.meta.errorMap['onChange']) {
<em role="alert">{{ age.api.state.meta.errorMap['onChange'] }}</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
// ...
}
值得一提的是,我们的 errors 数组和 errorMap 与验证器返回的类型匹配。这意味着
@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
// ...
}
@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
// ...
}
如上所示,每个 [tanstackField] 都通过 onChange、onBlur 等回调接受自己的验证规则。也可以通过将类似的回调传递给 injectForm() 函数,在表单级别(而不是逐个字段)定义验证规则。
示例
@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)
}
@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)
}
虽然我们认为大多数验证将是同步的,但在许多情况下,网络调用或其他异步操作对于验证非常有用。
为此,我们有专门的 onChangeAsync、onBlurAsync 和其他可用于验证的方法
@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
}
// ...
}
@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
}
// ...
}
同步和异步验证可以共存。例如,可以在同一字段上同时定义 onBlur 和 onBlurAsync
@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
}
// ...
}
@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
}
// ...
}
同步验证方法 (onBlur) 首先运行,异步方法 (onBlurAsync) 仅在同步方法 (onBlur) 成功时才运行。要更改此行为,请将 asyncAlways 选项设置为 true,异步方法将运行,而不管同步方法的结果如何。
虽然异步调用是在数据库验证时的首选方法,但在每次击键时都运行网络请求是 DDOS 数据库的好方法。
相反,我们通过添加一个属性,启用了一种简单的方法来防抖您的 async 调用
<ng-container
[tanstackField]="form"
name="age"
asyncDebounceMs={500}
[validators]="{ onChangeAsync: someValidator }"
#age="field"
>
<!-- ... -->
</ng-container>
<ng-container
[tanstackField]="form"
name="age"
asyncDebounceMs={500}
[validators]="{ onChangeAsync: someValidator }"
#age="field"
>
<!-- ... -->
</ng-container>
这将防抖每个异步调用,延迟 500 毫秒。您甚至可以为每个验证属性覆盖此属性
<ng-container
[tanstackField]="form"
name="age"
[validators]="{
onChangeAsyncDebounceMs: 1500,
onChangeAsync: someValidator,
onBlurAsync: otherValidator
}"
#age="field"
>
<!-- ... -->
</ng-container>
<ng-container
[tanstackField]="form"
name="age"
[validators]="{
onChangeAsyncDebounceMs: 1500,
onChangeAsync: someValidator,
onBlurAsync: otherValidator
}"
#age="field"
>
<!-- ... -->
</ng-container>
这将每 1500 毫秒运行一次 onChangeAsync,而 onBlurAsync 将每 500 毫秒运行一次。
虽然函数为您的验证提供了更大的灵活性和自定义性,但它们可能有点冗长。为了帮助解决这个问题,有一些库提供了基于 schema 的验证,以使简写和类型严格的验证变得更加容易。您还可以为整个表单定义一个 schema,并将其传递到表单级别,错误将自动传播到字段。
TanStack Form 原生支持所有遵循 Standard Schema 规范的库,最值得注意的是
注意:请确保使用最新版本的 schema 库,因为旧版本可能尚不支持 Standard Schema。
要使用这些库中的 schema,您可以像使用自定义函数一样将它们传递给 validators props
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
// ...
}
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
// ...
}
表单和字段级别的异步验证也受支持
@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',
},
)
// ...
}
@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',
},
)
// ...
}
onChange、onBlur 等回调也会在表单提交时运行,如果表单无效,则会阻止提交。
表单状态对象有一个 canSubmit 标志,当任何字段无效且表单已被触摸时,该标志为 false(canSubmit 在表单被触摸之前为 true,即使某些字段根据其 onChange/onBlur props 在“技术上”无效)。
您可以通过 injectStore 订阅它,并使用该值来禁用提交按钮,例如,当表单无效时(实际上,禁用的按钮无法访问,请改用 aria-disabled)。
@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)
// ...
}
@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)
// ...
}
您的每周 JavaScript 新闻。每周一免费发送给超过 10 万名开发者。