ui
ui copied to clipboard
feat: function to trigger open/close dialog
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/>
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
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
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 It seems like you need JS on client side to me tho, this is the time where you have to use client component instead.
@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 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
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/
@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?
If you want control the opening state by yourself, the
open
prop is the way to go, however, you also need to set theonOpenChange
, so that the close button wiil able to trigger theonOpenChange
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!
If you want control the opening state by yourself, the
open
prop is the way to go, however, you also need to set theonOpenChange
, so that the close button wiil able to trigger theonOpenChange
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!
Might help some - adapting shadcn/ui dialog for parallel and intercepting routes
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.
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>
);
}
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?
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.
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.
If you want control the opening state by yourself, the
open
prop is the way to go, however, you also need to set theonOpenChange
, so that the close button wiil able to trigger theonOpenChange
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
{!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 :)
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.
Very useful!
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...
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;
@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
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 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 will do, just finishing some refactoring that hopefully will fix it
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>