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?
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.