Last active
February 14, 2026 10:11
-
-
Save nemedib/a6ecf16caf34a634450ec72599bdb8be to your computer and use it in GitHub Desktop.
Typed async dialog store for Svelte shadcn dialogs. open() returns a Promise inferred from the dialog component’s close(result) prop. Includes a DialogHost renderer and example usage.
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
| <script lang="ts"> | |
| import { DialogStore } from "$lib/context/dialog-store.svelte"; | |
| import DialogHost from "$lib/components/ui/dialog/dialog-host.svelte"; | |
| import { setContext } from "svelte"; | |
| setContext("dialog", new DialogStore()); | |
| </script> | |
| <DialogHost /> |
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
| <script lang="ts"> | |
| import AreYouSureDialog from "$lib/components/dialogs/are-you-sure-dialog.svelte"; | |
| import { DialogStore } from "$lib/context/dialog-store.svelte"; | |
| import { getContext } from "svelte"; | |
| const dialogStore = getContext<DialogStore>("dialog"); | |
| async function onClick() { | |
| const result = await dialogStore.open(AreYouSureDialog, { | |
| title: "Are you sure?", | |
| description: "...", | |
| }); | |
| if (!result) return; | |
| alert("You are sure!") | |
| } | |
| </script> | |
| <button onclick={onClick}> | |
| Click me | |
| </button> |
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
| <script lang="ts"> | |
| import * as Dialog from "$lib/components/ui/dialog/index.js"; | |
| import Button from "../ui/button/button.svelte"; | |
| type Props = { | |
| close: (data: boolean) => void; | |
| title: string; | |
| description: string; | |
| }; | |
| const { close, title, description }: Props = $props(); | |
| </script> | |
| <Dialog.Content interactOutsideBehavior="ignore"> | |
| <Dialog.Header> | |
| <Dialog.Title>{title}</Dialog.Title> | |
| <Dialog.Description>{description}</Dialog.Description> | |
| </Dialog.Header> | |
| <Dialog.Footer> | |
| <Button onclick={() => close(true)}> | |
| Ok | |
| </Button> | |
| <Button variant="ghost" onclick={() => close(false)}> | |
| Cancel | |
| </Button> | |
| </Dialog.Footer> | |
| </Dialog.Content> |
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
| <script lang="ts"> | |
| import * as Dialog from "$lib/components/ui/dialog"; | |
| import { DialogStore } from "$lib/context/dialog-store.svelte"; | |
| import { getContext } from "svelte"; | |
| const dialogStore = getContext<DialogStore>("dialog"); | |
| let top = $derived(dialogStore.stack.at(-1)); | |
| </script> | |
| {#if top} | |
| <Dialog.Root | |
| open={true} | |
| onOpenChange={(open) => { | |
| if (!open) dialogStore.close(top.id, undefined); | |
| }} | |
| > | |
| <top.component {...top.props} close={(result: any) => dialogStore.close(top.id, result)} /> | |
| </Dialog.Root> | |
| {/if} |
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
| /* eslint-disable @typescript-eslint/no-explicit-any */ | |
| import { type Component, type ComponentProps } from "svelte"; | |
| export type DialogResult<C extends Component<any>> = | |
| ComponentProps<C> extends { close?: (result: infer R) => any } ? R | undefined : void; | |
| export type DialogPublicProps<C extends Component<any>> = | |
| Omit<ComponentProps<C>, "close">; | |
| type AnyComponent = Component<any>; | |
| export type DialogConfig = { closeOnClickOutside?: boolean } | |
| type Entry<C extends AnyComponent> = { | |
| id: string; | |
| component: C; | |
| props?: DialogPublicProps<C>; | |
| config?: DialogConfig | |
| resolve: (value: DialogResult<C>) => void; | |
| }; | |
| export class DialogStore { | |
| private _stack = $state<Entry<any>[]>([]); | |
| get stack() { | |
| return this._stack; | |
| } | |
| open<C extends AnyComponent>( | |
| component: C, | |
| props?: DialogPublicProps<C>, | |
| config?: DialogConfig | |
| ): Promise<DialogResult<C>> { | |
| return new Promise((resolve) => { | |
| const id = crypto.randomUUID(); | |
| const entry: Entry<C> = { id, component, config, props, resolve }; | |
| this._stack.push(entry); | |
| }); | |
| } | |
| close<C extends AnyComponent>(id: number, result: DialogResult<C>) { | |
| const indexToDelete = this._stack.findIndex((x) => x.id === id) | |
| if (indexToDelete >= 0) { | |
| this._stack[indexToDelete].resolve(result); | |
| this._stack.splice(indexToDelete, 1) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment