Skip to content

Instantly share code, notes, and snippets.

@composite
Forked from alexanderson1993/AlertDialogProvider.tsx
Last active July 9, 2024 09:09
Show Gist options
  • Save composite/f5785ab7be0a317dbb88f32d72ca3e5c to your computer and use it in GitHub Desktop.
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.
// 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>
);
}
'use client';
import * as React from 'react';
export type AlertType = 'alert' | 'confirm' | 'prompt';
export type AlertResult<T extends AlertType> = Promise<
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<'confirm'>>();
dispatch({
...params,
type: 'confirm',
closeButton: params.closeButton ?? 'Close',
actionButton: params.actionButton ?? 'Confirm',
resolver: resolve,
});
return promise;
}
'use client';
import * as React from 'react';
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) => {
if (!opened) handleClose();
}}
>
<AlertDialogContent asChild onFocusOutside={(e) => 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">{dialog?.actionButton}</Button>}
<Button
type="button"
variant={dialog?.type === 'alert' ? 'default' : 'outline'}
onClick={() => handleClose()}
>
{dialog?.closeButton}
</Button>
</AlertDialogFooter>
</form>
</AlertDialogContent>
</AlertDialog>
);
}
// 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>
);
}
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