Frontend Development 16 min read

Elegant Dialog Invocation in Vue: A Scalable Architecture for Configurable Pop‑ups

This article presents a scalable Vue architecture that separates generic dialog logic from component‑specific configuration panels, using dynamic creation with createApp and Element‑Plus dialogs to achieve high cohesion, low coupling, and easy maintenance for numerous low‑code components.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Elegant Dialog Invocation in Vue: A Scalable Architecture for Configurable Pop‑ups

Introduction: The article explains how to invoke dialogs elegantly in Vue, focusing on a dialog‑based configuration pattern for low‑code platforms.

Requirement background: It describes two interaction patterns—side‑panel and dialog‑based configuration—and the challenges of scaling many component dialogs.

Structure design: A new architecture is proposed that separates dialog UI (Panel.vue) from its entry (Dialog/index.js) and uses a BaseDialog wrapper, with a unified component map for dynamic loading.

Directory layout:

src/
├── components/
│   ├── BarChart/
│   │   ├── Dialog/
│   │   │   ├── index.js   # Dialog entry
│   │   │   ├── Panel.vue  # UI view
│   │   ├── Drag.vue
│   │   └── index.js
│   ├── LineChart/
│   │   ├── Dialog/
│   │   │   ├── index.js
│   │   │   ├── Panel.vue
│   │   ├── Drag.vue
│   │   └── index.js
│   ├── PieChart/
│   │   ├── Dialog/
│   │   │   ├── index.js
│   │   │   ├── Panel.vue
│   │   ├── Drag.vue
│   │   └── index.js
│   ├── BaseDialog.vue
│   └── index.js
├── utils/
│   ├── BaseControl.js
│   └── dialog.js
└── App.vue

Component entry (e.g., BarChart/Dialog/index.js) registers a static create method that calls dialogWithComponent to render the Panel.vue inside an Element‑Plus el-dialog :

import Panel from "./Panel.vue";
import { dialogWithComponent } from "../../../utils/dialog.js";

Panel.create = async (panelProps = {}) => {
  return dialogWithComponent((render) => render(Panel, panelProps), {
    title: panelProps.label,
    width: "400px",
  });
};

export default Panel;

Panel.vue contains only the specific UI and exposes a getValue method for the parent to retrieve configuration data:

<template>
  <h1>Bar Chart Configuration</h1>
</template>

<script setup>
defineExpose({
  async getValue() {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return { type: "barChart" };
  },
});
</script>

The utility dialogWithComponent creates a temporary Vue app, mounts a BaseDialog wrapper, handles confirm/cancel, and resolves a promise with the data returned by the panel’s getValue method:

export function dialogWithComponent(ContentComponent, dialogProps = {}) {
  return new Promise((resolve) => {
    const container = document.createElement("div");
    document.body.appendChild(container);
    let vm = null;
    let loading = ref(false);
    const dialogRef = ref(null);
    const contentRef = ref(null);

    const unmount = () => {
      if (vm) {
        vm.unmount();
        vm = null;
      }
      document.body.removeChild(container);
    };
    const confirm = async () => {
      let result = {};
      const instance = contentRef.value;
      if (instance && instance.getValue) {
        loading.value = true;
        try {
          result = await instance.getValue();
        } catch (error) {
          typeof error === "string" && ElMessage.error(error);
          loading.value = false;
          return;
        }
        loading.value = false;
      }
      unmount();
      resolve(result);
    };

    vm = createApp({
      render() {
        return h(
          BaseDialog,
          {
            ref: dialogRef,
            modelValue: true,
            loading: loading.value,
            onDialogConfirm: confirm,
            onDialogCancel: unmount,
            ...dialogProps,
          },
          {
            default: () => createVNode(h, ContentComponent, contentRef),
          }
        );
      },
    });
    vm.mount(container);
  });
}

The helper createVNode upgrades Vue’s h function to automatically inject a ref, allowing the dialog utility to access the panel component instance:

export function createVNode(h, Component, ref = null) {
  if (!Component) return null;
  let instance = null;
  const render = (type, props = {}, children) =>
    h(type, { ...props, ref: (el) => { if (ref) ref.value = el; } }, children);
  if (typeof Component === "function") {
    instance = Component(render);
  } else {
    instance = render(Component);
  }
  return instance;
}

BaseDialog.vue provides a generic el-dialog with optional footer buttons and emits confirm/cancel events:

<template>
  <el-dialog v-bind="dialogAttrs">
    <slot></slot>
    <template v-if="showFooter" #footer>
      <span>
        <template v-if="!$slots.footer">
          <el-button @click="handleCancel">Cancel</el-button>
          <el-button type="primary" :loading="loading" @click="handleConfirm">Confirm</el-button>
        </template>
        <slot v-else></slot>
      </span>
    </template>
  </el-dialog>
</template>

<script setup>
import { useAttrs, computed } from "vue";
import { ElDialog, ElButton } from "element-plus";
defineProps({
  showFooter: { type: Boolean, default: true },
  loading: { type: Boolean, default: false },
});
const emit = defineEmits(["dialogCancel", "dialogConfirm"]);
const attrs = useAttrs();
const dialogAttrs = computed(() => ({ ...attrs }));
function handleCancel() { emit("dialogCancel"); }
function handleConfirm() { emit("dialogConfirm"); }
</script>

Usage in App.vue demonstrates a button list that triggers openDialog(type) , which dynamically creates the appropriate dialog via the component map:

<template>
  <el-button type="primary" v-for="type in componentList" :key="type" @click="openDialog(type)">
    {{ type }}
  </el-button>
</template>

<script setup>
import { ElButton } from "element-plus";
import { componentMap } from "./components";

const componentInstanceMap = Object.keys(componentMap).reduce((pre, key) => {
  const instance = new componentMap[key]();
  pre[key] = instance;
  return pre;
}, {});

async function openDialog(type) {
  const component = await componentMap[type].DialogComponent.create(
    { type },
    componentInstanceMap[type]
  );
  console.log("component", component);
}
</script>

The new architecture achieves high cohesion and low coupling, making dialog management scalable, maintainable, and easy to extend across many component types.

VueFrontend ArchitectureElement-PlusDialogDynamic Component
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.