Forked from alexanderson1993/AlertDialogProvider.tsx
Created
November 21, 2024 17:47
-
-
Save iamFoxx/80b09737eac32020ff881ad83336bdd4 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 hidden or 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 hidden or 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 hidden or 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> | |
</> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment