Skip to content

Instantly share code, notes, and snippets.

@adnanalbeda
Last active March 13, 2025 10:23
Show Gist options
  • Save adnanalbeda/12d6fbe8a40d1a79a0ca9e772b0a3863 to your computer and use it in GitHub Desktop.
Save adnanalbeda/12d6fbe8a40d1a79a0ca9e772b0a3863 to your computer and use it in GitHub Desktop.
React-RadixUI
import {
Children,
cloneElement,
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import { Slot, SlotProps } from "@radix-ui/react-slot";
import { DialogProps } from "@radix-ui/react-dialog";
type Maybe<T> = T | null | undefined;
const MultiDialogContainerContext = createContext<unknown>(null);
MultiDialogContainerContext.displayName = "MultiDialogContainerContext";
export function useMultiDialog<T = unknown>(): [
Maybe<T>,
React.Dispatch<React.SetStateAction<Maybe<T>>>,
];
export function useMultiDialog<T = unknown>(
v: T,
): [boolean, (v: boolean) => void];
export function useMultiDialog<T = unknown>(v?: T) {
const s = useContext(MultiDialogContainerContext) as [
Maybe<T>,
React.Dispatch<React.SetStateAction<Maybe<T>>>,
];
if (!s)
throw new Error(
"Cannot use 'useMultiDialog' outside 'MultiDialogProvider'.",
);
if (v == null) return s;
const [dialog, setDialog] = s;
const onOpenChange = useCallback(
(o: boolean) => (o ? setDialog(v) : setDialog(null)),
[v],
);
const open = dialog === v;
const result = useMemo(() => [open, onOpenChange] as const, [open]);
return result;
}
export function MultiDialogTrigger<T = unknown>({
value,
onClick,
...props
}: SlotProps &
React.RefAttributes<HTMLElement> & {
value: T;
}) {
const [, open] = useMultiDialog(value);
const oc = useCallback<React.MouseEventHandler<HTMLElement>>(
(e) => {
open(true);
onClick && onClick(e);
},
[value, onClick],
);
return <Slot onClick={oc} {...props} />;
}
export function MultiDialogContainer<T = unknown>({
value,
children,
...props
}: Omit<DialogProps, "open" | "onOpenChange"> & {
value: T;
children?: JSX.Element;
}) {
const [open, onOpenChange] = useMultiDialog(value);
return useMemo(() => {
Children.only(children);
return children
? cloneElement(children, {
...props,
open,
onOpenChange,
})
: null;
}, [children, open]);
}
type Builder<T = unknown> = {
readonly Trigger: (
...args: Parameters<typeof MultiDialogTrigger<T>>
) => React.ReactNode;
readonly Container: (
...args: Parameters<typeof MultiDialogContainer<T>>
) => React.ReactNode;
};
const builder = {
Trigger: MultiDialogTrigger,
Container: MultiDialogContainer,
} as const;
export type MultiDialogBuilder<T = unknown> = (
builder: Builder<T>,
) => React.ReactNode;
export function MultiDialog<T = unknown>({
defaultOpen = null,
children,
}: {
defaultOpen?: T | null;
children?: React.ReactNode | MultiDialogBuilder<T>;
}) {
const [state, setState] = useState<T | null>(defaultOpen);
const c = useMemo(
() => (typeof children === "function" ? children(builder) : children),
[children],
);
return (
<MultiDialogContainerContext.Provider value={[state, setState]}>
{c}
</MultiDialogContainerContext.Provider>
);
}
@adnanalbeda
Copy link
Author

adnanalbeda commented Feb 10, 2024

multiDialog.tsx Usage

type Modals = "edit" | "delete" ; // or enum

//* Use MultiDialogBuilder (mdb) is optional, but it allows `value` autocomplete & intellisense
<MuliDialog<Modals>>
  {(mdb) => (<>
    <mdb.Trigger value="edit">
        <button>Edit</button>
    </mdb.Trigger>
    <mdb.Trigger value="delete">
        {/* Can be used with dropdown menu item or context menu item */}
        <button>Edit</button>
    </mdb.Trigger>
    <mdb.Container value="edit">
      <Dialog>
        <DialogPortal>
          <DialogOverlay />
          <DialogContent>
            EDIT FORM CONTENT
          </DialogContent>
        </DialogPortal>
      </Dialog>
    </mdb.Container>
    <mdb.Container value="delete">
      <Dialog>
        <DialogPortal>
          <DialogOverlay />
          <DialogContent>
            DELETE CONTENT
          </DialogContent>
        </DialogPortal>
      </Dialog>
    </mdb.Container>
  </>)}
</MultiDialog>

Manually Closing Dialog After Processing Data

<MuliDialog<Modals>>
  {(mdb) => (<>
    <mdb.Trigger value="edit">
        <button>Edit</button>
    </mdb.Trigger>
    <mdb.Trigger value="delete">
        {/* Can be used with dropdown menu item or context menu item */}
        <button>Edit</button>
    </mdb.Trigger>
    <mdb.Container value="edit">
      <EditFormDialog />
    <mdb.Container value="delete">
       <DeleteItemDialog/>
    </mdb.Container>
  </>)}
</MultiDialog>

// Method 1, by using global `useMultiDialog` hook. 
function EditFormDialog(props:DialogProps) {
    const [, setOpen] = useMultiDialog();

   return  (
     <Dialog {...props}>
        <DialogPortal>
          <DialogOverlay />
          <DialogContent> 
            <Form onSubmit={(e) => { 
              // process data
              setOpen(null); // setting value to null close the dialog.
            }}>
            EDIT FORM CONTENT
            </Form>
          </DialogContent>
        </DialogPortal>
      </Dialog> 
      );
}

// Method 2, by specifying the dialog id in the hook.
function DeleteItemDialog(props:DialogProps) {
    const [, setIsOpen] = useMultiDialog<Modals>("delete");

   return  (
     <Dialog {...props}>
        <DialogPortal>
          <DialogOverlay />
          <DialogContent> 
            DELETE ITEM DETAILS
            <button onClick={() => {
              // process data
              setIsOpen(false);
            }}>
             DELETE
           </button>
          </DialogContent>
        </DialogPortal>
      </Dialog> 
      );
}

@mmo80
Copy link

mmo80 commented Apr 10, 2024

Hey @adnanalbeda! Great work with the multidialog.. works great! But i have one question.

How can i trigger the dialog close "manually"? Does not work as it does with standard shadcn componets with the open and handleOpenChange.

Thanks in advance!

//... 

const [openPopovers, setOpenPopovers] = useState<OpenPopovers>({});

const handleOpenChange = (id: number, open: boolean) => {
  setOpenPopovers((prev) => ({ ...prev, [id.toString()]: open }));
};

//... 

return (
//... 
<mdb.Container value="delete">
  <Dialog open={openPopovers[file.id] || false} onOpenChange={(open) => handleOpenChange(file.id, open)}>
    <DialogContent>
      <DialogHeader>
        <DialogTitle>Are you absolutely sure?</DialogTitle>
        <DialogDescription>
          This action cannot be undone. Are you sure you want to permanently delete this file?
        </DialogDescription>
      </DialogHeader>
      <DialogFooter>
        <Button
          onClick={() => {
            handleOpenChange(file.id, false);
          }}
        >
          Cancel
        </Button>
        <Button
          onClick={() => {
            removeDocument(file.id);
          }}
        >
          Confirm
        </Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</mdb.Container>
//... 

@adnanalbeda
Copy link
Author

adnanalbeda commented Apr 14, 2024

How can i trigger the dialog close "manually"? Does not work as it does with standard shadcn componets with the open and handleOpenChange.

Thanks in advance!

Hi @mmo80 !

I'm glad you found my code useful.

I just updated the usage comment and explained how to do that. I hope you find it helpful.

You can also change the code a bit and pass setState to the builder function:

export type MultiDialogBuilder<T = unknown> = (
  builder: Builder<T>,
  setOpen: (v: T | null) => void
) => React.ReactNode;
export function MultiDialog<T = unknown>({
  defaultOpen = null,
  children,
}: {
  defaultOpen?: T | null;
  children?: React.ReactNode | MultiDialogBuilder<T>;
}) {
  const [state, setState] = useState<T | null>(defaultOpen);

  const c = useMemo(
    () => (typeof children === "function" ? children(builder, setState) : children),
    [children],
  );

  return (
    <MultiDialogContainerContext.Provider value={[state, setState]}>
      {c}
    </MultiDialogContainerContext.Provider>
  );
}

and use it to manually close the dialog:

<MuliDialog<Modals>>
  {(mdb,setOpen) => (<>
    <mdb.Trigger value="edit">
        <button>Edit</button>
    </mdb.Trigger>
    <mdb.Trigger value="delete">
        {/* Can be used with dropdown menu item or context menu item */}
        <button>Edit</button>
    </mdb.Trigger>
    <mdb.Container value="edit">
      <Dialog>
        <DialogPortal>
          <DialogOverlay />
          <DialogContent>
            EDIT FORM CONTENT
            <button onClick={()=>{
              // do some processing
              setOpen(null);
            }}>
            SUBMIT
            </button>
          </DialogContent>
        </DialogPortal>
      </Dialog>
    </mdb.Container>
    <mdb.Container value="delete">
      <Dialog>
        <DialogPortal>
          <DialogOverlay />
          <DialogContent>
            DELETE CONTENT
          </DialogContent>
        </DialogPortal>
      </Dialog>
    </mdb.Container>
  </>)}
</MultiDialog>

@mmo80
Copy link

mmo80 commented Apr 16, 2024

Will try it out, tnx!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment