Skip to content

Instantly share code, notes, and snippets.

@nthung2112
Forked from alexanderson1993/AlertDialogProvider.tsx
Last active November 30, 2024 14:40
Show Gist options
  • Save nthung2112/37eea04a9884d2f7d47c71bdc0a7c903 to your computer and use it in GitHub Desktop.
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.
'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',
});
}
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>
</>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment