Skip to content

[Feature Request]: Form Groups #419

@crutchcorn

Description

@crutchcorn

Description

When building a form stepper, like so:

image

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>
    );
};

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions