diff --git a/docs/assets/stepper.png b/docs/assets/stepper.png
new file mode 100644
index 000000000..8e006d9e7
Binary files /dev/null and b/docs/assets/stepper.png differ
diff --git a/docs/config.json b/docs/config.json
index 45ea9c833..181f6139e 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -128,6 +128,10 @@
"label": "Arrays",
"to": "framework/react/guides/arrays"
},
+ {
+ "label": "Form Groups",
+ "to": "framework/react/guides/form-groups"
+ },
{
"label": "Linked Fields",
"to": "framework/react/guides/linked-fields"
@@ -201,6 +205,10 @@
"label": "Arrays",
"to": "framework/preact/guides/arrays"
},
+ {
+ "label": "Form Groups",
+ "to": "framework/preact/guides/form-groups"
+ },
{
"label": "Linked Fields",
"to": "framework/preact/guides/linked-fields"
@@ -262,6 +270,10 @@
"label": "Arrays",
"to": "framework/vue/guides/arrays"
},
+ {
+ "label": "Form Groups",
+ "to": "framework/vue/guides/form-groups"
+ },
{
"label": "Linked Fields",
"to": "framework/vue/guides/linked-fields"
@@ -287,6 +299,10 @@
"label": "Arrays",
"to": "framework/angular/guides/arrays"
},
+ {
+ "label": "Form Groups",
+ "to": "framework/angular/guides/form-groups"
+ },
{
"label": "Form Composition",
"to": "framework/angular/guides/form-composition"
@@ -316,6 +332,10 @@
"label": "Arrays",
"to": "framework/solid/guides/arrays"
},
+ {
+ "label": "Form Groups",
+ "to": "framework/solid/guides/form-groups"
+ },
{
"label": "Linked Fields",
"to": "framework/solid/guides/linked-fields"
@@ -345,6 +365,10 @@
"label": "Arrays",
"to": "framework/lit/guides/arrays"
},
+ {
+ "label": "Form Groups",
+ "to": "framework/lit/guides/form-groups"
+ },
{
"label": "Form Composition",
"to": "framework/lit/guides/form-composition"
@@ -374,6 +398,10 @@
"label": "Arrays",
"to": "framework/svelte/guides/arrays"
},
+ {
+ "label": "Form Groups",
+ "to": "framework/svelte/guides/form-groups"
+ },
{
"label": "Linked Fields",
"to": "framework/svelte/guides/linked-fields"
@@ -644,6 +672,10 @@
"label": "Simple",
"to": "framework/react/examples/simple"
},
+ {
+ "label": "Multi-Step Wizard",
+ "to": "framework/react/examples/multi-step-wizard"
+ },
{
"label": "Arrays",
"to": "framework/react/examples/array"
@@ -697,6 +729,10 @@
"label": "Simple",
"to": "framework/vue/examples/simple"
},
+ {
+ "label": "Multi-Step Wizard",
+ "to": "framework/vue/examples/multi-step-wizard"
+ },
{
"label": "Arrays",
"to": "framework/vue/examples/array"
@@ -714,6 +750,10 @@
"label": "Simple",
"to": "framework/angular/examples/simple"
},
+ {
+ "label": "Multi-Step Wizard",
+ "to": "framework/angular/examples/multi-step-wizard"
+ },
{
"label": "Arrays",
"to": "framework/angular/examples/array"
@@ -735,6 +775,10 @@
"label": "Simple",
"to": "framework/solid/examples/simple"
},
+ {
+ "label": "Multi-Step Wizard",
+ "to": "framework/solid/examples/multi-step-wizard"
+ },
{
"label": "Arrays",
"to": "framework/solid/examples/array"
@@ -756,6 +800,10 @@
"label": "Simple",
"to": "framework/lit/examples/simple"
},
+ {
+ "label": "Multi-Step Wizard",
+ "to": "framework/lit/examples/multi-step-wizard"
+ },
{
"label": "Array",
"to": "framework/lit/examples/array"
@@ -770,6 +818,19 @@
}
]
},
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "Simple",
+ "to": "framework/preact/examples/simple"
+ },
+ {
+ "label": "Multi-Step Wizard",
+ "to": "framework/preact/examples/multi-step-wizard"
+ }
+ ]
+ },
{
"label": "svelte",
"children": [
@@ -777,6 +838,10 @@
"label": "Simple",
"to": "framework/svelte/examples/simple"
},
+ {
+ "label": "Multi-Step Wizard",
+ "to": "framework/svelte/examples/multi-step-wizard"
+ },
{
"label": "Arrays",
"to": "framework/svelte/examples/array"
diff --git a/docs/framework/angular/guides/form-groups.md b/docs/framework/angular/guides/form-groups.md
new file mode 100644
index 000000000..96beebddd
--- /dev/null
+++ b/docs/framework/angular/guides/form-groups.md
@@ -0,0 +1,302 @@
+---
+id: form-groups
+title: 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`:
+
+```angular-ts
+import { Component } from '@angular/core'
+import { TanStackFormGroup, injectForm } from '@tanstack/angular-form'
+
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [TanStackFormGroup],
+ template: `
+
+
+
+
+ `,
+})
+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:
+
+```angular-ts
+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) {
+
+
+
+ }
+
+ @if (step() === 1) {
+
+
+
+ }
+ `,
+})
+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`](./form-composition.md) 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:
+
+```angular-ts
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [TanStackFormGroup],
+ template: `
+
+
+
+
+ `,
+})
+export class AppComponent {
+ form = injectForm({
+ defaultValues: {
+ step1: { name: '' },
+ },
+ })
+
+ groupValidator = () => 'Error'
+}
+```
+
+- Can set errors on sub-fields:
+
+```angular-ts
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [TanStackFormGroup],
+ template: `
+
+ `,
+})
+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:
+
+```angular-ts
+import { z } from 'zod'
+
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [TanStackFormGroup],
+ template: `
+
+ `,
+})
+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:
+>
+> ```ts
+> const 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`)](./dynamic-validation.md) with a form group, do not rely on the `onDynamic` validator passed to `injectForm`:
+
+```ts
+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:
+
+```angular-ts
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [TanStackFormGroup],
+ template: `
+
+ `,
+})
+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
diff --git a/docs/framework/lit/guides/form-groups.md b/docs/framework/lit/guides/form-groups.md
new file mode 100644
index 000000000..1736602ff
--- /dev/null
+++ b/docs/framework/lit/guides/form-groups.md
@@ -0,0 +1,236 @@
+---
+id: form-groups
+title: 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: `form.group(...)`.
+
+## Usage
+
+To use a form group in TanStack Form, you'll create a `TanStackFormController`, then use its `group` directive like you would use its `field` directive:
+
+```ts
+import { LitElement, html } from 'lit'
+import { customElement } from 'lit/decorators.js'
+import { TanStackFormController } from '@tanstack/lit-form'
+
+@customElement('my-form')
+export class MyForm extends LitElement {
+ #form = new TanStackFormController(this, {
+ defaultValues: {
+ step1: {
+ name: '',
+ },
+ step2: {
+ age: 0,
+ },
+ },
+ })
+
+ render() {
+ return html`
+ ${this.#form.group({ name: 'step1' }, (group) => {
+ // `group` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue`
+ // ...
+ return html``
+ })}
+ `
+ }
+}
+```
+
+This becomes much more useful when paired with external state to conditionally render a form group:
+
+```ts
+import { LitElement, html, nothing } from 'lit'
+import { customElement, state } from 'lit/decorators.js'
+import { TanStackFormController } from '@tanstack/lit-form'
+
+@customElement('my-form')
+export class MyForm extends LitElement {
+ @state()
+ private step = 0
+
+ #form = new TanStackFormController(this, {
+ defaultValues: {
+ step1: {
+ name: '',
+ },
+ step2: {
+ age: 0,
+ },
+ },
+ })
+
+ render() {
+ return html`
+ ${this.step === 0
+ ? this.#form.group(
+ {
+ name: 'step1',
+ onGroupSubmit: () => {
+ // We can move the step forward when validation passes
+ this.step++
+ },
+ onGroupSubmitInvalid: () => {
+ // Or handle invalid submissions, just like a top-level form
+ },
+ onSubmitMeta: {} as SomeType,
+ },
+ (group) => html`
+
+ `,
+ )
+ : nothing}
+ ${this.step === 1
+ ? this.#form.group(
+ {
+ name: 'step2',
+ onGroupSubmit: () => {
+ // Then, use `this.#form.api.handleSubmit()` to submit the entire form
+ this.#form.api.handleSubmit()
+ },
+ },
+ (group) => html`
+
+ `,
+ )
+ : nothing}
+ `
+ }
+}
+```
+
+When you split each step into its own custom element, pass the `TanStackFormController` as a property and type it with [`getFormType`](./form-composition.md). Because Lit child elements that receive a controller by property do not automatically re-render when the controller state changes, subscribe to `form.api.store` with `TanStackStoreSelector` in the child element.
+
+## 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:
+
+```ts
+${this.#form.group(
+ { name: 'step1', validators: { onChange: () => 'Error' } },
+ (group) => html`
+
+
+ `,
+)}
+```
+
+- Can set errors on sub-fields:
+
+```ts
+${this.#form.group(
+ {
+ name: 'step1',
+ validators: {
+ onChange: ({ value, groupApi }) => ({
+ group: value.name === 'error' ? 'Group error' : undefined,
+ fields: {
+ // Must use the name of the field relative to the form group as the error key,
+ // to stay consistent with how standard schema works with form groups
+ name: value.name === 'error' ? 'Field error' : undefined,
+ },
+ }),
+ },
+ },
+ (group) => html``,
+)}
+```
+
+- And can even accept standard schemas:
+
+```ts
+${this.#form.group(
+ {
+ name: 'step1',
+ validators: {
+ onChange: z.object({
+ name: z.string().min(2),
+ }),
+ },
+ },
+ (group) => html``,
+)}
+```
+
+> The reason we don't use the full path names for fields is so that you can compose your schemas like so:
+>
+> ```ts
+> const 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`)](./dynamic-validation.md) with a form group, do not rely on the `onDynamic` validator passed to `TanStackFormController`:
+
+```ts
+#form = new TanStackFormController(this, {
+ 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:
+
+```ts
+${this.#form.group(
+ { name: 'step1', validators: { onDynamic: step1Schema } },
+ (group) => html``,
+)}
+```
+
+It will treat `group.submissionAttempts` as the way to change what validator is run before/after submit.
+
+## Form Group State
+
+Just like you're able to access `group.state.meta.errors`, you're also able to access the group's value using `group.state.value`. Likewise, here are some valuable properties you can access in the `group.state.meta`:
+
+- `group.state.meta.isFieldsValid`: `true` when the field-level validators have no errors
+- `group.state.meta.isGroupValid`: `true` when the group-level validators have no errors
+- `group.state.meta.isValid`: `true` when both the field-level and group-level validators have no errors
+- `group.state.meta.isSubmitting`: `true` when the group is in the process of being submitted
diff --git a/docs/framework/preact/guides/form-groups.md b/docs/framework/preact/guides/form-groups.md
new file mode 100644
index 000000000..f3d908d31
--- /dev/null
+++ b/docs/framework/preact/guides/form-groups.md
@@ -0,0 +1,177 @@
+---
+id: form-groups
+title: 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: ``.
+
+## Usage
+
+To use a form group in TanStack Form, you'll use `useForm` or [`useAppForm`](./form-composition.md) to create a `form` variable, then reference its `FormGroup` component like you would a `Field`:
+
+```tsx
+const form = useForm({
+ defaultValues: {
+ step1: {
+ name: ""
+ },
+ step2: {
+ age: 0
+ }
+ }
+})
+
+return (
+ (
+ // `group` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue`
+ // ...
+ )}
+ />
+)
+```
+
+This becomes much more useful when paired with external state to conditionally render a `FormGroup`:
+
+```tsx
+const [step, setStep] = useState(0)
+const form = useForm({
+ defaultValues: {
+ step1: {
+ name: ""
+ },
+ step2: {
+ age: 0
+ }
+ }
+})
+
+return (
+ <>
+ {step === 0 ? {
+ // We can move the step forward when validation passes
+ setStep(step + 1)
+ }}
+ onGroupSubmitInvalid={() => {
+ // Or handle invalid submissions, just like a top-level form
+ }}
+ onSubmitMeta={{} as SomeType}
+ children={group => (
+ // Use `group.handleSubmit()` to submit the sub-form, but not the parent form
+ // ...
+ )}
+ /> : null }
+ {step === 1 ? (
+ // Then, use `form.handleSubmit()` to submit the entire form
+ // ...
+ )}
+ /> : null }
+ >
+)
+```
+
+## Form Group Validation
+
+Form groups have a distinct validation proceedure that we think makes sense for sub-forms:
+
+- Form groups can have their own validation:
+
+```tsx
+ 'Error' }}
+ children={(group) => {
+ group.state.meta.errorMap // {onChange: "Error" | undefined}
+ group.state.meta.errors // ("Error")[]
+ }}
+/>
+```
+
+- Can set errors on sub-fields:
+
+```tsx
+ ({
+ 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:
+
+```tsx
+
+```
+
+> The reason we don't use the full path names for fields is so that you can compose your schemas like so:
+>
+> ```
+> const 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`)](./dynamic-validation.md) with a form group, do not rely on the `onDynamic` validator passed to `useForm`:
+
+```tsx
+useForm({
+ 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 `FormGroup` itself:
+
+```tsx
+
+```
+
+It will treat `group.submissionAttempts` as the way to change what validator is ran before/after submit.
+
+## Form Group State
+
+Just like you're able to access `group.state.meta.errors`, you're also able to access the group's value using `group.state.value`. Likewise, here are some valuable properties you can access in the `group.state.meta`:
+
+- `group.state.meta.isFieldsValid`: `true` when the field-level validators have no errors
+- `group.state.meta.isGroupValid`: `true` when the group-level validators have no errors
+- `group.state.meta.isValid`: `true` when both the field-level and group-level validators have no errors
+- `group.state.meta.isSubmitting`: `true` when the group is in the process of being submitted
diff --git a/docs/framework/react/guides/form-groups.md b/docs/framework/react/guides/form-groups.md
new file mode 100644
index 000000000..f3d908d31
--- /dev/null
+++ b/docs/framework/react/guides/form-groups.md
@@ -0,0 +1,177 @@
+---
+id: form-groups
+title: 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: ``.
+
+## Usage
+
+To use a form group in TanStack Form, you'll use `useForm` or [`useAppForm`](./form-composition.md) to create a `form` variable, then reference its `FormGroup` component like you would a `Field`:
+
+```tsx
+const form = useForm({
+ defaultValues: {
+ step1: {
+ name: ""
+ },
+ step2: {
+ age: 0
+ }
+ }
+})
+
+return (
+ (
+ // `group` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue`
+ // ...
+ )}
+ />
+)
+```
+
+This becomes much more useful when paired with external state to conditionally render a `FormGroup`:
+
+```tsx
+const [step, setStep] = useState(0)
+const form = useForm({
+ defaultValues: {
+ step1: {
+ name: ""
+ },
+ step2: {
+ age: 0
+ }
+ }
+})
+
+return (
+ <>
+ {step === 0 ? {
+ // We can move the step forward when validation passes
+ setStep(step + 1)
+ }}
+ onGroupSubmitInvalid={() => {
+ // Or handle invalid submissions, just like a top-level form
+ }}
+ onSubmitMeta={{} as SomeType}
+ children={group => (
+ // Use `group.handleSubmit()` to submit the sub-form, but not the parent form
+ // ...
+ )}
+ /> : null }
+ {step === 1 ? (
+ // Then, use `form.handleSubmit()` to submit the entire form
+ // ...
+ )}
+ /> : null }
+ >
+)
+```
+
+## Form Group Validation
+
+Form groups have a distinct validation proceedure that we think makes sense for sub-forms:
+
+- Form groups can have their own validation:
+
+```tsx
+ 'Error' }}
+ children={(group) => {
+ group.state.meta.errorMap // {onChange: "Error" | undefined}
+ group.state.meta.errors // ("Error")[]
+ }}
+/>
+```
+
+- Can set errors on sub-fields:
+
+```tsx
+ ({
+ 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:
+
+```tsx
+
+```
+
+> The reason we don't use the full path names for fields is so that you can compose your schemas like so:
+>
+> ```
+> const 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`)](./dynamic-validation.md) with a form group, do not rely on the `onDynamic` validator passed to `useForm`:
+
+```tsx
+useForm({
+ 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 `FormGroup` itself:
+
+```tsx
+
+```
+
+It will treat `group.submissionAttempts` as the way to change what validator is ran before/after submit.
+
+## Form Group State
+
+Just like you're able to access `group.state.meta.errors`, you're also able to access the group's value using `group.state.value`. Likewise, here are some valuable properties you can access in the `group.state.meta`:
+
+- `group.state.meta.isFieldsValid`: `true` when the field-level validators have no errors
+- `group.state.meta.isGroupValid`: `true` when the group-level validators have no errors
+- `group.state.meta.isValid`: `true` when both the field-level and group-level validators have no errors
+- `group.state.meta.isSubmitting`: `true` when the group is in the process of being submitted
diff --git a/docs/framework/solid/guides/form-groups.md b/docs/framework/solid/guides/form-groups.md
new file mode 100644
index 000000000..1a6e97ac9
--- /dev/null
+++ b/docs/framework/solid/guides/form-groups.md
@@ -0,0 +1,178 @@
+---
+id: form-groups
+title: 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: ``.
+
+## Usage
+
+To use a form group in TanStack Form, you'll use `createForm` or [`useAppForm`](./form-composition.md) to create a `form` variable, then reference its `FormGroup` component like you would a `Field`:
+
+```tsx
+const form = createForm(() => ({
+ defaultValues: {
+ step1: {
+ name: '',
+ },
+ step2: {
+ age: 0,
+ },
+ },
+}))
+
+return (
+
+ {(group) => (
+ // `group()` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue`
+ // ...
+ )}
+
+)
+```
+
+This becomes much more useful when paired with external state to conditionally render a `FormGroup`:
+
+```tsx
+const [step, setStep] = createSignal(0)
+const form = createForm(() => ({
+ defaultValues: {
+ step1: {
+ name: '',
+ },
+ step2: {
+ age: 0,
+ },
+ },
+}))
+
+return (
+ <>
+
+ {
+ // We can move the step forward when validation passes
+ setStep(step() + 1)
+ }}
+ onGroupSubmitInvalid={() => {
+ // Or handle invalid submissions, just like a top-level form
+ }}
+ onSubmitMeta={{} as SomeType}
+ >
+ {(group) => (
+ // Use `group().handleSubmit()` to submit the sub-form, but not the parent form
+ // ...
+ )}
+
+
+
+
+ {(group) => (
+ // Then, use `form.handleSubmit()` to submit the entire form
+ // ...
+ )}
+
+
+ >
+)
+```
+
+## Form Group Validation
+
+Form groups have a distinct validation proceedure that we think makes sense for sub-forms:
+
+- Form groups can have their own validation:
+
+```tsx
+ 'Error' }}>
+ {(group) => {
+ group().state.meta.errorMap // {onChange: "Error" | undefined}
+ group().state.meta.errors // ("Error")[]
+ }}
+
+```
+
+- Can set errors on sub-fields:
+
+```tsx
+ ({
+ 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:
+
+```tsx
+
+```
+
+> The reason we don't use the full path names for fields is so that you can compose your schemas like so:
+>
+> ```
+> const 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`)](./dynamic-validation.md) with a form group, do not rely on the `onDynamic` validator passed to `createForm`:
+
+```tsx
+createForm(() => ({
+ 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 `FormGroup` itself:
+
+```tsx
+
+```
+
+It will treat `group().submissionAttempts` as the way to change what validator is ran before/after submit.
+
+## Form Group State
+
+Just like you're able to access `group().state.meta.errors`, you're also able to access the group's value using `group().state.value`. Likewise, here are some valuable properties you can access in the `group().state.meta`:
+
+- `group().state.meta.isFieldsValid`: `true` when the field-level validators have no errors
+- `group().state.meta.isGroupValid`: `true` when the group-level validators have no errors
+- `group().state.meta.isValid`: `true` when both the field-level and group-level validators have no errors
+- `group().state.meta.isSubmitting`: `true` when the group is in the process of being submitted
diff --git a/docs/framework/svelte/guides/form-groups.md b/docs/framework/svelte/guides/form-groups.md
new file mode 100644
index 000000000..f6a85c99b
--- /dev/null
+++ b/docs/framework/svelte/guides/form-groups.md
@@ -0,0 +1,196 @@
+---
+id: form-groups
+title: 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: ``.
+
+## Usage
+
+To use a form group in TanStack Form, you'll use `createForm` or [`createAppForm`](./form-composition.md) to create a `form` variable, then reference its `FormGroup` component like you would a `Field`:
+
+```svelte
+
+
+
+ {#snippet children(group)}
+
+
+ {/snippet}
+
+```
+
+This becomes much more useful when paired with external state to conditionally render a `FormGroup`:
+
+```svelte
+
+
+{#if step === 0}
+ {
+ // We can move the step forward when validation passes
+ step++
+ }}
+ onGroupSubmitInvalid={() => {
+ // Or handle invalid submissions, just like a top-level form
+ }}
+ onSubmitMeta={{} as SomeType}
+ >
+ {#snippet children(group)}
+
+ {/snippet}
+
+{:else if step === 1}
+ form.handleSubmit()}>
+ {#snippet children(group)}
+
+ {/snippet}
+
+{/if}
+```
+
+## 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:
+
+```svelte
+ 'Error' }}>
+ {#snippet children(group)}
+
+
+ {/snippet}
+
+```
+
+- Can set errors on sub-fields:
+
+```svelte
+ ({
+ 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:
+
+```svelte
+
+```
+
+> The reason we don't use the full path names for fields is so that you can compose your schemas like so:
+>
+> ```ts
+> const 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`)](./dynamic-validation.md) with a form group, do not rely on the `onDynamic` validator passed to `createForm`:
+
+```ts
+createForm(() => ({
+ 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 `FormGroup` itself:
+
+```svelte
+
+```
+
+It will treat `group.submissionAttempts` as the way to change what validator is ran before/after submit.
+
+## Form Group State
+
+Just like you're able to access `group.state.meta.errors`, you're also able to access the group's value using `group.state.value`. Likewise, here are some valuable properties you can access in the `group.state.meta`:
+
+- `group.state.meta.isFieldsValid`: `true` when the field-level validators have no errors
+- `group.state.meta.isGroupValid`: `true` when the group-level validators have no errors
+- `group.state.meta.isValid`: `true` when both the field-level and group-level validators have no errors
+- `group.state.meta.isSubmitting`: `true` when the group is in the process of being submitted
diff --git a/docs/framework/vue/guides/form-groups.md b/docs/framework/vue/guides/form-groups.md
new file mode 100644
index 000000000..0a4da5ed5
--- /dev/null
+++ b/docs/framework/vue/guides/form-groups.md
@@ -0,0 +1,200 @@
+---
+id: form-groups
+title: 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: ``.
+
+## Usage
+
+To use a form group in TanStack Form, you'll use `useForm` to create a `form` variable, then reference its `FormGroup` component like you would a `Field`:
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+This becomes much more useful when paired with external state to conditionally render a `FormGroup`:
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+## Form Group Validation
+
+Form groups have a distinct validation proceedure that we think makes sense for sub-forms:
+
+- Form groups can have their own validation:
+
+```vue
+
+
+
+
+
+
+```
+
+- Can set errors on sub-fields:
+
+```vue
+
+
+
+```
+
+- And can even accept standard schemas:
+
+```vue
+
+
+
+```
+
+> The reason we don't use the full path names for fields is so that you can compose your schemas like so:
+>
+> ```ts
+> const 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`)](./dynamic-validation.md) with a form group, do not rely on the `onDynamic` validator passed to `useForm`:
+
+```ts
+useForm({
+ 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 `FormGroup` itself:
+
+```vue
+
+
+
+```
+
+It will treat `formGroup.submissionAttempts` as the way to change what validator is ran before/after submit.
+
+## Form Group State
+
+Just like you're able to access `formGroup.state.meta.errors`, you're also able to access the group's value using `formGroup.state.value`. Likewise, here are some valuable properties you can access in the `formGroup.state.meta`:
+
+- `formGroup.state.meta.isFieldsValid`: `true` when the field-level validators have no errors
+- `formGroup.state.meta.isGroupValid`: `true` when the group-level validators have no errors
+- `formGroup.state.meta.isValid`: `true` when both the field-level and group-level validators have no errors
+- `formGroup.state.meta.isSubmitting`: `true` when the group is in the process of being submitted
diff --git a/examples/angular/multi-step-wizard/.editorconfig b/examples/angular/multi-step-wizard/.editorconfig
new file mode 100644
index 000000000..59d9a3a3e
--- /dev/null
+++ b/examples/angular/multi-step-wizard/.editorconfig
@@ -0,0 +1,16 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/examples/angular/multi-step-wizard/.gitignore b/examples/angular/multi-step-wizard/.gitignore
new file mode 100644
index 000000000..0711527ef
--- /dev/null
+++ b/examples/angular/multi-step-wizard/.gitignore
@@ -0,0 +1,42 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
diff --git a/examples/angular/multi-step-wizard/README.md b/examples/angular/multi-step-wizard/README.md
new file mode 100644
index 000000000..3e87f4c6d
--- /dev/null
+++ b/examples/angular/multi-step-wizard/README.md
@@ -0,0 +1,27 @@
+# Simple
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.1.
+
+## Development server
+
+Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
+
+## Code scaffolding
+
+Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
+
+## Build
+
+Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
+
+## Running unit tests
+
+Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
+
+## Running end-to-end tests
+
+Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
+
+## Further help
+
+To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
diff --git a/examples/angular/multi-step-wizard/angular.json b/examples/angular/multi-step-wizard/angular.json
new file mode 100644
index 000000000..0cc433f7f
--- /dev/null
+++ b/examples/angular/multi-step-wizard/angular.json
@@ -0,0 +1,78 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "simple": {
+ "projectType": "application",
+ "schematics": {},
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:application",
+ "options": {
+ "outputPath": "dist/simple",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "tsconfig.app.json",
+ "assets": ["src/favicon.ico", "src/assets"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "simple:build:production"
+ },
+ "development": {
+ "buildTarget": "simple:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "simple:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "polyfills": ["zone.js", "zone.js/testing"],
+ "tsConfig": "tsconfig.spec.json",
+ "assets": ["src/favicon.ico", "src/assets"],
+ "scripts": []
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/angular/multi-step-wizard/package.json b/examples/angular/multi-step-wizard/package.json
new file mode 100644
index 000000000..b69b1b9d5
--- /dev/null
+++ b/examples/angular/multi-step-wizard/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@tanstack/form-example-angular-multi-step-wizard",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng cache clean && ng serve",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development",
+ "test": "ng test"
+ },
+ "dependencies": {
+ "@angular/animations": "^21.2.14",
+ "@angular/common": "^21.2.14",
+ "@angular/compiler": "^21.2.14",
+ "@angular/core": "^21.2.14",
+ "@angular/forms": "^21.2.14",
+ "@angular/platform-browser": "^21.2.14",
+ "@angular/platform-browser-dynamic": "^21.2.14",
+ "@angular/router": "^21.2.14",
+ "@tanstack/angular-form": "^1.32.1",
+ "rxjs": "^7.8.2",
+ "tslib": "^2.8.1",
+ "zod": "^3.25.76",
+ "zone.js": "0.15.1"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^21.2.12",
+ "@angular/cli": "^21.2.12",
+ "@angular/compiler-cli": "^21.2.14",
+ "typescript": "5.9.3"
+ }
+}
diff --git a/examples/angular/multi-step-wizard/src/app/app.component.ts b/examples/angular/multi-step-wizard/src/app/app.component.ts
new file mode 100644
index 000000000..5426f0529
--- /dev/null
+++ b/examples/angular/multi-step-wizard/src/app/app.component.ts
@@ -0,0 +1,61 @@
+import { Component, signal } from '@angular/core'
+import {
+ TanStackWithForm,
+ injectForm,
+ injectStore,
+ revalidateLogic,
+} from '@tanstack/angular-form'
+import { z } from 'zod'
+import { Step1Component } from './step1.component'
+import { Step2Component } from './step2.component'
+import { step1Schema, step2Schema, wizardFormOpts } from './shared-form'
+
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [TanStackWithForm, Step1Component, Step2Component],
+ template: `
+ @if (step() === 0) {
+
+ }
+ @if (step() === 1) {
+
+ }
+ `,
+})
+export class AppComponent {
+ step = signal(0)
+
+ form = injectForm({
+ ...wizardFormOpts,
+ validationLogic: revalidateLogic(),
+ // onDynamic is only used when `form.handleSubmit` is called itself.
+ // When the FormGroup's `handleSubmit` is called, it will only validate the
+ // current step's schema. This means that this schema will not be called
+ // when the user submits the form group, but instead when they submit the
+ // entire form.
+ validators: {
+ onDynamic: z.object({
+ step1: step1Schema,
+ step2: step2Schema,
+ }),
+ },
+ onSubmit: ({ value }) => {
+ alert(`Form submitted: ${JSON.stringify(value)}`)
+ },
+ })
+
+ isSubmitting = injectStore(this.form, (state) => state.isSubmitting)
+}
diff --git a/examples/angular/multi-step-wizard/src/app/shared-form.ts b/examples/angular/multi-step-wizard/src/app/shared-form.ts
new file mode 100644
index 000000000..06440fc55
--- /dev/null
+++ b/examples/angular/multi-step-wizard/src/app/shared-form.ts
@@ -0,0 +1,21 @@
+import { formOptions } from '@tanstack/angular-form'
+import { z } from 'zod'
+
+export const step1Schema = z.object({
+ name: z.string().min(2, 'Name must be at least 2 characters'),
+})
+
+export const step2Schema = z.object({
+ name: z.string().min(3, 'Name must be at least 3 characters'),
+})
+
+export const wizardFormOpts = formOptions({
+ defaultValues: {
+ step1: {
+ name: '',
+ },
+ step2: {
+ name: '',
+ },
+ },
+})
diff --git a/examples/angular/multi-step-wizard/src/app/step1.component.ts b/examples/angular/multi-step-wizard/src/app/step1.component.ts
new file mode 100644
index 000000000..7a5d4d3e5
--- /dev/null
+++ b/examples/angular/multi-step-wizard/src/app/step1.component.ts
@@ -0,0 +1,69 @@
+import { Component, input, output } from '@angular/core'
+import {
+ TanStackAppField,
+ TanStackField,
+ TanStackFormGroup,
+ injectWithForm,
+} from '@tanstack/angular-form'
+import { TextFieldComponent } from './text-field.component'
+import { step1Schema, wizardFormOpts } from './shared-form'
+
+@Component({
+ selector: 'app-step1',
+ standalone: true,
+ imports: [
+ TanStackField,
+ TanStackAppField,
+ TanStackFormGroup,
+ TextFieldComponent,
+ ],
+ template: `
+
+
+
+ `,
+})
+export class Step1Component {
+ withForm = injectWithForm({ ...wizardFormOpts })
+ step = input.required()
+ isSubmitting = input.required()
+ stepChange = output()
+
+ step1Schema = step1Schema
+ stringify = (value: unknown) => JSON.stringify(value, null, 2)
+
+ onGroupSubmit = () => {
+ this.stepChange.emit(this.step() + 1)
+ }
+
+ onGroupSubmitInvalid = () => {
+ // Just like a form, you can also handle invalid submits at the group level,
+ // which is useful for multi-step wizards to prevent going to the next step
+ // if the current step is invalid
+ }
+}
diff --git a/examples/angular/multi-step-wizard/src/app/step2.component.ts b/examples/angular/multi-step-wizard/src/app/step2.component.ts
new file mode 100644
index 000000000..4e648966e
--- /dev/null
+++ b/examples/angular/multi-step-wizard/src/app/step2.component.ts
@@ -0,0 +1,61 @@
+import { Component, input, output } from '@angular/core'
+import {
+ TanStackAppField,
+ TanStackField,
+ TanStackFormGroup,
+ injectWithForm,
+} from '@tanstack/angular-form'
+import { TextFieldComponent } from './text-field.component'
+import { step2Schema, wizardFormOpts } from './shared-form'
+
+@Component({
+ selector: 'app-step2',
+ standalone: true,
+ imports: [
+ TanStackField,
+ TanStackAppField,
+ TanStackFormGroup,
+ TextFieldComponent,
+ ],
+ template: `
+
+
+
+ `,
+})
+export class Step2Component {
+ withForm = injectWithForm({ ...wizardFormOpts })
+ step = input.required()
+ isSubmitting = input.required()
+ stepChange = output()
+
+ step2Schema = step2Schema
+
+ onGroupSubmit = () => {
+ this.withForm.form.handleSubmit()
+ }
+}
diff --git a/examples/angular/multi-step-wizard/src/app/text-field.component.ts b/examples/angular/multi-step-wizard/src/app/text-field.component.ts
new file mode 100644
index 000000000..5dada6468
--- /dev/null
+++ b/examples/angular/multi-step-wizard/src/app/text-field.component.ts
@@ -0,0 +1,28 @@
+import { Component, input } from '@angular/core'
+import { injectField } from '@tanstack/angular-form'
+
+@Component({
+ selector: 'app-text-field',
+ standalone: true,
+ template: `
+
+
+ @for (error of field.api.state.meta.errors; track $index) {
+