Frontend Development 13 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Creating a Universal Functional Modal Utility for Vue 3

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.

typescriptVuelazy-loadingprovide/injectFunctional Componentslotsmodal
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.