Creating a Universal Functional Modal Utility for Vue 3
This article demonstrates how to build a highly reusable, function‑based modal system in Vue 3 that supports lazy loading, prop passing, event binding, provide/inject data injection, custom slots, and method exposure, allowing developers to display any modal component with a single function call.
The article introduces a functional modal utility for Vue 3 that lets developers show any modal component by simply calling a function, aiming to reduce boilerplate and mental overhead when handling many pop‑up dialogs.
Basic Implementation
A minimal modal component using Ant Design Vue's <a-modal> is presented, followed by a showModal function that creates a container element, determines whether the modal component is asynchronous, defines props, creates a virtual node with h , renders it with render , and cleans up after closing.
import { defineAsyncComponent, h, nextTick, render } from 'vue'
import type { Component } from 'vue'
interface IModalOptions {
modalComponent: Component | any
appendTo?: HTMLElement | string
[name: string]: unknown
}
const getAppendToElement = (appendTo: IModalOptions['appendTo']): HTMLElement => {
let appendToEL: HTMLElement | null = document.body
if (appendTo) {
if (typeof appendTo === 'string') {
appendToEL = document.querySelector
(appendTo)
}
if (appendTo instanceof HTMLElement) {
appendToEL = appendTo
}
if (!(appendToEL instanceof HTMLElement)) {
appendToEL = document.body
}
}
return appendToEL
}
export default function showModal(options: IModalOptions) {
const container = document.createElement('div')
const isAsync = typeof options.modalComponent === 'function'
const modalComponent = isAsync
? defineAsyncComponent(options.modalComponent)
: options.modalComponent
const props: Record
= {}
for (const key in options) {
if (!['modalComponent', 'appendTo'].includes(key)) props[key] = options[key]
}
const vNode = h(modalComponent, {
visible: true,
...props,
'onUpdate:visible': () => {
nextTick(() => {
close()
})
}
})
render(vNode, container)
getAppendToElement(options.appendTo).appendChild(container)
function close() {
render(null, container)
container.parentNode?.removeChild(container)
}
}Injecting Provide Data
Because the modal is mounted to document.body , it loses the component tree context and cannot access provide data via inject . The solution adds a recursive getProvides helper to collect all provides from parent instances and merges them into the modal's instance before rendering.
import { defineAsyncComponent, getCurrentInstance, h, nextTick, render, createVNode } from 'vue'
import type { Component } from 'vue'
function getProvides(instance: any) {
let provides = instance?.provides || {}
if (instance.parent) {
provides = { ...provides, ...getProvides(instance.parent) }
}
return provides
}
export default function useShowModal() {
const currentInstance = getCurrentInstance() as any
const provides = getProvides(currentInstance)
// ... rest of implementation uses provides
}Supporting Custom Slots
The utility is extended with a slots field in IModalOptions . The h function receives these slots, allowing callers to pass default and named slot render functions.
type RawSlots = {
[name: string]: unknown
$stable?: boolean
}
interface IModalOptions {
modalComponent: Component | any
appendTo?: HTMLElement | string
slots?: RawSlots // new field
[name: string]: unknown
}
// usage example
showModal({
modalComponent: () => import('@/pages/home/components/modal.vue'),
title: 'Modal Title',
prop: 'some prop',
slots: {
default: (arg: any) => h('button', arg.type),
footer: (arg: any) => h('button', arg.type)
}
})Exposing Component Methods
A ref is bound to the modal component. For synchronous components the instance is returned directly; for asynchronous components a promise resolves when the ref is populated, enabling callers to invoke exposed methods such as getInfo .
const innerRef = ref()
const vNode = createVNode({
// ...
render: () => h(modalComponent, { ...props, ref: innerRef })
})
if (!isAsync) {
return innerRef.value
} else {
return new Promise(resolve => {
watch(innerRef, () => resolve(innerRef.value), { once: true })
})
}Usage Example
In index.vue a parent component provides a message, calls useShowModal , and triggers the modal on button click. The returned modal instance is used to log injected data.
import { provide, h } from 'vue'
import { message } from 'ant-design-vue'
import useShowModal from '@/utils/useShowModal'
provide('message', 'I am the parent component')
const showModal = useShowModal()
const onClick = async () => {
const Modal = await showModal({
modalComponent: () => import('@/pages/home/components/modal.vue'),
title: 'Modal Title',
prop: 'param',
slots: {
default: (arg: any) => h('button', arg.type),
footer: (arg: any) => h('button', arg.type)
},
onLoadList: () => message.success('Form submitted, list refreshed')
})
console.log(Modal.getInfo())
}Conclusion
The final utility supports lazy loading, prop passing, event binding, provide/inject data injection, custom slots, and method exposure, providing a robust, function‑based approach to handling modal dialogs in Vue 3 applications.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.