Skip to content

Form Groups

Form groups are a way to structure related fields in a form. They allow you to group fields together and slice forms into smaller parts. This can be useful for organizing forms with many fields or for creating reusable group components that may be added to multiple forms.

Their main purpose is to slice forms into manageable parts. They are not nested forms; they do not submit data on their own. They are just a way to group fields together in a parent form.

Features

  • Multi-layered validation with both HTML attributes or Standard Schemas.
  • Aggregated state for validation, dirty, touched, and more.
  • Supports either fieldset and legend elements or any other HTML elements.
  • Automatic field name prefixing for organizing submitted data structure.

Anatomy

Shipping Address
Group Label
Street
City
Group Fields
Group

Creating a Form Group

To create a form group, you can use the useFormGroup composable. Just like fields, the useFormGroup composable returns values that should be bound to the anatomy elements.

Typically, you need to create a FormGroup component that you can use to structure your form fields. You can use fieldset as a base element for your FormGroup component or any other element that you prefer.

Form groups require a name prop, which will be used to nest the fields’ values in the form data. In the next examples, fill out the data and submit the form to see how the form data are structured.

With a fieldset element

<script setup lang="ts">
import { useForm } from '@formwerk/core';
import TextField from './TextField.vue';
import FormGroup from './FormGroup.vue';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => {
alert(JSON.stringify(data.toObject(), null, 2));
});
</script>
<template>
<form @submit="onSubmit" novalidate>
<TextField name="email" label="Email" type="email" required />
<FormGroup name="shipping" label="Shipping information">
<TextField name="address" label="Address" required />
<TextField name="city" label="City" required />
<TextField name="zip" label="ZIP" required />
</FormGroup>
<button type="submit">Submit</button>
</form>
</template>
<template>
<fieldset v-bind="groupProps">
<legend v-bind="labelProps">{{ label }}</legend>
<slot />
</fieldset>
</template>
<script setup lang="ts">
import { type FormGroupProps, useFormGroup } from '@formwerk/core';
const props = defineProps<FormGroupProps>();
const { labelProps, groupProps } = useFormGroup(props);
</script>
<style>
fieldset {
border: 1px solid #e0e0e0;
margin: 10px 0;
padding: 8px;
border-radius: 6px;
legend {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
</style>

Naturally, styling is entirely up to you. This can be much easier with other HTML elements like div, as shown in the next section.

With any HTML element

You can use any HTML element to create a form group. Formwerk’s useFormGroup bindings automatically change based on the element that is bound to it to ensure users get a consistent experience regardless of what element you choose to use.

Here’s an example using a div element:

<script setup lang="ts">
import { useForm } from '@formwerk/core';
import TextField from './TextField.vue';
import FormGroup from './FormGroup.vue';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => {
alert(JSON.stringify(data.toObject(), null, 2));
});
</script>
<template>
<form @submit="onSubmit" novalidate>
<TextField name="email" label="Email" type="email" required />
<FormGroup name="shipping" label="Shipping information">
<TextField name="address" label="Address" required />
<TextField name="city" label="City" required />
<TextField name="zip" label="ZIP" required />
</FormGroup>
<button type="submit">Submit</button>
</form>
</template>
<template>
<div v-bind="groupProps">
<h3 v-bind="labelProps">{{ label }}</h3>
<slot />
</div>
</template>
<script setup lang="ts">
import { type FormGroupProps, useFormGroup } from '@formwerk/core';
const props = defineProps<FormGroupProps>();
const { labelProps, groupProps } = useFormGroup(props);
</script>

Validation

The form guide mentioned briefly that groups can have their own validation Standard Schema just like forms do. They do not override the parent form schema; they are just an extension of it.

Formwerk prioritizes the validation sources in the following order:

  1. HTML Constraints are checked first; only if they are valid, continue to the next step.
  2. Field-level’s Standard Schema is checked next; only if it is valid, continue to the next step.
  3. Group-level’s Standard Schema is checked next; only if it is valid, continue to the next step.
  4. Form-level’s Standard Schema is checked last.
<script setup lang="ts">
import { useForm } from '@formwerk/core';
import TextField from './TextField.vue';
import FormGroup from './FormGroup.vue';
import { z } from 'zod';
const shippingSchema = z.object({
address: z.string().min(5),
city: z.string().min(3),
zip: z.string().length(5),
});
const schema = z.object({
email: z.string().email(),
});
const { handleSubmit } = useForm({
schema,
});
const onSubmit = handleSubmit((data) => {
alert(JSON.stringify(data.toObject(), null, 2));
});
</script>
<template>
<form @submit="onSubmit" novalidate>
<TextField name="email" label="Email" />
<FormGroup
name="shipping"
label="Shipping information"
:schema="shippingSchema"
>
<TextField name="address" label="Address" />
<TextField name="city" label="City" />
<TextField name="zip" label="ZIP" />
</FormGroup>
<button type="submit">Submit</button>
</form>
</template>

Group state

The group tracks and aggregates the fields’ state that are part of it. The following states are an aggregation of the fields that are part of the group:

NameTypeDescription
isDirtyBooleanIndicates whether any field within the group has been modified.
isValidBooleanIndicates whether all fields within the group pass their validation checks.
isTouchedBooleanIndicates whether any field within the group has been interacted with.

It also exposes the following getters and functions:

NameTypeDescription
getErrors() => IssueCollection[]Returns all errors within the group.
getValues() => Record<string, any>Returns all values within the group.
getError(name: string) => string | undefinedReturns the error for a given field within the group.
displayError(name: string) => string | undefinedDisplays the error for a given field within the group if the field was touched.
validate() => PromiseValidates all fields within the group.

Group Names and Nested Paths

A group can accept a name prop, which will prefix all the field names nested under it with that same name.

This means that when submitting the form, the data will be nested under the group name.

<script setup lang="ts">
import { useForm } from '@formwerk/core';
import TextField from './TextField.vue';
import FormGroup from './FormGroup.vue';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => {
alert(JSON.stringify(data.toObject(), null, 2));
});
</script>
<template>
<form @submit="onSubmit" novalidate>
<TextField name="field" label="Not Nested" />
<FormGroup name="group-1" label="Group 1">
<TextField name="field" label="Field 1 - Group 1" />
</FormGroup>
<FormGroup name="group-2" label="Group 2">
<TextField name="field" label="Field 1 - Group 2" />
</FormGroup>
<button type="submit">Submit</button>
</form>
</template>

API

Props

These are the properties that can be passed to the useFormGroup composable.

NameTypeDescription
disabledWhether the form group is disabled.
disableHtmlValidationWhether HTML5 validation should be disabled for this form group.
labelThe label for the form group.
nameThe name/path of the form group.
schemaThe validation schema for the form group.

Returns

These are the properties in the object returned by the useFormGroup composable.

NameTypeDescription
displayErrorDisplays an error for a given field.
groupElReference to the group element.
groupPropsProps for the group element.
isDirtyWhether the form group is dirty.
isDisabledWhether the form group is disabled.
isTouchedWhether the form group is touched.
isValidWhether the form group is valid.
labelPropsProps for the label element.
validate
() => Promise<GroupValidationResult<TOutput> & { type: "GROUP"; }>
Validates the form group.