Skip to content

Instantly share code, notes, and snippets.

@alexanderson1993
Created April 2, 2023 19:07
Show Gist options
  • Save alexanderson1993/623bf0324f740ec4e33f33b59487dda7 to your computer and use it in GitHub Desktop.
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.
"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>
</>
);
}
@Nishchit14
Copy link

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:

export const AlertDialogContext = React.createContext<(
  params: AlertAction
) => Promise<AlertAction["type"] extends "alert" | "confirm" ? boolean : null | string>>(() => null!);

In the corrected code:

  • The generic type parameter <T extends AlertAction> is removed from the context type definition.
  • params: T is replaced with params: AlertAction to directly use the AlertAction type.

@Nishchit14
Copy link

This is the fantastic solution, Thank you very much @alexanderson1993

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