Imperative flow
Imperative flow
Modals are opened from code and awaited like any other async call. Result, cancellation, and chained logic stay in one function.
Why promises, not v-model
A v-model-style API splits a modal's outcome across props, refs, and event handlers. With a promise, the entire flow stays in one place:
const name = await openModal<string>(NameDialog).catch(() => null)
if (!name) return
await api.rename(id, name)
The modal call is a function call that happens to render UI. The caller awaits it, reads the result, and continues.
Lifecycle
openModal(Component, options)
│
├── component mounts inside its group target
│ user interacts …
│
├── confirm(data) → exit animation → promise resolves with data
└── close() / Esc / overlay → exit animation → promise rejects (ModalClosedError)
The promise settles after the exit animation finishes, so your CSS transition always plays out. Pass instantExit: true to skip it.
Inside the modal
useModalContext<T>() exposes confirm(data) and close():
<script setup lang="ts">
import { ModalRoot, ModalContent, useModalContext } from '@kolirt/vue-modal'
defineOptions({ modalGroup: 'default' })
const { close, confirm } = useModalContext<boolean>()
</script>
<template>
<ModalRoot>
<ModalContent>
<button @click="close()">Cancel</button>
<button @click="confirm(true)">OK</button>
</ModalContent>
</ModalRoot>
</template>
confirm(data)— resolves the promise withdata.close()— rejects withModalClosedError.
On the calling side
import { openModal, ModalClosedError } from '@kolirt/vue-modal'
// Inline try/catch — explicit success vs cancel
try {
const ok = await openModal<boolean>(ConfirmDialog, {
props: { message: 'Delete this item?' }
})
if (ok) await api.delete(id)
} catch (e) {
if (!(e instanceof ModalClosedError)) throw e
}
// .catch() shorthand — collapse cancel to a falsy value
const ok = await openModal<boolean>(ConfirmDialog, {
props: { message: 'Delete this item?' }
}).catch(() => false)
if (ok) await api.delete(id)
Chaining
Because openModal returns a real promise, multi-step flows stay linear:
async function rename() {
const name = await openModal<string>(NameDialog).catch(() => null)
if (!name) return
const ok = await openModal<boolean>(ConfirmDialog, {
props: { message: `Rename to "${name}"?` }
}).catch(() => false)
if (ok) await api.rename(id, name)
}
One try/catch wraps the whole sequence. No shared state stuck between handlers.
replaceModal
replaceModal(Component, options) closes the topmost modal of the target group instantly (skipping its exit animation and ignoring guards), then opens a new one. Use it for wizard-style step swaps where the previous step's exit animation would feel slow.
import { replaceModal } from '@kolirt/vue-modal'
replaceModal(StepTwo, {
props: { data: collected },
instantEnter: true
})
The previous step's promise rejects with ModalClosedError — make sure the original caller catches it. See Opening & closing for the full action reference.
