[Feature Request]: Form Groups
Description
When building a form stepper, 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.
Ideally, it would be nice to have a FormGroup where you could validate the group, but not the form itself - submit the value and move on to the next step.
API Proposal
// ...
const formOpts = formOptions({
defaultValues: {
step1: {
name: "",
},
step2: {
name: "",
},
},
})
const Step2Form = withForm({
...formOpts,
render: function Render({ form }) {
return (
<form.FormGroup
name="step2"
onGroupSubmit={({ value: _value }) => {
form.handleSubmit();
}}
>
{(formGroup) => (
<form onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
formGroup.handleSubmit();
}}>
<form.AppField
name="step2.name"
>
{field => (
<field.TextField />
)}
</form.AppField>
<button type="submit">Next</button>
</form>
)}
</form.FormGroup>
)
},
})
export const StepperForm = () => {
const [step, setStep] = useState(0);
const form = useAppForm({
...formOpts,
validationLogic: revalidateLogic(),
validators: {
onDynamic: z.object({
step1: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
}),
// Will run when `step2` group is submitted or the whole form is submitted.
// When `step2` group is submitted, it will run the form's validators, then throw aways errors on `step1`
step2: z.object({
name: z.string().min(3, "Name must be at least 3 characters"),
}),
})
},
onSubmit: ({ value }) => {
console.log("Form submitted:", value);
}
});
return (
<div>
{step === 1 && (
// FormGroup internally provides a sub-form context for its children including a `doNotValidate` flag to disable the parent form's validation on field changes
<form.FormGroup
name="step1"
validators={{
// If `validators` are defined on the FormGroup, they will disable the parent form's validators for this group's `onGroupSubmit`
// Only required for async or for performance optimizations on sync validations
onDynamic: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
})
}}
onGroupSubmit={({ value: _value }) => {
setStep(step + 1);
}}
onGroupSubmitInvalid={() => { }}
>
{(formGroup) => (
<form onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
formGroup.handleSubmit();
}}>
{/* Then, Field component consumes `sub-form` context and enables us to pass options to `FieldApi` */}
<form.AppField
name="step1.name"
>
{field => (
<field.TextField />
)}
</form.AppField>
<button type="submit">Next</button>
{/* formGroup contains errorMaps and errors, just like forms and fields */}
<pre>{JSON.stringify(formGroup.state.errorMap, null, 2)}</pre>
</form>
)}
</form.FormGroup>
)}
{/* Can even extract it using `formGroup` */}
{step === 2 && (
<Step2Form form={form} />
)}
</div>
);
};
Just wanted to leave some additional context: We sometimes have quite complex forms that have a lot of fields that are only visible on a certain condition, say for example a billing and a delivery address, they are basically the same, but one is only used under the condition that the two are not the same. So FormGroups can be a great way to structure a form as well.
+1
At AWS, a common pattern we implement is a “Wizard” pattern which guides a user through a multi-step form.
Example: https://cloudscape.design/examples/react/wizard.html
I'm currently working on a multi-step form, similar to a wizard. The proposed form.FormGroup component would simplify the code a lot.
@timothyac we're not quite ready for this feature yet as part of our 1.x release, but we'll work on it soon after!
That said, I'd love to help the AWS team figure out how to integrate Cloudscape with TanStack Form when it's ready. If y'all need any help with anything, let me know (even via DMs - they're open)
Any update on the feature? Or a way to make this work?