Forked from alexanderson1993/AlertDialogProvider.tsx
Last active
November 30, 2024 14:40
-
-
Save nthung2112/37eea04a9884d2f7d47c71bdc0a7c903 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/alert-dialog'; | |
export const ModalContext = React.createContext< | |
<T extends ModalAction>( | |
params: T | |
) => Promise<T['type'] extends 'alert' | 'confirm' ? boolean : null | string> | |
>(() => null!); | |
export type ModalAction = | |
| { 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 ModalState { | |
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 modalReducer(state: ModalState, action: ModalAction): ModalState { | |
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 ModalProvider({ children }: { children: React.ReactNode }) { | |
const [state, dispatch] = React.useReducer(modalReducer, { | |
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 ModalAction>(params: T) => { | |
dispatch(params); | |
return new Promise<T['type'] extends 'alert' | 'confirm' ? boolean : null | string>( | |
(resolve) => { | |
resolveRef.current = resolve; | |
} | |
); | |
}, []); | |
return ( | |
<ModalContext.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> | |
</ModalContext.Provider> | |
); | |
} | |
export type ModalParams<T extends 'alert' | 'confirm' | 'prompt'> = | |
| Omit<Extract<ModalAction, { type: T }>, 'type'> | |
| string; | |
export function useConfirm() { | |
const dialog = React.useContext(ModalContext); | |
return React.useCallback( | |
(params: ModalParams<'confirm'>) => { | |
return dialog({ | |
...(typeof params === 'string' ? { title: params } : params), | |
type: 'confirm', | |
}); | |
}, | |
[dialog] | |
); | |
} | |
export function usePrompt() { | |
const dialog = React.useContext(ModalContext); | |
return (params: ModalParams<'prompt'>) => | |
dialog({ | |
...(typeof params === 'string' ? { title: params } : params), | |
type: 'prompt', | |
}); | |
} | |
export function useAlert() { | |
const dialog = React.useContext(ModalContext); | |
return (params: ModalParams<'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