Form Groups
When building a multi-stage form that has many stages, like so:

It's common for each step to have its own form. However, this complicates the form submission and validation process by requiring you to add complex logic.
Luckily, TanStack Form provides a way to build out sub-forms that make this kind of development trivial to implement: [tanstackFormGroup].
Usage#
To use a form group in TanStack Form, you'll use injectForm to create a form variable, then reference it with the tanstackFormGroup directive like you would with tanstackField:
import { Component } from '@angular/core'
import { TanStackFormGroup, injectForm } from '@tanstack/angular-form'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackFormGroup],
template: `
<ng-container [tanstackFormGroup]="form" name="step1" #group="formGroup">
<!-- `group.api` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue` -->
<!-- ... -->
</ng-container>
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
step1: {
name: '',
},
step2: {
age: 0,
},
},
})
}This becomes much more useful when paired with external state to conditionally render a form group:
import { Component, signal } from '@angular/core'
import {
TanStackField,
TanStackFormGroup,
injectForm,
} from '@tanstack/angular-form'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField, TanStackFormGroup],
template: `
@if (step() === 0) {
<ng-container
[tanstackFormGroup]="form"
name="step1"
[onGroupSubmit]="onStep1Submit"
[onGroupSubmitInvalid]="onStep1SubmitInvalid"
[onSubmitMeta]="submitMeta"
#group="formGroup"
>
<form
(submit)="
$event.preventDefault();
$event.stopPropagation();
group.api.handleSubmit()
"
>
<ng-container [tanstackField]="form" name="step1.name" #name="field">
<!-- ... -->
</ng-container>
</form>
</ng-container>
}
@if (step() === 1) {
<ng-container
[tanstackFormGroup]="form"
name="step2"
[onGroupSubmit]="onStep2Submit"
#group="formGroup"
>
<form
(submit)="
$event.preventDefault();
$event.stopPropagation();
group.api.handleSubmit()
"
>
<ng-container [tanstackField]="form" name="step2.age" #age="field">
<!-- ... -->
</ng-container>
</form>
</ng-container>
}
`,
})
export class AppComponent {
step = signal(0)
submitMeta = {} as SomeType
form = injectForm({
defaultValues: {
step1: {
name: '',
},
step2: {
age: 0,
},
},
})
onStep1Submit = () => {
// We can move the step forward when validation passes
this.step.update((step) => step + 1)
}
onStep1SubmitInvalid = () => {
// Or handle invalid submissions, just like a top-level form
}
onStep2Submit = () => {
// Then, use `form.handleSubmit()` to submit the entire form
this.form.handleSubmit()
}
}When you split each step into its own component, pass the parent form down with tanstack-with-form and read it in the child with injectWithForm. The child can then use [tanstackFormGroup]="withForm.form" and [tanstackField]="withForm.form" against the same parent form instance.
Form Group Validation#
Form groups have a distinct validation procedure that we think makes sense for sub-forms:
- Form groups can have their own validation:
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackFormGroup],
template: `
<ng-container
[tanstackFormGroup]="form"
name="step1"
[validators]="{ onChange: groupValidator }"
#group="formGroup"
>
<!-- group.api.state.meta.errorMap // {onChange: "Error" | undefined} -->
<!-- group.api.state.meta.errors // ("Error")[] -->
</ng-container>
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
step1: { name: '' },
},
})
groupValidator = () => 'Error'
}- Can set errors on sub-fields:
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackFormGroup],
template: `
<ng-container
[tanstackFormGroup]="form"
name="step1"
[validators]="{ onChange: groupValidator }"
></ng-container>
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
step1: { name: '' },
},
})
groupValidator = ({ value }: { value: { name: string } }) => ({
group: value.name === 'error' ? 'Group error' : undefined,
fields: {
// Must use the name of the field relative to the FormGroup as the error key,
// to stay consistent with how standard schema works with form groups
name: value.name === 'error' ? 'Field error' : undefined,
},
})
}- And can even accept standard schemas:
import { z } from 'zod'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackFormGroup],
template: `
<ng-container
[tanstackFormGroup]="form"
name="step1"
[validators]="{ onChange: step1Schema }"
></ng-container>
`,
})
export class AppComponent {
step1Schema = z.object({
name: z.string().min(2),
})
form = injectForm({
defaultValues: {
step1: { name: '' },
},
})
}The reason we don't use the full path names for fields is so that you can compose your schemas like so:
tsconst step1Schema = z.object({ name: z.string().min(2), }) const schema = z.object({ step1: step1Schema, step2: step2Schema, })And pass the step1Schema to a form group and schema to the parent form. That way, partially validated data will still flag errors if the group is bypassed.
Dynamic Group Validation#
If you want to use dynamic validation (onDynamic) with a form group, do not rely on the onDynamic validator passed to injectForm:
form = injectForm({
validationLogic: revalidateLogic(),
validators: {
// This validator will not run `onChange` when a sub-form is submitted;
// it will only run `onChange` when the form itself is submitted.
onDynamic: schema,
},
})Instead, pass your sub-schema for the group to the onDynamic validation of the group itself:
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackFormGroup],
template: `
<ng-container
[tanstackFormGroup]="form"
name="step1"
[validators]="{ onDynamic: step1Schema }"
></ng-container>
`,
})
export class AppComponent {
step1Schema = step1Schema
form = injectForm({
defaultValues: {
step1: { name: '' },
},
})
}It will treat group.api.submissionAttempts as the way to change what validator is run before/after submit.
Form Group State#
Just like you're able to access group.api.state.meta.errors, you're also able to access the group's value using group.api.state.value. Likewise, here are some valuable properties you can access in the group.api.state.meta:
- group.api.state.meta.isFieldsValid: true when the field-level validators have no errors
- group.api.state.meta.isGroupValid: true when the group-level validators have no errors
- group.api.state.meta.isValid: true when both the field-level and group-level validators have no errors
- group.api.state.meta.isSubmitting: true when the group is in the process of being submitted