shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
74.45k stars 4.6k forks source link

[Bug]: Triggering a form within a sheet that’s embedded in form on a different sheet #5768

Open ChoaibMouhrach opened 6 days ago

ChoaibMouhrach commented 6 days ago

Describe the bug

I have a sheet with a form, and another sheet with its own form. Submitting the form on the second sheet triggers the form on the first sheet.

Create product


"use client";

import { UnitsInput } from "@/client/components/custom/units-input";
import { Button } from "@/client/components/ui/button";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/client/components/ui/form";
import { Input } from "@/client/components/ui/input";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/client/components/ui/sheet";
import { Textarea } from "@/client/components/ui/textarea";
import { SOMETHING_WENT_WRONG } from "@/common/constants";
import { createProductSchema } from "@/common/validations";
import { createProductAction } from "@/server/actions/product";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAction } from "next-safe-action/hooks";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

const schema = createProductSchema;
type Payload = z.infer<typeof schema>;

export const Create = () => {
  const router = useRouter();

  const form = useForm<Payload>({
    resolver: zodResolver(schema),
    values: {
      name: "",
      unitId: "",
      price: 1,
      shortDescription: "",
      description: "",
    },
  });

  const createProduct = useAction(createProductAction, {
    onSuccess: () => {
      toast.success("Product created successfully");
      form.reset();
      router.refresh();
    },
    onError: ({ error }) => {
      toast.error(error.serverError || SOMETHING_WENT_WRONG);
    },
  });

  const onSubmit = (payload: Payload) => {
    createProduct.execute(payload);
  };

  return (
    <Sheet>
      <SheetTrigger asChild>
        <Button size="sm" variant="outline">
          New product
        </Button>
      </SheetTrigger>
      <SheetContent className="flex flex-col overflow-y-auto">
        <SheetHeader>
          <SheetTitle>New product</SheetTitle>
          <SheetDescription>
            You can add new products from here.
          </SheetDescription>
        </SheetHeader>
        <Form {...form}>
          <form
            className="flex flex-col gap-4"
            onSubmit={form.handleSubmit(onSubmit)}
          >
            <FormField
              name="name"
              control={form.control}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Name</FormLabel>
                  <FormControl>
                    <Input {...field} placeholder="Fearux" />
                  </FormControl>
                  <FormDescription>The name of the product.</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              name="unitId"
              control={form.control}
              render={() => (
                <FormItem>
                  <FormLabel>UnitId</FormLabel>
                  <FormControl>
                    <UnitsInput />
                  </FormControl>
                  <FormDescription>The unit of the product.</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              name="price"
              control={form.control}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Price</FormLabel>
                  <FormControl>
                    <Input
                      {...field}
                      onChange={(e) =>
                        field.onChange(parseFloat(e.target.value))
                      }
                      placeholder="Fearux"
                      type="number"
                      min="0.001"
                      step="0.001"
                    />
                  </FormControl>
                  <FormDescription>The price of the product.</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              name="shortDescription"
              control={form.control}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Short description</FormLabel>
                  <FormControl>
                    <Input {...field} placeholder="This product is..." />
                  </FormControl>
                  <FormDescription>
                    The short description of the product.
                  </FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              name="description"
              control={form.control}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Description</FormLabel>
                  <FormControl>
                    <Textarea
                      {...field}
                      placeholder="This product is..."
                      rows={8}
                    />
                  </FormControl>
                  <FormDescription>
                    The description of the product.
                  </FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />

            <div>
              <Button type="submit" pending={createProduct.isPending}>
                Add
              </Button>
            </div>
          </form>
        </Form>
      </SheetContent>
    </Sheet>
  );
};

Units input

import { Plus } from "lucide-react";
import { Button } from "../ui/button";
import { Combobox } from "../ui/combobox";
import { UnitsCreate } from "./units-create";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { getUnitsAction } from "@/server/actions/unit";
import { CustomError } from "@/server/lib/action";
import { SOMETHING_WENT_WRONG } from "@/common/constants";
import { Skeleton } from "../ui/skeleton";
import debounce from "debounce";

export const UnitsInput = () => {
  const [query, setQuery] = useState("");

  const unitsQuery = useQuery({
    queryKey: ["units", query],
    placeholderData: (phd) => phd,
    queryFn: async () => {
      const response = await getUnitsAction({
        query,
        page: 1,
      });

      if (!response?.data) {
        throw new CustomError(response?.serverError || SOMETHING_WENT_WRONG);
      }

      return response.data.data;
    },
  });

  const onQuery = debounce(setQuery, 300);

  if (unitsQuery.isSuccess) {
    return (
      <div className="flex items-center gap-2">
        <Combobox
          onQuery={onQuery}
          className="flex-1"
          items={unitsQuery.data.map((unit) => ({
            label: unit.name,
            value: unit.id,
          }))}
        />
        <UnitsCreate refetch={() => unitsQuery.refetch()}>
          <Button size="icon" variant="outline" className="shrink-0">
            <Plus className="w-4 h-4" />
          </Button>
        </UnitsCreate>
      </div>
    );
  }

  return <Skeleton className="h-9" />;
};

Create unit


"use client";

import { Button } from "@/client/components/ui/button";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/client/components/ui/form";
import { Input } from "@/client/components/ui/input";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/client/components/ui/sheet";
import { SOMETHING_WENT_WRONG } from "@/common/constants";
import { createUnitSchema } from "@/common/validations";
import { createUnitAction } from "@/server/actions/unit";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAction } from "next-safe-action/hooks";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

const schema = createUnitSchema;
type Payload = z.infer<typeof schema>;

interface UnitsCreateProps {
  children: React.ReactNode;
  refetch?: () => void;
}

export const UnitsCreate: React.FC<UnitsCreateProps> = ({
  children,
  refetch,
}) => {
  const router = useRouter();

  const form = useForm<Payload>({
    resolver: zodResolver(schema),
    values: {
      name: "",
    },
  });

  const createUnit = useAction(createUnitAction, {
    onSuccess: () => {
      toast.success("Unit created successfully");
      (refetch || router.refresh)();
      form.reset();
    },
    onError: ({ error }) => {
      toast.error(error.serverError || SOMETHING_WENT_WRONG);
    },
  });

  const onSubmit = (payload: Payload) => {
    createUnit.execute(payload);
  };

  return (
    <Sheet>
      <SheetTrigger asChild>{children}</SheetTrigger>
      <SheetContent className="flex flex-col" side="left">
        <SheetHeader>
          <SheetTitle>New unit</SheetTitle>
          <SheetDescription>You can add new units from here.</SheetDescription>
        </SheetHeader>
        <Form {...form}>
          <form
            className="flex flex-col gap-4"
            onSubmit={form.handleSubmit(onSubmit)}
          >
            <FormField
              name="name"
              control={form.control}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Name</FormLabel>
                  <FormControl>
                    <Input {...field} placeholder="kg" />
                  </FormControl>
                  <FormDescription>The name of the unit.</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />

            <div>
              <Button type="submit" pending={createUnit.isPending}>
                Add
              </Button>
            </div>
          </form>
        </Form>
      </SheetContent>
    </Sheet>
  );
};

https://github.com/user-attachments/assets/f18ed23e-f9fb-48bd-8eed-b5af3ed9c360

Affected component/components

sheet, form, button

How to reproduce

render a sheet with a form inside and inside that form render another sheet with another form inside and try submiting the second form and see that the first one triggers

Codesandbox/StackBlitz link

No response

Logs

No response

System Info

MBP m3 pro
sequoia 15.1
Google chrome Version 130.0.6723.92 (Official Build) (arm64)

Before submitting

Sparticuz commented 1 day ago

Also having issues with this. I don't think it's related to it being in a sheet because mine are in dialogs, but they forms are still 'nested' even though both the sheet component and dialog component uses portal to attach them to document.body. I'm not sure what the reason is.

Sparticuz commented 1 day ago

I'm wondering if this is related. https://github.com/radix-ui/primitives/issues/3199