Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tanstack/form/llms.txt
Use this file to discover all available pages before exploring further.
Understand the fundamental building blocks of TanStack Form including form instances, fields, state management, and validation patterns.
A Form Instance represents an individual form and provides methods for managing form state. Create one using the injectForm function with Angular’s dependency injection:
const form = injectForm({
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})
The form instance is injected into your component and can be passed to field directives.
Field
A Field represents a single form input element. Create fields using the tanstackField directive with a template reference variable:
<ng-container [tanstackField]="form" name="firstName" #firstName="field">
<input
[value]="firstName.api.state.value"
(blur)="firstName.api.handleBlur()"
(input)="firstName.api.handleChange($any($event).target.value)"
/>
</ng-container>
The name prop must match a key in your form’s default values. The template variable exposes the field’s API through the api property.
Field State
Each field maintains its own state including current value, validation status, and metadata. Access it via fieldApi.state:
const {
value,
meta: { errors, isValidating },
} = field.state
Four key states track user interaction:
- isTouched - Set after the user changes or blurs the field
- isDirty - Set after the field value changes (persistent, even if reverted)
- isPristine - Remains true until the field value changes (opposite of isDirty)
- isBlurred - Set after the field loses focus
const { isTouched, isDirty, isPristine, isBlurred } = field.state.meta
Understanding isDirty Behavior
TanStack Form uses a persistent dirty state model. Once a field is changed, it remains dirty even if reverted to the default value.
For non-persistent dirty behavior, use the isDefaultValue flag:
const { isDefaultValue, isTouched } = field.state.meta
// Non-persistent dirty check
const nonPersistentIsDirty = !isDefaultValue
Field API
The Field API provides methods for managing field state. Access it through the template variable:
<input
[value]="fieldName.api.state.value"
(blur)="fieldName.api.handleBlur()"
(input)="fieldName.api.handleChange($any($event).target.value)"
/>
Validation
Define validation rules at the field level using the validators prop:
import { Component } from '@angular/core'
import { TanStackField, injectForm } from '@tanstack/angular-form'
import type { FieldValidateFn, FieldValidateAsyncFn } from '@tanstack/angular-form'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container
[tanstackField]="form"
name="firstName"
[validators]="{
onChange: firstNameValidator,
onChangeAsyncDebounceMs: 500,
onChangeAsync: firstNameAsyncValidator
}"
#firstName="field"
>
<input
[value]="firstName.api.state.value"
(blur)="firstName.api.handleBlur()"
(input)="firstName.api.handleChange($any($event).target.value)"
/>
@if (firstName.api.state.meta.errors) {
<em role="alert">{{ firstName.api.state.meta.errors.join(', ') }}</em>
}
</ng-container>
`,
})
export class AppComponent {
firstNameValidator: FieldValidateFn<any, any, string, any> = ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined
firstNameAsyncValidator: FieldValidateAsyncFn<any, string, any> =
async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error') && 'No "error" allowed in first name'
}
form = injectForm({
defaultValues: {
firstName: '',
},
onSubmit({ value }) {
console.log(value)
},
})
}
Standard Schema Libraries
Use schema validation libraries that support the Standard Schema specification:
import { z } from 'zod'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container
[tanstackField]="form"
name="firstName"
[validators]="{
onChange: z.string().min(3, 'First name must be at least 3 characters'),
onChangeAsyncDebounceMs: 500,
onChangeAsync: firstNameAsyncValidator
}"
#firstName="field"
>
<!-- ... -->
</ng-container>
`,
})
export class AppComponent {
firstNameAsyncValidator = z.string().refine(
async (value) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return !value.includes('error')
},
{
message: "No 'error' allowed in first name",
},
)
form = injectForm({
defaultValues: {
firstName: '',
},
onSubmit({ value }) {
console.log(value)
},
})
z = z
}
Reactivity
Subscribe to form and field state changes using injectStore:
import { injectForm, injectStore } from '@tanstack/angular-form'
@Component({/*...*/})
class AppComponent {
form = injectForm({/*...*/})
canSubmit = injectStore(this.form, (state) => state.canSubmit)
isSubmitting = injectStore(this.form, (state) => state.isSubmitting)
}
Use the reactive values in your template:
<button type="submit" [disabled]="!canSubmit()">
{{ isSubmitting() ? '...' : 'Submit' }}
</button>
Listeners
React to specific field events by passing listener functions:
import { Component } from '@angular/core'
import { TanStackField, injectForm } from '@tanstack/angular-form'
import type { FieldListenerFn } from '@tanstack/angular-form'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container
[tanstackField]="form"
name="country"
[listeners]="{
onChange: onCountryChange
}"
#country="field"
></ng-container>
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
country: '',
province: '',
},
onSubmit({ value }) {
console.log(value)
},
})
onCountryChange: FieldListenerFn<any, any, any, any, string> = ({ value }) => {
console.log(`Country changed to: ${value}, resetting province`)
this.form.setFieldValue('province', '')
}
}
Array Fields
Manage dynamic lists of values using array field methods:
import { Component } from '@angular/core'
import { TanStackField, injectForm } from '@tanstack/angular-form'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container [tanstackField]="form" name="hobbies" #hobbies="field">
<div>
Hobbies
<div>
@if (!hobbies.api.state.value.length) {
No hobbies found
}
@for (_ of hobbies.api.state.value; track $index) {
<div>
<ng-container
[tanstackField]="form"
[name]="getHobbyName($index)"
#hobbyName="field"
>
<div>
<label [for]="hobbyName.api.name">Name:</label>
<input
[id]="hobbyName.api.name"
[name]="hobbyName.api.name"
[value]="hobbyName.api.state.value"
(blur)="hobbyName.api.handleBlur()"
(input)="hobbyName.api.handleChange($any($event).target.value)"
/>
<button
type="button"
(click)="hobbies.api.removeValue($index)"
>
X
</button>
</div>
</ng-container>
</div>
}
</div>
<button type="button" (click)="hobbies.api.pushValue(defaultHobby)">
Add hobby
</button>
</div>
</ng-container>
`,
})
export class AppComponent {
defaultHobby = {
name: '',
description: '',
yearsOfExperience: 0,
}
getHobbyName = (idx: number) => `hobbies[${idx}].name` as const
form = injectForm({
defaultValues: {
hobbies: [] as Array<{
name: string
description: string
yearsOfExperience: number
}>,
},
onSubmit({ value }) {
alert(JSON.stringify(value))
},
})
}
Array Field Methods
pushValue(value) - Add a new item to the end
removeValue(index) - Remove an item at index
swapValues(indexA, indexB) - Swap two items
moveValue(from, to) - Move an item to a new position
insertValue(index, value) - Insert an item at index
replaceValue(index, value) - Replace an item at index
clearValues() - Remove all items