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