vue-layerx Decouples Vue Popups into Route-Like Imperative Calls and Container-Free Content
Foreword
When writing popups in Vue, aren't you already fed up with these two "awkward" problems that have tormented frontend developers for so long?
- Cumbersome invocation:
- Every time you use a popup, you must go through the rigid chain of "declaring a
visiblestate -> pre-embedding the component in the template -> binding show/hide". - To dynamically pop up different popups after a click, you generally pre-write all possible popups in the template in advance, which is "fake" dynamism.
- Every time you use a popup, you must go through the rigid chain of "declaring a
- Difficult reuse:
- The popup component and business content are written in the same component, making it very troublesome to reuse the business content.
- Similarly, if the business requires changing the popup to a drawer, it is also very troublesome.
We just want to display some business content after a click. Why do we have to write so heavily?
1. Cut the Crap: See the Gap with Two Sets of Code Comparisons
Invocation Comparison (Calling Page)
In the traditional approach, you need to import a bunch of specific popup monolithic components, pre-embed them in the template, and maintain a set of show/hide variables and callback logic for each popup.
❌ Traditional Declarative Approach (The caller is held hostage by a bunch of specific popups and states)
<!-- TraditionalParent.vue -->
<template>
<div class="page-container">
<el-button @click="openCreate">Create User</el-button>
<el-button @click="openEdit(currentRow)">Edit User</el-button>
<!-- Must pre-embed various specific popups in the template, declaring a bunch of visible variables -->
<UserDialog
v-model="isUserDialogVisible"
:mode="modalMode"
:data="currentFormData"
@success="handleUserSuccess"
/>
<!-- If there are other businesses, you must continue piling up other Dialog pre-embeds... -->
<!-- <AuthDialog v-model="isAuthVisible" /> -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import UserDialog from './UserDialog.vue' // Import a specific popup component tightly bound to the container
// Declare some variables solely for the popup
const isUserDialogVisible = ref(false)
const modalMode = ref<'create' | 'edit'>('create')
const currentFormData = ref(null)
const openCreate = () => {
modalMode.value = 'create'
currentFormData.value = null
isUserDialogVisible.value = true
}
const openEdit = (row) => {
modalMode.value = 'edit'
currentFormData.value = row
isUserDialogVisible.value = true
}
const handleUserSuccess = () => {
isUserDialogVisible.value = false
// Subsequent logic like refreshing the list...
}
</script>
New Approach (Zero Component Pre-embedding, Call on Demand)
<!-- LayerxParent.vue -->
<template>
<div class="page-container">
<!-- The template is extremely clean, without importing any specific XxxDialog components or any state boilerplate code -->
<el-button @click="userDialog.open({ props: { mode: 'create' } })">
Create User
</el-button>
<!-- Just one line of code, can be placed in the template, invoked instantly with parameters, the experience is like route navigation -->
<el-button @click="row => userDialog.open({ props: { mode: 'edit', data: row } })">
Edit User
</el-button>
</div>
</template>
<script setup lang="ts">
import { useDialog } from '@/composables/dialog' // Team's unified popup composable factory
import UserForm from './UserForm.vue' // Note: Directly import the pure business content component here!
// Bind the pure business form to generate a popup scheduling instance
const userDialog = useDialog(UserForm, {
props: {
onSuccess() {
// Subsequent logic like refreshing the list...
}
}
})
</script>
Business Popup Comparison (Content Component)
Traditional development habits weld the container (like el-dialog) and the internal form logic together in the same UserDialog.vue monolithic file.
❌ Traditional Highly Coupled Monolithic Popup (Form and popup welded together, cannot be reused)
<!-- UserDialog.vue -->
<template>
<!-- The popup component is hardcoded at the top level of the business file, making it impossible to embed directly in-page outside the popup environment -->
<el-dialog
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
:title="mode === 'create' ? 'Create User' : 'Edit User'"
width="480px"
>
<!-- The form is mixed inside the popup monolith, integrated with el-dialog -->
<el-form :model="formData">
<el-form-item label="Username"><el-input v-model="formData.name" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="$emit('update:modelValue', false)">Cancel</el-button>
<el-button type="primary" @click="handleSubmit">Confirm</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ modelValue: boolean; mode: 'create' | 'edit'; data?: any }>()
const emit = defineEmits(['update:modelValue', 'success'])
const formData = ref({ name: '' })
const handleSubmit = async () => {
await saveUser(formData.value)
emit('success')
// Business logic mixed with popup logic (interfering code)
emit('update:modelValue', false)
}
</script>
It's clearly just business content, but there's always some popup code.
New Approach (Pure Business Content, Container Separated, Reusability Doubled)
<!-- UserForm.vue -->
<template>
<!-- This is just a 100% pure business form component, without any popup container! It naturally supports direct in-page embedding! -->
<el-form :model="formData">
<el-form-item label="Username"><el-input v-model="formData.name" /></el-form-item>
</el-form>
<!-- Cross-space-time slot delivery: Use familiar template syntax to dynamically deliver buttons to the footer of the future external container -->
<LayerTemplate :to="layer" name="footer">
<el-button @click="$emit('cancel')">Cancel</el-button>
<el-button type="primary" @click="handleSubmit">Confirm</el-button>
</LayerTemplate>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { defineLayer, LayerTemplate } from 'vue-layerx'
// Use defineLayer to declare the popup container configuration "when this component is opened as a popup"
// All popup-related code is consolidated in one place
const layer = defineLayer({
props: { title: 'User Info', width: '480px' },
// When the form completes its business and reports these events externally, the popup is automatically destroyed
content: { closeOn: ['success', 'cancel'], },
})
const emit = defineEmits(['success', 'cancel'])
const formData = ref({ name: '' })
const handleSubmit = async () => {
await saveUser(formData.value)
// Pure business logic, only needs to report the business result externally, no need to know whether it's a popup or something else
emit('success')
}
</script>
Less code, more aggregated logic, higher reusability.
We believe that Confirm and Cancel belong to the content component to make the business logic complete. To pass rendered content from the content component to the popup container without forcing the use of JSX, we designed the
LayerTemplatecomponent, which works just like the familiar<template #footer>.
2. Core Breakthrough: "Route-Style" Dynamic Invocation and "De-Containerized" Content
The new tool vue-layerx completely breaks the mental shackles of traditional declarative popups, reshaping the paradigm around two core pain points:
1. Route-Style Invocation: Zero Pre-embedding, On-Demand Scheduling at Runtime
In the traditional approach, the parent page must do a lot of useless "placeholder" work for popups: import components, paste tags in the template, declare visible variables. This is a "rigid static pre-embedding".
vue-layerx transforms this into "route-style dynamic invocation". The parent page does not need to know in advance what it will pop up in the future; the template is completely blank. This is just like Vue Router—you don't pre-embed all the sub-pages you want to navigate to in the page template; you just write a line of router.push('/profile') in JS. Similarly, here you only need one line of userDialog.open().
Turning popup invocation into imperative scheduling like route navigation gives the parent page perfect cleanliness and absolute dynamic expansion capability.
2. De-Popup-ization: Write Only Pure Business Forms, Not a Single Line of "Popup Code"
The reason traditional popups are hard to reuse is that we weld the form and the el-dialog container together.
vue-layerx advocates "de-popup-containerization of content". The file you create (like UserForm.vue) is a 100% pure business form component, and it shouldn't even know that it will be placed inside a popup in the future.
- No popup tags: No
el-dialogat the top level, only pureel-form. - No interfering logic: After a successful submission, just rightfully trigger the business report
emit('success').
The interaction with the external popup container is consolidated into the declarative defineLayer configuration:
const layer = defineLayer({
props: { title: 'User Info', width: '480px' },
content: { closeOn: ['success'] }, // Contract: When the form successfully reports the business result, the external pipeline automatically destroys the popup
})
The content asset is completely stripped from the popup container, giving it terrifying freedom: today it's an imperative popup, tomorrow it can be directly thrown into a regular page as a normal component, or mounted as a Drawer, achieving truly doubled reusability.
3. Future-Oriented: Why This Is Infrastructure for the AI Era?
If we shift our gaze away from rigid traditional back-offices and look at the AI intelligent interaction systems (like Agent conversation interfaces, LUI) that are undergoing massive changes right now, you will find that the traditional template pre-embedding approach has completely failed, and the new paradigm is shifting from a "nice-to-have" to an "absolute necessity".
1. Runtime Intent-Driven Dynamic Invocation
In the AI era, the interaction flow between the user and the system is entirely determined by the large model's intent. The user says: "Help me approve this batch of orders, and by the way, change the supplier for the second order." At this point, the AI will dynamically analyze during execution and decide to pop up a supplier modification form in the conversation flow.
In this process, the frontend cannot predict at which second the user will trigger which business popup. You cannot mechanically import and pre-embed all dozens of possible XxxDialogs in the project into the calling page template. Only through dynamic imperative scheduling like dialog.open({ component: UserForm }), mounting in real-time at runtime based on the large model's invocation intent, can you perfectly interface with the AI's ever-changing execution flow.
2. Extremely High Fluidity with Completely Uncertain Hosts
In intelligent interfaces, the rendering "host" of the same business form (like UserForm.vue) changes every second:
- It might be directly embedded as a card in the AI chat bubble flow.
- It might need to be scheduled by the AI as a global Dialog popup because there are too many fields.
- Under narrow screens or mobile interactions, it needs to expand sideways as a Drawer.
If you use the traditional coupled monolith, you have to copy almost identical code for these three hosts, or write disgusting environment checks inside the component. But under the vue-layerx paradigm, UserForm.vue is 100% pure business. Whether it is stuffed into a chat flow, thrown into a Dialog, or mounted into a Drawer is entirely up to the host scheduler. The fluidity of content directly determines the R&D efficiency of interfaces in the AI era.
4. Conclusion
vue-layerx is not just a simple imperative popup tool, but a whole new engineering paradigm attempting to combat the "awkwardness of popups".
During the design process, we fully obsessed over a series of deep business area features such as mental model preservation by refusing JSX, reactive parameter passing, progressive migration for large projects, multi-layer context binding, instant component destruction mechanisms, HMR updates and DevTools debugging, and multi-level configuration override logic. If you are interested in these features and the architectural design thinking behind them, welcome to read:
- Enterprise-Grade Imperative Popup Solution: Adapt Existing Dialogs with Three Lines of Code
- What to Consider When Building an Imperative Popup Tool—Why I Designed vue-layerx This Way
For more best practices and implementation guides, welcome to visit:
From today on, don't let rigid show/hide states and specific component pre-embeddings hold your parent pages hostage anymore. Let's return invocation to route-style imperative scheduling, leave content to pure business form components, and build a more decoupled, future-oriented modern frontend popup layer infrastructure with less code!
Top 4 of 26 from juejin.cn, machine-translated. The original thread is authoritative.
The way I understand popups, it should be exactly like this: internally you can directly access slots via el-dialog and so on, rather than separating the popup from the business logic... So when I use imperative components, I don't need an extra slot passed in from outside; whatever slot I need, I just write it directly inside the component's el-dialog... Then externally, you just use the entire UserForm to directly open the whole el-dialog.
The problem with this approach is that the business content and the popup container are tightly coupled. If I want to extract the business content separately, it's very difficult (for example, laying out the popup content inline within a detail view), and in dynamic scenarios — like displaying a dialog as a drawer in an approval flow — it's also very hard to do.
Just extract the business content into a separate component, that's it...
I tried it out, and now I pretty much understand. Your thinking is actually quite similar to mine, it's just that the slot abstraction you have is too abstract. Essentially, it's still a popup internally emitting events, or a business component internally emitting events for the outside to receive. The way your business component decides what the confirm and cancel button slots inside the popup look like, and what events they bind — that's really abstract, super abstract.
Because I initially kept thinking that, say, a business component decides what its slots look like inside Popup A. If after clicking confirm in Popup A you need to open Popup B, then your business component would have to not only place Popup A's slots but also put the logic for opening Popup B. I thought that logic was way too abstract. Now it seems your underlying logic is actually similar to mine — also exposing things through events, meaning the data inside the business component or the data inside the popup. That's actually fine. It's just that your slot thing is really too abstract.
Apart from that slot thing, the rest of the design seems pretty good. So how do you handle the provide and inject inside the popup?
I stared at this for ages and finally understood why it's written this way... My understanding of a popup is like this.
This article has a bit of a marketing flavor, but I think the value of the vue-layerx tool goes far beyond imperative popups — it's a new way of writing popups, so I hope to share it with more friends. I believe popup invocation shouldn't be reactive; popups should be like routes — a content-agnostic presentation method. From the way various popup component libraries unanimously offer imperative invocation for lightweight components like ElMessage and ElMessageBox, to the fact that nearly all popups provide a destroy-on-close configuration — these point to two things: imperative invocation is more intuitive than declarative invocation, and popups are content-agnostic. It's just that this pain point isn't severe enough, and solving it requires a certain level of technical skill, so everyone just let it slide. But starting today, we can tackle it head-on.