GitHub55X

DialogCaller

Vue dialog caller to open components programmatically with a promise-based stack API.

PreviousNext

Features

  • Programmatic API — Register a component once with useDialogCaller(), then await caller.call(props) from any handler
  • Promise-based results — Components finish calls with call.resolve() or call.reject(); the outer promise always resolves (shape configurable)
  • Stack support — Nested and concurrent dialogs mount as separate stack entries with automatic z-index layering
  • Optional dialog shellDialogCaller.DialogShell wires open state and external dismiss for Ark UI dialogs

Installation

Install from the Vuzeno registry:

      
      bunx --bun shadcn-vue@latest add https://vuzeno.com/r/dialog-caller.json
    

Setup

Mount the host once in your app root:

      
      <script setup lang="ts">
import { DialogCaller } from "@/components/ui/dialog-caller";
</script>

<template>
  <NuxtPage />
  <DialogCaller.Host />
</template>
    

Usage

Register a dialog component once, then call it imperatively:

      
      <script setup lang="ts">
import { useDialogCaller } from "@/components/ui/dialog-caller";
import DeleteConfirmDialog from "./DeleteConfirmDialog.vue";

const deleteDialog = useDialogCaller(DeleteConfirmDialog, {
  exitDuration: 250,
  resultShape: "cancelled",
});

async function onDelete(item: Item) {
  const result = await deleteDialog.call({ name: item.name });

  if (!result.cancelled && result.data) {
    await api.delete(item.id);
  }
}
</script>
    

Inside the called component, use useDialogCallerContext() and assign it to call:

      
      <script setup lang="ts">
const props = defineProps<{ name: string }>();
const call = useDialogCallerContext<boolean>();
</script>

<template>
  <DialogCaller.DialogShell>
    <Dialog.Content>
      <Dialog.Title>Delete {{ props.name }}?</Dialog.Title>
      <Dialog.Actions>
        <Button @click="call.reject('cancel')">Cancel</Button>
        <Button @click="call.resolve(true)">Delete</Button>
      </Dialog.Actions>
    </Dialog.Content>
  </DialogCaller.DialogShell>
</template>
    

Composition

      
      DialogCaller.Host
└── (dynamic stack entries)

DialogCaller.Provider
└── custom host wiring via useDialogCallerHost()

DialogCaller.DialogShell
└── Dialog.Content
    └── your dialog UI
    

Examples

Delete confirmation

Use a single useDialogCaller registration for the whole table — no dialog duplicated per row:

Nested dialogs

Edit the form, then cancel with unsaved changes. The confirmation dialog stacks on top while the form stays mounted:

API

useDialogCaller(component, options?)

Returns { call(props), reject() }.

OptionTypeDefaultDescription
idstringcomponent nameRegistry key for idempotent registration
exitDurationnumber200Milliseconds to keep the instance mounted after resolve/reject (exit animation)
resultShape"cancelled" | "data" | "raw""cancelled"Shape of the value returned by call()
defaultRejectValueunknownnullValue returned on reject when resultShape is "raw"

useDialogCallerContext()

Returns { props, phase, resolve, reject, id }. Assign to call and use call.resolve() / call.reject() without destructuring.

Result shapes

resultShapeAfter call.resolve(data)After call.reject(reason?)
"cancelled"{ cancelled: false, data, reason: null }{ cancelled: true, data: null, reason? }
"data"{ data }{ data: null }
"raw"datadefaultRejectValue

Notes

  • Each call() mounts a new stack entry. For HTTP-heavy dialogs, fetch data from props (e.g. with watch) rather than in onMounted.
  • DialogCaller.Host must be mounted before calling call() — otherwise an error is thrown.