# Form Groups

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

![Form stepper](https://raw.githubusercontent.com/TanStack/form/main/docs/assets/stepper.png)

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.FormGroup>`.

## 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
<script lang="ts">
  import { createForm } from '@tanstack/svelte-form'

  const form = createForm(() => ({
    defaultValues: {
      step1: {
        name: '',
      },
      step2: {
        age: 0,
      },
    },
  }))
</script>

<form.FormGroup name="step1">
  {#snippet children(group)}
    <!-- `group` here has all of the form-like methods you'd expect like `deleteField` or `insertFieldValue` -->
    <!-- ... -->
  {/snippet}
</form.FormGroup>
```

This becomes much more useful when paired with external state to conditionally render a `FormGroup`:

```svelte
<script lang="ts">
  import { createForm } from '@tanstack/svelte-form'

  let step = $state(0)

  const form = createForm(() => ({
    defaultValues: {
      step1: {
        name: '',
      },
      step2: {
        age: 0,
      },
    },
  }))
</script>

{#if step === 0}
  <form.FormGroup
    name="step1"
    onGroupSubmit={() => {
      // 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)}
      <form
        onsubmit={(e) => {
          e.preventDefault()
          e.stopPropagation()
          // Use `group.handleSubmit()` to submit the sub-form, but not the parent form
          group.handleSubmit()
        }}
      >
        <!-- ... -->
      </form>
    {/snippet}
  </form.FormGroup>
{:else if step === 1}
  <form.FormGroup name="step2" onGroupSubmit={() => form.handleSubmit()}>
    {#snippet children(group)}
      <form
        onsubmit={(e) => {
          e.preventDefault()
          e.stopPropagation()
          group.handleSubmit()
        }}
      >
        <!-- Then, use `form.handleSubmit()` from `onGroupSubmit` to submit the entire form -->
        <!-- ... -->
      </form>
    {/snippet}
  </form.FormGroup>
{/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
<form.FormGroup name="step1" validators={{ onChange: () => 'Error' }}>
  {#snippet children(group)}
    <!-- group.state.meta.errorMap // {onChange: "Error" | undefined} -->
    <!-- group.state.meta.errors // ("Error")[] -->
  {/snippet}
</form.FormGroup>
```

- Can set errors on sub-fields:

```svelte
<form.FormGroup
  name="step1"
  validators={{
    onChange: ({ value, groupApi }) => ({
      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
<form.FormGroup
  name="step1"
  validators={{
    onChange: z.object({
      name: z.string().min(2),
    }),
  }}
/>
```

> 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
<form.FormGroup name="step1" validators={{ onDynamic: step1Schema }} />
```

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
