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<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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptVuelazy loadingProvide/InjectFunctional Component__slots__modal
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

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.