form icon indicating copy to clipboard operation
form copied to clipboard

[Feature Request]: Form Groups

Open crutchcorn opened this issue 2 years ago • 26 comments

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

crutchcorn avatar Aug 30 '23 14:08 crutchcorn

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.

Christian24 avatar Oct 30 '23 18:10 Christian24

+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

timothyac avatar Dec 31 '23 08:12 timothyac

I'm currently working on a multi-step form, similar to a wizard. The proposed form.FormGroup component would simplify the code a lot.

francesconi avatar Jan 02 '24 08:01 francesconi

@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)

crutchcorn avatar Jan 09 '24 01:01 crutchcorn

Any update on the feature? Or a way to make this work?

samuellawerentz avatar Jun 07 '24 04:06 samuellawerentz