-
-
Save alexanderson1993/623bf0324f740ec4e33f33b59487dda7 to your computer and use it in GitHub Desktop.
"use client"; | |
import * as React from "react"; | |
import { Input } from "@/components/ui/Input"; | |
import { Button } from "@/components/ui/Button"; | |
import { | |
AlertDialog, | |
AlertDialogContent, | |
AlertDialogHeader, | |
AlertDialogTitle, | |
AlertDialogDescription, | |
AlertDialogFooter, | |
} from "@/components/ui/AlertDialog"; | |
export const AlertDialogContext = React.createContext< | |
<T extends AlertAction>( | |
params: T | |
) => Promise<T["type"] extends "alert" | "confirm" ? boolean : null | string> | |
>(() => null!); | |
export type AlertAction = | |
| { type: "alert"; title: string; body?: string; cancelButton?: string } | |
| { | |
type: "confirm"; | |
title: string; | |
body?: string; | |
cancelButton?: string; | |
actionButton?: string; | |
} | |
| { | |
type: "prompt"; | |
title: string; | |
body?: string; | |
cancelButton?: string; | |
actionButton?: string; | |
defaultValue?: string; | |
inputProps?: React.DetailedHTMLProps< | |
React.InputHTMLAttributes<HTMLInputElement>, | |
HTMLInputElement | |
>; | |
} | |
| { type: "close" }; | |
interface AlertDialogState { | |
open: boolean; | |
title: string; | |
body: string; | |
type: "alert" | "confirm" | "prompt"; | |
cancelButton: string; | |
actionButton: string; | |
defaultValue?: string; | |
inputProps?: React.PropsWithoutRef< | |
React.DetailedHTMLProps< | |
React.InputHTMLAttributes<HTMLInputElement>, | |
HTMLInputElement | |
> | |
>; | |
} | |
export function alertDialogReducer( | |
state: AlertDialogState, | |
action: AlertAction | |
): AlertDialogState { | |
switch (action.type) { | |
case "close": | |
return { ...state, open: false }; | |
case "alert": | |
case "confirm": | |
case "prompt": | |
return { | |
...state, | |
open: true, | |
...action, | |
cancelButton: | |
action.cancelButton || (action.type === "alert" ? "Okay" : "Cancel"), | |
actionButton: | |
("actionButton" in action && action.actionButton) || "Okay", | |
}; | |
default: | |
return state; | |
} | |
} | |
export function AlertDialogProvider({ | |
children, | |
}: { | |
children: React.ReactNode; | |
}) { | |
const [state, dispatch] = React.useReducer(alertDialogReducer, { | |
open: false, | |
title: "", | |
body: "", | |
type: "alert", | |
cancelButton: "Cancel", | |
actionButton: "Okay", | |
}); | |
const resolveRef = React.useRef<(tf: any) => void>(); | |
function close() { | |
dispatch({ type: "close" }); | |
resolveRef.current?.(false); | |
} | |
function confirm(value?: string) { | |
dispatch({ type: "close" }); | |
resolveRef.current?.(value ?? true); | |
} | |
const dialog = React.useCallback(async <T extends AlertAction>(params: T) => { | |
dispatch(params); | |
return new Promise< | |
T["type"] extends "alert" | "confirm" ? boolean : null | string | |
>((resolve) => { | |
resolveRef.current = resolve; | |
}); | |
}, []); | |
return ( | |
<AlertDialogContext.Provider value={dialog}> | |
{children} | |
<AlertDialog | |
open={state.open} | |
onOpenChange={(open) => { | |
if (!open) close(); | |
return; | |
}} | |
> | |
<AlertDialogContent asChild> | |
<form | |
onSubmit={(event) => { | |
event.preventDefault(); | |
confirm(event.currentTarget.prompt?.value); | |
}} | |
> | |
<AlertDialogHeader> | |
<AlertDialogTitle>{state.title}</AlertDialogTitle> | |
{state.body ? ( | |
<AlertDialogDescription>{state.body}</AlertDialogDescription> | |
) : null} | |
</AlertDialogHeader> | |
{state.type === "prompt" && ( | |
<Input | |
name="prompt" | |
defaultValue={state.defaultValue} | |
{...state.inputProps} | |
/> | |
)} | |
<AlertDialogFooter> | |
<Button type="button" onClick={close}> | |
{state.cancelButton} | |
</Button> | |
{state.type === "alert" ? null : ( | |
<Button type="submit">{state.actionButton}</Button> | |
)} | |
</AlertDialogFooter> | |
</form> | |
</AlertDialogContent> | |
</AlertDialog> | |
</AlertDialogContext.Provider> | |
); | |
} | |
type Params<T extends "alert" | "confirm" | "prompt"> = | |
| Omit<Extract<AlertAction, { type: T }>, "type"> | |
| string; | |
export function useConfirm() { | |
const dialog = React.useContext(AlertDialogContext); | |
return React.useCallback( | |
(params: Params<"confirm">) => { | |
return dialog({ | |
...(typeof params === "string" ? { title: params } : params), | |
type: "confirm", | |
}); | |
}, | |
[dialog] | |
); | |
} | |
export function usePrompt() { | |
const dialog = React.useContext(AlertDialogContext); | |
return (params: Params<"prompt">) => | |
dialog({ | |
...(typeof params === "string" ? { title: params } : params), | |
type: "prompt", | |
}); | |
} | |
export function useAlert() { | |
const dialog = React.useContext(AlertDialogContext); | |
return (params: Params<"alert">) => | |
dialog({ | |
...(typeof params === "string" ? { title: params } : params), | |
type: "alert", | |
}); | |
} |
import App from './app' | |
import { hydrateRoot, createRoot } from "react-dom/client"; | |
import App from "./App"; | |
import AlertDialogProvider from "@/components/ui/AlertDialogProvider"; | |
createRoot(document.getElementById("root")).render( | |
<AlertDialogProvider>{children}</AlertDialogProvider> | |
); |
import { | |
useAlert, | |
useConfirm, | |
usePrompt, | |
} from "@/components/ui/AlertDialogProvider"; | |
import { Button } from "@/components/ui/Button"; | |
export default function Test() { | |
const alert = useAlert(); | |
const confirm = useConfirm(); | |
const prompt = usePrompt(); | |
return ( | |
<> | |
<Button | |
onClick={async () => { | |
console.log( | |
await alert({ | |
title: "Test", | |
body: "Just wanted to say you're cool.", | |
cancelButton: "Heyo!", | |
}) // false | |
); | |
}} | |
type="button" | |
> | |
Test Alert | |
</Button> | |
<Button | |
onClick={async () => { | |
console.log( | |
await confirm({ | |
title: "Test", | |
body: "Are you sure you want to do that?", | |
cancelButton: "On second thought...", | |
}) // true | false | |
); | |
}} | |
type="button" | |
> | |
Test Confirm | |
</Button> | |
<Button | |
onClick={async () => { | |
console.log( | |
await prompt({ | |
title: "Test", | |
body: "Hey there! This is a test.", | |
defaultValue: "Something something" + Math.random().toString(), | |
}) // string | false | |
); | |
}} | |
type="button" | |
> | |
Test Prompt | |
</Button> | |
</> | |
); | |
} |
This is the fantastic solution, Thank you very much @alexanderson1993
very good, thanks
That is a very nice approach. Thank you
When you are triggering it from a shadcn context menu, you must specify modal={false} on the context menu. Otherwise it will freeze the dom
I have made some changes so you can specify the cancelButtonVariant
and the actionButtonVariant
when calling any of them.
I have also moved the default action and cancel text to a central location at the top.
This also includes the fix from @Nishchit14
The default variants are "default"
"use client";
import * as React from "react";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
} from "@/components/ui/alert-dialog";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export const AlertDialogContext = React.createContext<(
params: AlertAction
) => Promise<AlertAction["type"] extends "alert" | "confirm" ? boolean : null | string>>(() => null!);
type ButtonVariant = "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
const defaultCancelButtonText:string = "Cancel";
const defaultActionButtonText:string = "Okay";
export type AlertAction =
| {
type: "alert";
title: string;
body?: string;
cancelButton?: string;
cancelButtonVariant?: ButtonVariant;
}
| {
type: "confirm";
title: string;
body?: string;
cancelButton?: string;
actionButton?: string;
cancelButtonVariant?: ButtonVariant;
actionButtonVariant?: ButtonVariant;
}
| {
type: "prompt";
title: string;
body?: string;
cancelButton?: string;
actionButton?: string;
defaultValue?: string;
cancelButtonVariant?: ButtonVariant;
actionButtonVariant?: ButtonVariant;
inputProps?: React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>;
}
| { type: "close" };
interface AlertDialogState {
open: boolean;
title: string;
body: string;
type: "alert" | "confirm" | "prompt";
cancelButton: string;
actionButton: string;
cancelButtonVariant: ButtonVariant;
actionButtonVariant: ButtonVariant;
defaultValue?: string;
inputProps?: React.PropsWithoutRef<
React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>
>;
}
export function alertDialogReducer(
state: AlertDialogState,
action: AlertAction
): AlertDialogState {
switch (action.type) {
case "close":
return { ...state, open: false };
case "alert":
case "confirm":
case "prompt":
return {
...state,
open: true,
...action,
cancelButton: action.cancelButton || (action.type === "alert" ? defaultActionButtonText : defaultCancelButtonText),
actionButton: ("actionButton" in action && action.actionButton) || defaultActionButtonText,
cancelButtonVariant: action.cancelButtonVariant || "default",
actionButtonVariant: ("actionButtonVariant" in action && action.actionButtonVariant) || "default",
};
default:
return state;
}
}
export function AlertDialogProvider({
children,
}: {
children: React.ReactNode;
}) {
const [state, dispatch] = React.useReducer(alertDialogReducer, {
open: false,
title: "",
body: "",
type: "alert",
cancelButton: defaultCancelButtonText,
actionButton: defaultActionButtonText,
cancelButtonVariant: "default",
actionButtonVariant: "default",
});
const resolveRef = React.useRef<(tf: any) => void>();
function close() {
dispatch({ type: "close" });
resolveRef.current?.(false);
}
function confirm(value?: string) {
dispatch({ type: "close" });
resolveRef.current?.(value ?? true);
}
const dialog = React.useCallback(async <T extends AlertAction>(params: T) => {
dispatch(params);
return new Promise<
T["type"] extends "alert" | "confirm" ? boolean : null | string
>((resolve) => {
resolveRef.current = resolve;
});
}, []);
return (
<AlertDialogContext.Provider value={dialog}>
{children}
<AlertDialog
open={state.open}
onOpenChange={(open) => {
if (!open) close();
return;
}}
>
<AlertDialogContent asChild>
<form
onSubmit={(event) => {
event.preventDefault();
confirm(event.currentTarget.prompt?.value);
}}
>
<AlertDialogHeader>
<AlertDialogTitle>{state.title}</AlertDialogTitle>
{state.body ? (
<AlertDialogDescription>{state.body}</AlertDialogDescription>
) : null}
</AlertDialogHeader>
{state.type === "prompt" && (
<Input
name="prompt"
defaultValue={state.defaultValue}
{...state.inputProps}
/>
)}
<AlertDialogFooter>
<Button type="button" onClick={close} variant={state.cancelButtonVariant}>
{state.cancelButton}
</Button>
{state.type === "alert" ? null : (
<Button type="submit" variant={state.actionButtonVariant}>{state.actionButton}</Button>
)}
</AlertDialogFooter>
</form>
</AlertDialogContent>
</AlertDialog>
</AlertDialogContext.Provider>
);
}
type Params<T extends "alert" | "confirm" | "prompt"> =
| Omit<Extract<AlertAction, { type: T }>, "type">
| string;
export function useConfirm() {
const dialog = React.useContext(AlertDialogContext);
return React.useCallback(
(params: Params<"confirm">) => {
return dialog({
...(typeof params === "string" ? { title: params } : params),
type: "confirm",
});
},
[dialog]
);
}
export function usePrompt() {
const dialog = React.useContext(AlertDialogContext);
return (params: Params<"prompt">) =>
dialog({
...(typeof params === "string" ? { title: params } : params),
type: "prompt",
});
}
export function useAlert() {
const dialog = React.useContext(AlertDialogContext);
return (params: Params<"alert">) =>
dialog({
...(typeof params === "string" ? { title: params } : params),
type: "alert",
});
}
This is really good and really useful, this should be a part of a shadcn registry somewhere (Shadcn lets you create your own custom components registry) so that it can be added to a project using just 1 command...
But overall, good stuff, thank you, this was very helpful.
@sankalpmukim you can easily achieve this with shadcn using https://github.com/desko27/react-call
The provided code has a syntax error, It's due to the incorrect placement of the generic type parameter
<T extends AlertAction>
. Here's the corrected version:In the corrected code:
<T extends AlertAction>
is removed from the context type definition.params: T
is replaced withparams: AlertAction
to directly use theAlertAction
type.