Submitting dialog form also submits main form #12212
-
I have a BlogPost editor component that renders an editor with three fields. The issueI reload a page and click on "Insert Image" button. It opens the dialog and I can upload an image. As expected. Shadcn uses Portal for rendering Dialog so this is not nested form. Expected behaviourThe upload image button should not submit the blog post. BlogpostEditor.tsx // ... Imports and type declaration
const BlogPostEditor = ({ username }: BlogPostEditorProps) => {
const form = useForm<z.infer<typeof BlogPostEditorSchema>>({
resolver: zodResolver(BlogPostEditorSchema),
defaultValues: {
title: "",
slug: "",
},
});
const editor = useEditor({
// Editor configuration
});
const onSubmit = async (data: z.infer<typeof BlogPostEditorSchema>) => {
const response = await fetch("/api/posts", {
method: "POST",
body: JSON.stringify({ ...data, author: username }),
});
const result = await response.json();
if (!result.success) // Handle error
if (result.success) // Handle success
};
if (!editor) {
return null;
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex justify-end mb-3">
<Button type="submit">Publish</Button>
</div>
<div className="grid gap-10">
<FormField
control={form.control}
name="title"
render={({ field, formState }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input
{...field}
onFocus={() => {
console.log(formState.touchedFields);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<Tiptap editor={editor} />
</div>
</div>
</form>
</Form>
);
}; Tiptap.tsx // ... Imports and type declaration
const Tiptap = ({ editor }: TiptapProps) => {
if (!editor) {
return null;
}
return (
<>
<Toolbar editor={editor} insertImage={insertImage} />
<EditorContent editor={editor} />
</>
);
};
const Toolbar = ({
editor,
insertImage,
}: {
editor: Editor;
insertImage: (src: string) => boolean | undefined;
}) => {
return (
<div className="border rounded-t-md flex gap-2 items-center py-0.5 px-0.5">
<SelectTextTypeDropdown editor={editor} />
<div>
// Other buttons
</div>
{/* The problem is here */}
<InsertImageDialog insertImage={insertImage} />
</div>
);
};
const InsertImageFormSchema = z.object({
image: z.instanceof(FileList),
});
interface InsertImageDialogProps {
insertImage: (src: string) => boolean | undefined;
}
const InsertImageDialog = ({ insertImage }: InsertImageDialogProps) => {
const form = useForm<z.infer<typeof InsertImageFormSchema>>({
resolver: zodResolver(InsertImageFormSchema),
});
const [isLoading, setIsLoading] = useState(false);
const imageRef = form.register("image");
const prepareFormData = (data: z.infer<typeof InsertImageFormSchema>) => {
const formdata = new FormData();
formdata.append("image", data.image[0]);
return formdata;
};
const uploadImage = async (formData: FormData) => {
const response = await fetch("/api/assets/images", {
method: "POST",
body: formData,
});
const result = await response.json();
return result;
};
const handleResponse = (result: any) => {
if (result.success) {
insertImage(result.imageURL);
}
setIsLoading(false);
};
const onSubmit = async (data: z.infer<typeof InsertImageFormSchema>) => {
console.log(data);
setIsLoading(true);
const formData = prepareFormData(data);
const result = await uploadImage(formData);
handleResponse(result);
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Insert image</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>Select image</DialogHeader>
<div className="my-3">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid gap-3">
<FormField
control={form.control}
name="image"
render={() => (
<FormItem>
<FormLabel>Select Image</FormLabel>
<FormControl>
<Input type="file" {...imageRef} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading}>
{!isLoading ? "Upload" : "Uploading..."}
</Button>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
); |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
Hello, I had the same issue with radix and RHF. Even tho the child components, which also contained forms themselves themselves, were rendered in portals outside the parent form, they were somehow still treated as nested forms. Since the submit event bubbles up, the parent form's submit handler was also called when the child forms were submitted. I worked around the issue by stopping propagation of the submit events in the children like so: InsertImageDialog example: <form onSubmit={(event) => {
event?.stopPropagation();
form.handleSubmit(onSubmit)(event);
}}>
....
</form> Cheers |
Beta Was this translation helpful? Give feedback.
-
Another solution is to stop it from bubbling up at the modal or dialog. That way you don't have to worry about it every time you make a child form. |
Beta Was this translation helpful? Give feedback.
Hello, I had the same issue with radix and RHF. Even tho the child components, which also contained forms themselves themselves, were rendered in portals outside the parent form, they were somehow still treated as nested forms. Since the submit event bubbles up, the parent form's submit handler was also called when the child forms were submitted. I worked around the issue by stopping propagation of the submit events in the children like so:
InsertImageDialog example:
Cheers