baitendbidz
baitendbidz

Reputation: 825

Nested dialog form also submits parent form

Given a form using React Hook Form, MUI and zod. The form object contains a field that is an array of objects and the user can add/remove/edit the list items.

I moved the add/edit part into a separate dialog component with its own form ( derived from the parent form schema ) and when submitting that dialog it modifies the actual parent form data.

Unfortunately when submitting the dialog the parent form also submits, which is a undesired behaviour.


Playground: https://stackblitz.com/edit/vitejs-vite-pkux65vh?file=src%2FApp.tsx

The default values set the form into a valid state. Try to open the "add" dialog and try to submit this dialog with an invalid state.

You should see that the parent form still submits although the dialog form is invalid and should not bubble up to the parent form.


import { zodResolver } from '@hookform/resolvers/zod';
import {
  Button,
  TextField,
  Container,
  Alert,
  AlertTitle,
  Dialog,
  DialogContent,
  DialogActions,
} from '@mui/material';
import { Dispatch, SetStateAction, useState } from 'react';
import { type SubmitHandler, useForm, useFieldArray } from 'react-hook-form';
import { z } from 'zod';

const parentFormFieldsSchema = z.object({
  foo: z.string().min(1),
  bars: z
    .array(
      z.object({
        title: z.string().min(1),
      })
    )
    .min(1),
});

function App() {
  const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);

  const {
    register,
    handleSubmit,
    formState: { errors },
    control,
  } = useForm<z.infer<typeof parentFormFieldsSchema>>({
    resolver: zodResolver(parentFormFieldsSchema),
    defaultValues: {
      foo: 'foo',
      bars: [{ title: 'bar-1' }],
    },
  });

  const { fields: barFields, append: appendBar } = useFieldArray({
    control,
    name: 'bars',
    keyName: 'fieldID',
  });

  const onSubmit: SubmitHandler<z.infer<typeof parentFormFieldsSchema>> = (
    data
  ) => alert('parent form submit');

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField
        label="Foo"
        {...register('foo')}
        error={errors.foo !== undefined}
        helperText={errors.foo?.message}
      />
      <Container sx={{ margin: 8 }}>
        <Button onClick={() => setIsAddDialogOpen(true)}>Add bar</Button>
        <AddBarDialog
          isDialogOpen={isAddDialogOpen}
          setIsDialogOpen={setIsAddDialogOpen}
          onBarAdded={(newBar) => appendBar(newBar)}
        />
      </Container>
      <Container sx={{ margin: 8 }}>Bars:</Container>
      {barFields.map((barField) => (
        <Container key={barField.fieldID}>
          <Container>Title: {barField.title}</Container>
        </Container>
      ))}
      {errors.bars && (
        <Alert severity="error">
          <AlertTitle>Invalid bars</AlertTitle>
          {errors.bars?.message ?? ''}
        </Alert>
      )}
      <Button type="submit">Submit Parent form</Button>
    </form>
  );
}

interface AddBarDialogProps {
  isDialogOpen: boolean;
  setIsDialogOpen: Dispatch<SetStateAction<boolean>>;
  onBarAdded: (
    bar: z.infer<typeof parentFormFieldsSchema.shape.bars.element>
  ) => void;
}

function AddBarDialog({
  isDialogOpen,
  setIsDialogOpen,
  onBarAdded,
}: AddBarDialogProps) {
  const childFormFieldsSchema = parentFormFieldsSchema.shape.bars.element;

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<z.infer<typeof childFormFieldsSchema>>({
    resolver: zodResolver(childFormFieldsSchema),
    defaultValues: {
      title: '',
    },
  });

  const onSubmit: SubmitHandler<z.infer<typeof childFormFieldsSchema>> = (
    data
  ) => {
    onBarAdded(data);
    setIsDialogOpen(false);
  };

  return (
    <Dialog
      open={isDialogOpen}
      onClose={() => setIsDialogOpen(false)}
      PaperProps={{
        component: 'div',
      }}
    >
      <form onSubmit={handleSubmit(onSubmit)}>
        <DialogContent>
          <TextField
            label="Title"
            {...register('title')}
            error={errors.title !== undefined}
            helperText={errors.title?.message}
          />
        </DialogContent>
        <DialogActions>
          <Button type="submit">Submit Child form</Button>
        </DialogActions>
      </form>
    </Dialog>
  );
}

I thought I could solve it by changing the dialog submit handler to

const onSubmit: SubmitHandler<z.infer<typeof childFormFieldsSchema>> = (data, event) => {
  event?.preventDefault(); // Prevent default form submission
  event?.stopPropagation(); // Stop event from bubbling up
  onBarAdded(data);
  setIsDialogOpen(false);
};

but that didn't fix it. Any ideas?

Upvotes: 2

Views: 112

Answers (1)

Gabriele Petrioli
Gabriele Petrioli

Reputation: 196227

You should not nest form elements. It is not allowed by the spec

Content model: Flow content, but with no form element descendants.

And in MDN it states

Warning: It's strictly forbidden to nest a form inside another form.
Nesting can cause forms to behave unpredictably, so it is a bad idea.


Look in the useFieldArray documentation, where they have a "nested form" example of how to do it.

Upvotes: 3

Related Questions