Skip to main content
Understand the fundamental building blocks of TanStack Form including form instances, fields, state management, and validation patterns.

Form Instance

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

Field Metadata Flags

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

Build docs developers (and LLMs) love