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<HTMLElement>(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<string, any> = {}
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
