Created
April 2, 2023 19:07
-
-
Save alexanderson1993/623bf0324f740ec4e33f33b59487dda7 to your computer and use it in GitHub Desktop.
A multi-purpose alert/confirm/prompt replacement built with shadcn/ui AlertDialog components.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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", | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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",
});
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.