ui icon indicating copy to clipboard operation
ui copied to clipboard

feat: function to trigger open/close dialog

Open madatbay opened this issue 1 year ago • 1 comments

Tried to search for it but didn't find a way to close the dialog with a method or function. This is useful after processing data like - making a fetch request, mutation, or something that is awaitable, or can change dialog state from outside In the example below, let's assume when the user clicks to "save changes", I want to handle some submit function as a result close dialog from that function.

... await updateData(data).then(res=>closeDialog()).catch(err=>setError(error))

Currently, the way I see is used after exporting Close from radix primitive. But this is a JSX element, cant be used as function to change dialog state

<DialogClose>Close<<DialogClose/>
Screenshot 2023-05-20 at 09 37 14

There is 2nd way to close Dialog by creating a state and binding it to open={open} prop in the Dialog component. We can open or close Dialog by changing that state. But when do so, the "X" button and clicking outside of the dialog to close it stops working

Why we need that feature?

I think this will solve the issue we have in Note section which asserts that we have to encase the trigger button to Dialog itself. I take this as a big problem because if I have a Dialog component, it's not possible to trigger it from multiple places. If I need to have that dialog in multiple places I need to copy/paste the same dialog code which is a duplication issue.

Sample use case: I have "Newsletter dialog" and I need to trigger this dialog with the button in the navbar, footer, or inside the main content. Or if the user stays for 5 mins on the website, I want to trigger that newsletter dialog

madatbay avatar May 20 '23 05:05 madatbay

If you want control the opening state by yourself, the open prop is the way to go, however, you also need to set the onOpenChange, so that the close button wiil able to trigger the onOpenChange event and change the state that you created.

Example:

const [open, setOpen] = useState(false);

return <Dialog open={open} onOpenChange={setOpen} />

Reference: https://www.radix-ui.com/docs/primitives/components/dialog#root

chungweileong94 avatar May 20 '23 10:05 chungweileong94

I am using Next13.4 app router in which all the components and pages are RSC and SSR respectively by default. I am rendering the Dialog in the custom component lets say AddToCartDialog.tsx in server component and want to close it programmatically something like handleCloseDialog(). I can't pass the prop state and setter function as React hooks do not work inside React Server Components. Is there clean way to do it? Or should I add a redundant client component wrapper for the dialogs?

OsamaQureshi147 avatar Sep 07 '23 11:09 OsamaQureshi147

@OsamaQureshi147 It seems like you need JS on client side to me tho, this is the time where you have to use client component instead.

chungweileong94 avatar Sep 07 '23 11:09 chungweileong94

@OsamaQureshi147 For a basic requirement, you can probably use the searchParams and do a condition like:

/some-page?productId=69&modal=true

export default function Page({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  const { productId, modal } = searchParams

  return (
    <div>
      <h1>Some Page</h1>
      <YourDialog open={modal === "true"} productId={productId} />
    </div>
  )
}

Then use <Link href="/some-page" /> to dismiss the modal.

Other than that, client component.

Demo:

https://github.com/shadcn-ui/ui/assets/13049130/d107ec0a-e6a4-45eb-a1be-839b74a5efc4

wobsoriano avatar Sep 29 '23 23:09 wobsoriano

@wobsoriano The problem is you can't use this approach if you're trying to open a modal from any components that generally live in an applications root layout such as a Navbar since search params are not available in that context, but I think it's good approach for subpages.

Source: https://nextjs.org/docs/app/api-reference/file-conventions/layout#layouts-do-not-receive-searchparams

msbeeman avatar Sep 30 '23 15:09 msbeeman

Right @msbeeman, I guess you can still use useSearchParams in the layout.

@OsamaQureshi147 this might be worth checking as well - Intercepting Routes

Example - https://nextjs-app-route-interception.vercel.app/

wobsoriano avatar Sep 30 '23 16:09 wobsoriano

@OsamaQureshi147 For a basic requirement, you can probably use the searchParams and do a condition like:

/some-page?productId=69&modal=true

export default function Page({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  const { productId, modal } = searchParams

  return (
    <div>
      <h1>Some Page</h1>
      <YourDialog open={modal === "true"} productId={productId} />
    </div>
  )
}

Then use <Link href="/some-page" /> to dismiss the modal.

Other than that, client component.

Demo:

Screen.Recording.2023-09-29.at.4.14.55.PM.mov

How can I make a required fields in the modal?

Steveb599 avatar Oct 19 '23 12:10 Steveb599

If you want control the opening state by yourself, the open prop is the way to go, however, you also need to set the onOpenChange, so that the close button wiil able to trigger the onOpenChange event and change the state that you created.

Example:

const [open, setOpen] = useState(false);

return <Dialog open={open} onOpenChange={setOpen} />

Reference: https://www.radix-ui.com/docs/primitives/components/dialog#root

thanks, not so clear in the doc!

zacBkh avatar Dec 16 '23 18:12 zacBkh

If you want control the opening state by yourself, the open prop is the way to go, however, you also need to set the onOpenChange, so that the close button wiil able to trigger the onOpenChange event and change the state that you created.

Example:

const [open, setOpen] = useState(false);

return <Dialog open={open} onOpenChange={setOpen} />

Reference: https://www.radix-ui.com/docs/primitives/components/dialog#root

Thank you very much, after searching and trying for couples of hours, I finally could reach it!

quyettranvu avatar Jan 08 '24 01:01 quyettranvu

I'm actually trying a different approach (https://github.com/chungweileong94/nextjs-parallel-route-dialog) by using the NextJS parallel route to control the dialog open state. The thing that I focus in my code example is to preserve the dialog closing animation.

chungweileong94 avatar Jan 16 '24 04:01 chungweileong94

This is how we can do, hope it helps

"use client";
// Imports here

export function AddProductModal() {
  const router = useRouter();
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button
          className="bg-red-50 text-red-600"
        >
          Add Product
        </Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
         // ADD PRODUCT FORM HERE
        <DialogFooter>
          <Button
            onClick={() => {
              addProduct(data).then(() => setOpen(false));
              router.refresh();
            }}
            className="bg-red-50 text-red-600 hover:bg-red-100"
            type="submit"
          >
            Add
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

sobitp59 avatar Jan 19 '24 08:01 sobitp59

I'm actually trying a different approach (https://github.com/chungweileong94/nextjs-parallel-route-dialog) by using the NextJS parallel route to control the dialog open state. The thing that I focus in my code example is to preserve the dialog closing animation.

I was doing it like you, but slightly differently. I changed my code to match your approach though and I think it's more manageable/closer to what I want. The thing is, the closing animation does work, but sometimes. I wonder if there is some sort of race condition that is causing it to sometimes not work?

jsaunders92 avatar Jan 24 '24 10:01 jsaunders92

I wonder if there is some sort of race condition that is causing it to sometimes not work?

Well, it works fine to fit my use-case. But I don't expect the close animation to work in cases like hard navigation. I do notice some limitation to the close animation, where the content of the dialog actually close instantly as soon as it triggers, but the animation is fast enough that you wouldn't notice it😬

You could try to use the browser history API with NextJS 14.1, to see if that helps, https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate, haven't try it myself personally.

But I will say if you really want to get the animation right, move things to client-side is the way to go.

chungweileong94 avatar Jan 24 '24 10:01 chungweileong94

You could try to use the browser history API with NextJS 14.1, to see if that helps, https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate, haven't try it myself personally.

I'll give this a go, I'll report back! I've already been messing around with the router.

But I will say if you really want to get the animation right, move things to client-side is the way to go. I think I much rather not. To the point, I'm willing to sacrifice the exit animation. Although I personally think it's easily doable. I mean it's like 90% there anyway already. I just need to work out what causes it to sometimes not work.

Either way, my code and then the changes based on your reorganisation of the folder structure based on your example has been really useful. So has Ariakit's Dialog with App Router documentation. I'll let you know if I get further with the exit animations.

jsaunders92 avatar Jan 24 '24 11:01 jsaunders92

If you want control the opening state by yourself, the open prop is the way to go, however, you also need to set the onOpenChange, so that the close button wiil able to trigger the onOpenChange event and change the state that you created.

Example:

const [open, setOpen] = useState(false);

return <Dialog open={open} onOpenChange={setOpen} />

Reference: https://www.radix-ui.com/docs/primitives/components/dialog#root

bruh, i suck i was try digging in wrong "dialogtrigger" wasted 3 days because of this. open dialog

nishaaanth2 avatar Feb 15 '24 16:02 nishaaanth2

{!form.formState.isValid ? ( <Button type="submit" className="w-full"> Save changes </Button> ) : ( <DialogClose asChild> <Button type="submit" className="w-full"> Save changes </Button> </DialogClose> )}

this worked for me :)

harsh7800 avatar Feb 28 '24 15:02 harsh7800

const [open, setOpen] = React.useState(false)

  const { reset } = form;
  const { isSubmitting, isSubmitSuccessful } = form.formState;

  React.useEffect(() => {
    isSubmitSuccessful && reset()

  }, [isSubmitSuccessful, reset])
<Dialog open={open} onOpenChange={setOpen}>

Dialog closing and form reset (react-hook-form) working properly with this. Didn't add anything like value={field.value} in the <Select onValueChange={field.onChange} defaultValue={field.value}> tag or anything in <SelectValue placeholder="Select priority" /> this tag.

saifurrahmantanvir avatar Mar 05 '24 16:03 saifurrahmantanvir

Very useful!

romhenri avatar Mar 21 '24 01:03 romhenri

This is how we can do, hope it helps

"use client";
// Imports here

export function AddProductModal() {
  const router = useRouter();
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button
          className="bg-red-50 text-red-600"
        >
          Add Product
        </Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
         // ADD PRODUCT FORM HERE
        <DialogFooter>
          <Button
            onClick={() => {
              addProduct(data).then(() => setOpen(false));
              router.refresh();
            }}
            className="bg-red-50 text-red-600 hover:bg-red-100"
            type="submit"
          >
            Add
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Does this only work on submit? I have been trying it in other parts of the code but it never works...

lvbn avatar Mar 26 '24 21:03 lvbn

This worked for me !!

"use client";
import React, { useEffect, useState } from "react";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/Components/ui/dialog";
import { Button } from "./ui/button";
import Image from "next/image";
import Search from "./Search";
import { usePathname } from "next/navigation";

function SearchDialog() {
  const [open, setOpen] = useState(false);
  const pathname = usePathname();
  useEffect(() => {
    setOpen(false);
  }, [pathname]);
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger className="z-10 cursor-pointer" asChild>
        <Image src="/search.svg" width={24} height={24} alt="search" />
      </DialogTrigger>
      <DialogContent className="rounded-xl">
        <DialogHeader>
          <DialogTitle>Share link</DialogTitle>
          <DialogDescription>
            Anyone who has this link will be able to view this.
            <DialogClose asChild>
              <Search />
            </DialogClose>
          </DialogDescription>
        </DialogHeader>
        <div className="flex items-center space-x-2"></div>
        <DialogFooter className="sm:justify-start">
          <DialogClose asChild>
            <Button type="button" variant="outline">
              Close
            </Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

export default SearchDialog;import React, { useEffect, useState } from "react";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/Components/ui/dialog";
import { Button } from "./ui/button";
import Image from "next/image";
import Search from "./Search";
import { usePathname } from "next/navigation";

function SearchDialog() {
  const [open, setOpen] = useState(false);
  const pathname = usePathname();
  useEffect(() => {
    setOpen(false);
  }, [pathname]);
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger className="z-10 cursor-pointer" asChild>
        <Image src="/search.svg" width={24} height={24} alt="search" />
      </DialogTrigger>
      <DialogContent className="rounded-xl">
        <DialogHeader>
          <DialogTitle>Share link</DialogTitle>
          <DialogDescription>
            Anyone who has this link will be able to view this.
            <DialogClose asChild>
              <Search />
            </DialogClose>
          </DialogDescription>
        </DialogHeader>
        <div className="flex items-center space-x-2"></div>
        <DialogFooter className="sm:justify-start">
          <DialogClose asChild>
            <Button type="button" variant="outline">
              Close
            </Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

export default SearchDialog;

SehajBindra avatar Mar 30 '24 19:03 SehajBindra

@OsamaQureshi147 For a basic requirement, you can probably use the searchParams and do a condition like:

/some-page?productId=69&modal=true

export default function Page({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  const { productId, modal } = searchParams

  return (
    <div>
      <h1>Some Page</h1>
      <YourDialog open={modal === "true"} productId={productId} />
    </div>
  )
}

Then use <Link href="/some-page" /> to dismiss the modal.

Other than that, client component.

Demo:

Screen.Recording.2023-09-29.at.4.14.55.PM.mov

I had a slightly different requirement to be able to open a modal using the normal DialogTrigger but also from my navigation bar component which was in my root layout. I was able use this approach with some slight adjustments to get it to work. This preserves all the nice animations for opening and closing the modal while allowing the modal to be opened or closed from anywhere in the component tree.

Code is here in-case anyone is curious: https://github.com/pnavk/nextjs-rsc-modal-dialog-example

pnavk avatar Apr 01 '24 02:04 pnavk

this issue should be re-opened, I have tried the approach of having a state variable with the [open, setOpen], when pressing cancel we need to reset the state to the state previous to opening the dialog, this state is being shared across dialog instanciations somehow, even setting a key prop didn't help, the behaviour of the cancel button is not the same as the "X" close button defaulted by the Dialog component but still both actions are buggy


'use client';

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger
} from '@/components/ui/dialog';
import { Textarea } from '../ui/textarea';

type InputDialogConfig = {
  triggerElement?: React.ReactNode;
  title?: string;
  description?: string;
  deleteLabel?: string;
  inputValue: string;
  onSave: (value: string) => void;
  onRemove?: () => void;
};

const CreateInputDialog = (config: InputDialogConfig) => {
  const {
    triggerElement = <Button variant="outline">Edit</Button>,
    title = 'Editing',
    description = null,
    deleteLabel = null,
    inputValue = '',
    onSave = () => {},
    onRemove = () => {}
  } = config;

  const [input, setInput] = useState(inputValue);
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>{triggerElement}</DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          {description && <DialogDescription>{description}</DialogDescription>}
        </DialogHeader>
        <div className="grid gap-4 py-4">
          <div className="grid grid-cols-4 items-center gap-4">
            <Textarea
              value={input}
              onChange={(e) => setInput(e.target.value)}
              className="col-span-4 h-[200px]"
            />
          </div>
        </div>
        <DialogFooter>
          {deleteLabel && (
            <Button
              type="button"
              variant="destructive"
              className="absolute left-6"
              onClick={(e) => {
                e.stopPropagation();
                onRemove();
                setOpen(false);
              }}
            >
              {deleteLabel}
            </Button>
          )}

          <Button
            type="button"
            variant={'outline'}
            onClick={(e) => {
              e.stopPropagation();
              setInput(inputValue);
              setOpen(false);
            }}
          >
            Cancel
          </Button>

          <Button
            type="button"
            onClick={(e) => {
              e.stopPropagation();
              onSave(input);
              setOpen(false);
            }}
          >
            Save
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};

export default CreateInputDialog;

please use this as an example and instantiate it twice on a higher order component, pass it a function to save, no need to test the delete as this has no possible issues, and after setting the two vars to different values and switching from one to other instanciation you will see the value being brought from one instantiation to another.

https://www.loom.com/share/2989c845f661479e9d6e1d7bd1072437?sid=fb91d584-3929-4796-8ebb-725114b9748a

thebadking avatar May 23 '24 12:05 thebadking

@thebadking It would be good if you could provide a small repro, it seems to me it's some state logic problem from upstream. By creating a small repro, you might somehow figure out what actually went wrong when the repro codebase is relatively small🙂

chungweileong94 avatar May 23 '24 13:05 chungweileong94

@chungweileong94 will do, just finishing some refactoring that hopefully will fix it

thebadking avatar May 24 '24 09:05 thebadking

Whoever came here looking for answers wraps the element that triggers close with <DailogClose />. It extends the close primitive under the hood and does the job perfectly

<DialogClose>
    <Button variant="outline">Cancel</Button>
</DialogClose>

shrey-v0 avatar Jun 26 '24 09:06 shrey-v0