Forked from alexanderson1993/AlertDialogProvider.tsx
Last active
November 21, 2024 08:35
-
-
Save composite/f5785ab7be0a317dbb88f32d72ca3e5c to your computer and use it in GitHub Desktop.
A multi-purpose alert/confirm/prompt replacement built with shadcn/ui AlertDialog components. No Context, SSR friendly, Also works on Next.js and Remix, but requires React 18 or later due to useSyncExternalStore.
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
// For Next.js App Router usage | |
import { type ReactNode } from 'react'; | |
import OneDialog from '@/components/OneDialog'; | |
export default function Layout({ | |
children, | |
}: Readonly<{ | |
children: ReactNode; | |
}>) { | |
return ( | |
<html lang="ko" className="dark m-0 h-full w-full"> | |
<head> | |
</head> | |
<body> | |
{children} | |
<OneDialog /> | |
</body> | |
</html> | |
); | |
} |
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'; | |
export type AlertType = 'alert' | 'confirm' | 'prompt'; | |
export type AlertResult<T extends AlertType> = T extends 'prompt' | |
? string | false | |
: T extends 'confirm' | |
? boolean | |
: true; | |
export type AlertRequest<T extends AlertType> = T extends 'alert' | |
? { | |
title?: React.ReactNode; | |
body: React.ReactNode; | |
closeButton?: React.ReactNode; | |
} | |
: T extends 'confirm' | |
? { | |
title?: React.ReactNode; | |
body: React.ReactNode; | |
closeButton?: React.ReactNode; | |
actionButton?: React.ReactNode; | |
} | |
: T extends 'prompt' | |
? { | |
title?: React.ReactNode; | |
body: React.ReactNode; | |
closeButton?: React.ReactNode; | |
actionButton?: React.ReactNode; | |
defaultValue?: string; | |
} | |
: never; | |
interface AlertDialogState<T extends AlertType> { | |
title?: React.ReactNode; | |
body: React.ReactNode; | |
type: T; | |
closeButton: React.ReactNode; | |
actionButton: React.ReactNode; | |
defaultValue?: string; | |
resolver?: (value: AlertResult<T>) => void; | |
} | |
const listeners: Array<(state: typeof memoryState) => void> = []; | |
let memoryState: { dialog: AlertDialogState<AlertType>; open?: true } = | |
{} as typeof memoryState; | |
const dispatch = <T extends AlertType>(dialog?: AlertDialogState<T>) => { | |
if (memoryState.dialog) { | |
const { dialog } = memoryState; | |
if (dialog?.resolver) { | |
dialog.resolver((dialog.type === 'alert') as unknown as AlertResult<T>); | |
delete dialog.resolver; | |
} | |
} | |
memoryState = dialog | |
? { dialog, open: true } | |
: { dialog: memoryState.dialog }; | |
listeners.forEach((listener) => listener(memoryState)); | |
}; | |
const promiser = <T>() => { | |
let resolve: unknown, reject: unknown; | |
const promise = new Promise<T>((ok, no) => { | |
resolve = ok; | |
reject = no; | |
}); | |
return { | |
promise, | |
resolve: resolve as (value: T | PromiseLike<T>) => void, | |
reject: reject as (reason?: any) => void, | |
}; | |
}; | |
function subscribe(listener: () => void) { | |
listeners.push(listener); | |
return () => { | |
const i = listeners.indexOf(listener); | |
if (i > -1) { | |
listeners.splice(i, 1); | |
} | |
}; | |
} | |
function getSnapshot() { | |
return memoryState; | |
} | |
export function useOneDialog() { | |
const state = React.useSyncExternalStore( | |
subscribe, | |
getSnapshot, | |
() => ({}) as typeof memoryState | |
); | |
return { ...state, dispatch }; | |
} | |
export function alert(params: AlertRequest<'alert'>) { | |
const { promise, resolve } = promiser<AlertResult<'alert'>>(); | |
dispatch({ | |
...params, | |
type: 'alert', | |
closeButton: params.closeButton ?? 'Close', | |
actionButton: '', | |
resolver: resolve, | |
}); | |
return promise; | |
} | |
export function confirm(params: AlertRequest<'confirm'>) { | |
const { promise, resolve } = promiser<AlertResult<'confirm'>>(); | |
dispatch({ | |
...params, | |
type: 'confirm', | |
closeButton: params.closeButton ?? 'Close', | |
actionButton: params.actionButton ?? 'Confirm', | |
resolver: resolve, | |
}); | |
return promise; | |
} | |
export function prompt(params: AlertRequest<'prompt'>) { | |
const { promise, resolve } = promiser<AlertResult<'prompt'>>(); | |
dispatch({ | |
...params, | |
type: 'prompt', | |
closeButton: params.closeButton ?? 'Close', | |
actionButton: params.actionButton ?? 'Confirm', | |
resolver: resolve, | |
}); | |
return promise; | |
} |
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 { | |
AlertDialog, | |
AlertDialogContent, | |
AlertDialogDescription, | |
AlertDialogFooter, | |
AlertDialogHeader, | |
AlertDialogTitle, | |
} from '@/components/ui/alert-dialog'; | |
import { Input } from '@/components/ui/input'; | |
import { Button } from '@/components/ui/button'; | |
import { | |
type AlertResult, | |
type AlertType, | |
useOneDialog, | |
} from './one-dialog'; | |
export default function OneDialog() { | |
const { dialog, open, dispatch } = useOneDialog(); | |
const handleClose = (result?: AlertResult<AlertType>) => { | |
if (dialog) { | |
const value: unknown = dialog.type === 'alert' ? true : (result ?? false); | |
if (dialog.resolver) { | |
dialog.resolver(value as AlertResult<AlertType>); | |
delete dialog.resolver; | |
} | |
dispatch(); | |
} | |
}; | |
return ( | |
<AlertDialog | |
open={!!open} | |
onOpenChange={(opened: boolean) => { | |
if (!opened) handleClose(); | |
}} | |
> | |
<AlertDialogContent | |
asChild | |
onFocusOutside={(e: Event) => e.preventDefault()} | |
> | |
<form | |
onSubmit={(event) => { | |
event.preventDefault(); | |
handleClose( | |
dialog?.type === 'prompt' | |
? event.currentTarget.prompt?.value || '' | |
: true | |
); | |
}} | |
> | |
<AlertDialogHeader> | |
<AlertDialogTitle>{dialog?.title ?? ''}</AlertDialogTitle> | |
{dialog?.body && ( | |
<AlertDialogDescription>{dialog?.body}</AlertDialogDescription> | |
)} | |
</AlertDialogHeader> | |
{dialog?.type === 'prompt' && ( | |
<Input name="prompt" defaultValue={dialog?.defaultValue} /> | |
)} | |
<AlertDialogFooter> | |
{dialog?.type !== 'alert' && ( | |
<Button type="submit" className="min-w-20">{dialog?.actionButton}</Button> | |
)} | |
<Button | |
type="button" | |
variant={dialog?.type === 'alert' ? 'default' : 'outline'} | |
className="min-w-20" | |
onClick={() => handleClose()} | |
> | |
{dialog?.closeButton} | |
</Button> | |
</AlertDialogFooter> | |
</form> | |
</AlertDialogContent> | |
</AlertDialog> | |
); | |
} |
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
// For Remix usage | |
import { | |
Links, | |
LiveReload, | |
Meta, | |
Outlet, | |
Scripts, | |
ScrollRestoration, | |
} from "@remix-run/react"; | |
import OneDialog from '@/components/OneDialog'; | |
export default function App() { | |
return ( | |
<html lang="en"> | |
<head> | |
<meta charSet="utf-8" /> | |
<meta | |
name="viewport" | |
content="width=device-width, initial-scale=1" | |
/> | |
<Meta /> | |
<Links /> | |
</head> | |
<body> | |
<Outlet /> | |
<ScrollRestoration /> | |
<Scripts /> | |
<LiveReload /> | |
<OneDialog /> | |
</body> | |
</html> | |
); | |
} |
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 { | |
alert, | |
confirm, | |
prompt, | |
} from "@/components/one-dialog"; | |
import { Button } from "@/components/ui/Button"; | |
export default function Test() { | |
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