Skip to main content

Multiple subcomponents

When dealing with a big form, it is useful to group some fields and break them up into multiple subcomponents, then handle them with useFormikContext

1. Main form component#

These will render the main formik component, the form sections via subcomponents and usually also the submit button section.

components/payment-types/details/PaymentTypeForm.tsx
export interface IPaymentType {  Name: string;  Code: string;}
export const PaymentTypeForm = () => {  const intl = useIntl();  const paymentType = useRecoilValue(getPaymentTypeState.response);
  // Initial form values when loading the form  const initialValues: IPaymentType = useMemo(    () => ({      ID: paymentType?.ID,      Name: paymentType?.Name,      Code: paymentType?.Code,      PaymentMethodID: paymentType?.PaymentMethodID,      BackendRelationID: paymentType?.BackendRelationID,      OrganizationUnitSetID: paymentType?.OrganizationUnitSetID,      CashJournalMethod: paymentType?.CashJournalMethod,      ReturnAction: paymentType?.ReturnAction,      LedgerClassID: paymentType?.LedgerClassID,      CaptureMoment: paymentType?.CaptureMoment,      Category: paymentType?.Category,      AutoFinalizeOnOrderPaid: paymentType?.AutoFinalizeOnOrderPaid,      IsRoundingType: paymentType?.IsRoundingType,      PrintOnDocuments: paymentType?.PrintOnDocuments,      IsActive: paymentType?.IsActive,      Options: {        ...paymentType?.Options,      },    }),    [paymentType],  );
  // Callback to save the form values in the backend and a loading state which indicates that a save is in progress  const { savePaymentType, isSaveLoading } = useAddPaymentMethod();
  return (    <Card>      <Formik        initialValues={initialValues}        enableReinitialize        onSubmit={(formValues, formikHelpers) => {          savePaymentType(formValues);          formikHelpers.setSubmitting(false);        }}      >        {({ submitForm, isSubmitting }) => (          <>            <PaymentTypeGeneralDetails />            <PaymentTypeOptionsDetails />            <Box>              <Grid container spacing={3} justify="flex-end">                <Grid item>                  <Button                    variant="contained"                    color="primary"                    isLoading={isSubmitting || isLoading}                    disabled={isSubmitting || isLoading}                    onClick={submitForm}                  >                    <FormattedMessage id="button.label.save" />                  </Button>                </Grid>              </Grid>            </Box>          </>        )}      </Formik>    </Card>  );};

We can render as many subcomponents as we want. We can also group them in Accordions or Cards or whatever the design might be.

2. Create form subcomponents#

These will each handle only part of the form and have access to the formik state via useFormikContext.

"components/payment-types/details/PaymentTypeGeneralDetails.tsx
const PaymentTypeGeneralDetails = () => {  const intl = useIntl();
  const { values, setFieldValue, setFieldTouched } =    useFormikContext<IPaymentType>();
  const organizationUnitSets = useOrganizationUnitSets();
  ...
  return (    <Box p={0}>      <Box p={0} mb="8px">        <Field name="Name">          {({ meta, field }: FieldProps) => (            <Input              required              label={intl.formatMessage({                id: "payment-types.details.general-settings.label.name",              })}              {...field}              value={field.value ?? ""}              error={meta.touched && !!meta.error}              helperText={meta.touched && !!meta.error ? meta.error : undefined}              endIcon={                <ContextualHelpIcon                  label={intl.formatMessage({                    id: "payment-types.details.general-settings.label.name",                  })}                  text={intl.formatMessage({                    id: "payment-types.details.general-settings.label.name.help",                  })}                />              }            />          )}        </Field>      </Box>      <Box p={0} mb="8px">        <Field name="Code">          {({ meta, field }: FieldProps) => (            <Input              required              label={intl.formatMessage({                id: "payment-types.details.general-settings.label.code",              })}              {...field}              value={field.value ?? ""}              error={meta.touched && !!meta.error}              helperText={meta.touched && !!meta.error ? meta.error : undefined}              endIcon={                <ContextualHelpIcon                  label={intl.formatMessage({                    id: "payment-types.details.general-settings.label.code",                  })}                  text={intl.formatMessage({                    id: "payment-types.details.general-settings.label.code.help",                  })}                />              }            />          )}        </Field>      </Box>      <Box p={0} mb="8px">        <Field name="OrganizationUnitSetID">          {({ meta, field }: FieldProps) => (            <Autocomplete              {...field}              required={!field.value}              label={intl.formatMessage({                id: "payment-types.details.general-settings.label.organization-unit-set-id",              })}              items={organizationUnitSets ?? []}              matchKeys={["Name", "ID"]}              optionIDKey="ID"              renderOptionValueKey="Name"              controlledSelectedItem={                organizationUnitSets?.find(                  (item) => item.ID === values?.OrganizationUnitSetID                ) ?? ""              }              handleSelectedItem={(selected) => {                setFieldValue("OrganizationUnitSetID", selected?.ID);                setFieldTouched("OrganizationUnitSetID", true, false);              }}              error={!!meta.error && meta.touched}              helperText={!!meta.error && meta.touched ? meta.error : undefined}            />          )}        </Field>      </Box>      ...      <Box p={0} mb="8px">        <Field name="PrintOnDocuments">          {(props: FieldProps) => (            <Checkbox              {...props.field}              checked={props.field.value ?? false}              onChange={(event) =>                setFieldValue("PrintOnDocuments", event.target.checked)              }            >              <FormattedMessage id="payment-types.details.general-settings.label.print-on-documents" />            </Checkbox>          )}        </Field>      </Box>      <Box p={0} mb="8px">        <Field name="IsActive">          {(props: FieldProps) => (            <Checkbox              {...props.field}              checked={props.field.value ?? false}              onChange={(event) =>                setFieldValue("IsActive", event.target.checked)              }            >              <FormattedMessage id="payment-types.details.general-settings.label.is-active" />            </Checkbox>          )}        </Field>      </Box>    </Box>  );};
"components/payment-types/details/PaymentTypeOptionsDetails.tsx
const PaymentTypeOptionsDetails = () => {  const intl = useIntl();
  const { values, setFieldValue } = useFormikContext<IPaymentType>();
  const roleOptions = useUserRoleOptions();
  ...
  return (    <Box p={0}>      ...      <Box p={0} mb="8px">        <Field name="Options.RequiredUserType">          {({ meta, field }: FieldProps) => (            <Select              {...field}              items={roleOptions ?? []}              value={                field.value !== undefined && roleOptions?.length                  ? field.value                  : ""              }              onChange={(event) =>                setFieldValue("Options.RequiredUserType", event.target.value)              }              name="roles"              label={intl.formatMessage({                id: "payment-types.details.options.label.role",              })}              fullWidth              error={!!meta.error && meta.touched}              helpertext={!!meta.error && meta.touched ? meta.error : undefined}            />          )}        </Field>      </Box>      ...      <Box p={0} mb="8px">        <Field name="Options.CanBeCancelled">          {(props: FieldProps) => (            <Checkbox              {...props.field}              checked={props.field.value ?? false}              onChange={(event) =>                setFieldValue("Options.CanBeCancelled", event.target.checked)              }            >              <FormattedMessage id="payment-types.details.options.label.can-be-cancelled" />            </Checkbox>          )}        </Field>      </Box>    </Box>  );};