TanStack Form supports array fields for managing dynamic lists of values, including arrays of objects with nested fields.
Basic Usage
Create an array field using the tanstackField directive and iterate over the values:
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="people" #people="field">
<div>
@for (_ of people.api.state.value; track $index) {
<!-- Render each item -->
}
</div>
</ng-container>
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
people: [] as Array<{ name: string; age: number }>,
},
onSubmit({ value }) {
alert(JSON.stringify(value))
},
})
}
Adding Items
Use the pushValue method to add items to the array:
<button (click)="people.api.pushValue(defaultPerson)" type="button">
Add person
</button>
Define the default value in your component:
export class AppComponent {
defaultPerson = { name: '', age: 0 }
// ...
}
Accessing Nested Fields
Access nested fields using bracket notation and a helper function:
<ng-container
[tanstackField]="form"
[name]="getPeopleName($index)"
#person="field"
>
<div>
<label>
<div>Name for person {{ $index }}</div>
<input
[value]="person.api.state.value"
(input)="person.api.handleChange($any($event).target.value)"
/>
</label>
</div>
</ng-container>
Define the helper function with proper typing:
export class AppComponent {
getPeopleName = (idx: number) => `people[${idx}].name` as const
// ...
}
The helper function is required for TypeScript type safety. Template string interpolation like "people[" + $index + "].name" produces a string type instead of the required literal type.
Complete Example
Here’s a full example with array management:
import { Component } from '@angular/core'
import { TanStackField, injectForm, injectStore } from '@tanstack/angular-form'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<form (submit)="handleSubmit($event)">
<div>
<ng-container [tanstackField]="form" name="people" #people="field">
<div>
@for (_ of people.api.state.value; track $index) {
<ng-container
[tanstackField]="form"
[name]="getPeopleName($index)"
#person="field"
>
<div>
<label>
<div>Name for person {{ $index }}</div>
<input
[value]="person.api.state.value"
(input)="person.api.handleChange($any($event).target.value)"
/>
</label>
</div>
</ng-container>
}
</div>
<button (click)="people.api.pushValue(defaultPerson)" type="button">
Add person
</button>
</ng-container>
</div>
<button type="submit" [disabled]="!canSubmit()">
{{ isSubmitting() ? '...' : 'Submit' }}
</button>
</form>
`,
})
export class AppComponent {
defaultPerson = { name: '', age: 0 }
form = injectForm({
defaultValues: {
people: [] as Array<{ name: string; age: number }>,
},
onSubmit({ value }) {
alert(JSON.stringify(value))
},
})
getPeopleName = (idx: number) => `people[${idx}].name` as const
canSubmit = injectStore(this.form, (state) => state.canSubmit)
isSubmitting = injectStore(this.form, (state) => state.isSubmitting)
handleSubmit(event: SubmitEvent) {
event.preventDefault()
event.stopPropagation()
this.form.handleSubmit()
}
}
Array Field Methods
The field API provides several methods for managing array values:
pushValue
Add a value to the end of the array:
people.api.pushValue({ name: 'John', age: 30 })
removeValue
Remove a value at a specific index:
people.api.removeValue(index)
Example with a remove button:
<button
type="button"
(click)="people.api.removeValue($index)"
>
Remove
</button>
insertValue
Insert a value at a specific index:
people.api.insertValue(2, { name: 'Jane', age: 25 })
replaceValue
Replace a value at a specific index:
people.api.replaceValue(1, { name: 'Bob', age: 35 })
swapValues
Swap two values by their indices:
people.api.swapValues(0, 2)
moveValue
Move a value from one index to another:
people.api.moveValue(0, 3)
clearValues
Remove all values from the array:
Nested Object Fields
Work with multiple nested fields within array items:
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>
<ng-container
[tanstackField]="form"
[name]="getHobbyDesc($index)"
#hobbyDesc="field"
>
<div>
<label [for]="hobbyDesc.api.name">Description:</label>
<input
[id]="hobbyDesc.api.name"
[name]="hobbyDesc.api.name"
[value]="hobbyDesc.api.state.value"
(blur)="hobbyDesc.api.handleBlur()"
(input)="hobbyDesc.api.handleChange($any($event).target.value)"
/>
</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
getHobbyDesc = (idx: number) => `hobbies[${idx}].description` as const
form = injectForm({
defaultValues: {
hobbies: [] as Array<{
name: string
description: string
yearsOfExperience: number
}>,
},
onSubmit({ value }) {
alert(JSON.stringify(value))
},
})
}
Array Validation
Validate array fields just like regular fields:
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<ng-container
[tanstackField]="form"
name="people"
[validators]="{
onChange: peopleValidator
}"
#people="field"
>
<div>
@for (_ of people.api.state.value; track $index) {
<!-- ... -->
}
</div>
@if (people.api.state.meta.errors) {
<em role="alert">{{ people.api.state.meta.errors.join(', ') }}</em>
}
</ng-container>
`,
})
export class AppComponent {
peopleValidator: FieldValidateFn<any, any, any, any, any[]> = ({ value }) =>
value.length === 0 ? 'At least one person is required' : undefined
// ...
}
Validate Individual Array Items
Apply validation to specific fields within array items:
<ng-container
[tanstackField]="form"
[name]="getPeopleName($index)"
[validators]="{
onChange: nameValidator
}"
#person="field"
>
<input
[value]="person.api.state.value"
(input)="person.api.handleChange($any($event).target.value)"
/>
@if (person.api.state.meta.errors) {
<em role="alert">{{ person.api.state.meta.errors.join(', ') }}</em>
}
</ng-container>
nameValidator: FieldValidateFn<any, any, any, any, string> = ({ value }) =>
value.length < 2 ? 'Name must be at least 2 characters' : undefined