useModal()
With useModal(), that means you don't have to add the modal to your Vue template and you don't have to use v-model or modalId to open or close the modal. You can simply use it to create a dynamic modal everywhere and control it programmatically.
Prerequisite
Dynamic modals are rendered by <ModalsContainer>, so make sure it is included in your app once:
<script setup lang="ts">
import { ModalsContainer } from 'vue-final-modal'
</script>
<template>
<!-- Your app -->
<ModalsContainer />
</template>
Usage
Passing Props and Events
useModal(options) takes the options that describe the modal component, and returns open() and close() functions to control it.
import { VueFinalModal, useModal } from 'vue-final-modal'
const { open, close } = useModal({
// Open the modal right after it was created, the default value is `false`.
defaultModelValue: false,
// Keep the modal mounted after it was closed, the default value is `false`.
keepAlive: false,
// The modal component. Use `<VueFinalModal>` or `<VueFinalDialog>` directly, or your own styled modal component.
component: VueFinalModal,
attrs: {
// Bind props to the modal component (VueFinalModal in this case).
clickToClose: true,
escToClose: true,
// Bind events to the modal component (VueFinalModal in this case).
// Any custom events can be listened for when prefixed with "on", e.g. "onEventName".
onBeforeOpen() { /* on before open */ },
onOpened() { /* on opened */ },
onBeforeClose() { /* on before close */ },
onClosed() { /* on closed */ },
},
})
// Open the modal
open().then(() => { /* Do something after modal opened */ })
// Close the modal
close().then(() => { /* Do something after modal closed */ })
Each required prop of component has to be given, in attrs or in props: props only has to give the required props that attrs leaves out, and a required prop given nowhere is a type error.
Reactive attrs
attrs can also be a ref, computed or a getter function. The modal will keep in sync with it, so you can update the modal's props dynamically without any extra API:
import { ref } from 'vue'
import { VueFinalModal, useModal } from 'vue-final-modal'
const title = ref('Hello World!')
const { open, close } = useModal({
component: VueFinalModal,
attrs: () => ({
title: title.value,
}),
})
// Updating the ref updates the opened modal as well
title.value = 'Hello Vue!'
A single prop can be a ref too, in attrs and in the attrs of a slot template:
const title = ref('Hello World!')
const { open, close } = useModal({
component: VueFinalModal,
attrs: { title },
})
Keeping the modal alive
By default the modal is unmounted when it closes and mounted again by the next open(). With keepAlive: true it stays mounted after close(), hidden with display: none, so its state survives. destroy() unmounts it when you are done with it. A kept-alive modal is not destroyed when the component that created it unmounts, so call destroy() yourself. See the Modal Keep Alive example.
import { useModal } from 'vue-final-modal'
import ModalDraft from './ModalDraft.vue'
const { open, close, destroy } = useModal({
keepAlive: true,
component: ModalDraft,
})
await open()
await close() // Still mounted, the draft inside is kept
await open() // The draft is still there
destroy() // Unmounted, the next open() starts from a fresh component
Passing Slots
with String
import { VueFinalModal, useModal } from 'vue-final-modal'
const { open, close } = useModal({
component: VueFinalModal,
attrs: { ... },
slots: {
default: '<p>The content of the modal</p>'
}
})
with Component
import { VueFinalModal, useModal } from 'vue-final-modal'
// ModalContent is the component you want to put into the modal content
import ModalContent from './ModalContent.vue'
const { open, close } = useModal({
component: VueFinalModal,
attrs: { ... },
slots: {
// You can import your own component as a slot and put it to `slots.default` without binding props and events.
default: ModalContent
}
})
with Component, Props and Events
import { VueFinalModal, defineTemplate, useModal } from 'vue-final-modal'
// ModalContent is the component you want to put into the modal content
import ModalContent from './ModalContent.vue'
const { open, close } = useModal({
component: VueFinalModal,
attrs: { ... },
slots: {
default: defineTemplate({
component: ModalContent,
attrs: {
// Bind ModalContent props
title: 'Hello world!'
// Bind ModalContent events
onConfirm() { }
}
})
}
})
with a ref or computed
Any of the slots above can also be a ref or computed. The modal keeps in sync with it:
import { computed, ref } from 'vue'
import { VueFinalModal, useModal } from 'vue-final-modal'
const message = ref('Hello World!')
const { open, close } = useModal({
component: VueFinalModal,
slots: {
default: computed(() => `<p>${message.value}</p>`),
},
})
// Updating the ref updates the opened modal as well
message.value = 'Hello Vue!'
slots is only type-checked when it is wrapped in defineTemplate(): without it, its attrs, props and listeners are taken as they are.defineTemplate() is a function that provides better DX for type checking. It just returns the same object you passed in. useModalSlot() from 4.x does the same and keeps working, and defineModal() is the same kind of helper for the template of a whole modal: its component, attrs and slots.What open() and close() resolve with
open() resolves with 'opened' once the enter transition has finished, and close() resolves with 'closed' once the leave transition has finished. They never stay pending: when the modal does not get there, they resolve with why, so compare the result to know what happened. The results are typed as ModalOpenResult and ModalCloseResult.
const { open } = useModal({ component: ModalConfirm })
if (await open() === 'opened') {
// The modal is opened and its enter transition has finished
}
| Result | When |
|---|---|
'opened', 'closed' | The enter or the leave transition has finished. |
'already-opened', 'already-closed' | The modal was already opened, or already closed. |
'stopped' | beforeOpen or beforeClose called event.stop(). |
'toggled-again' | The modal was toggled again before it finished, for example closed by the esc key while it was still opening. |
'destroyed' | The modal was destroyed before it finished, by destroy() or because the component that created it unmounted. |
'skipped-on-server' | open() only: the server skipped a modal created or opened outside a component, see Server-side rendering. |
An open() called while the modal is opening settles with that opening, and a close() called while it is closing settles with that closing. A close() called while the modal is still opening waits until it has opened, then closes it: open() resolves with 'opened' and close() with 'closed'.
<VueFinalDialog>, the components built on useModalLifecycle() and the functions of useVfm() resolve with the same results.
Status
status is a read-only ref of where the modal is: 'closed', 'opening', 'opened' or 'closing'. 'opening' and 'closing' last until the transitions have finished.
<script setup lang="ts">
const { open, status } = useModal({ component: ModalConfirm })
</script>
<template>
<button :disabled="status !== 'closed'" @click="open()">
Open
</button>
</template>
vfm.get(modalId)?.value.status and the status of useModalLifecycle() give the same for a modal component.
Server-side rendering
A modal opened while a component sets up, with defaultModelValue: true or an open() called in setup(), is part of the server HTML and hydrates already open. On the server its open() resolves with 'opened' right away.
A modal created or opened outside a component is skipped on the server with a warning, and its open() resolves with 'skipped-on-server' right away there. That covers a modal opened from a Nuxt plugin or a route middleware, and a modal a module creates once and opens in setup(): every request would share its open state. In the browser it opens once the app has mounted. Do not await that open() before the app mounts: it settles once the modal has opened.
After an await in setup(), it depends on where the component sits. A component around <ModalsContainer>, such as app.vue, finishes its setup before the container renders, so its modal is still part of the server HTML, as long as it calls useModal() before the await or uses <script setup>. The server does not wait for a component next to the container, such as a page: its modal shows in the browser only, after hydration.