vue-layerx Turns Any Vue Dialog Into a One-Line Imperative Call
vue-layerx 1.0.1 is officially released.
Docs: https://xuyimingwork.github.io/vue-layerx/
Repo: https://github.com/xuyimingwork/vue-layerx
When a business page has many pop-ups, the template often ends up looking like this:
- A pile of
xxxVisible/v-modelboilerplate code - Dialog / Drawer and forms bound together in the same file, making content reuse troublesome
- Wanting imperative
open()calls without replacing the existing component library and without writing JSX
If you've stepped into these pitfalls, this can serve as a "practical" solution introduction.
See the Result First: Open a Dialog with Three Lines of Code
const useDialog = createLayer(ElDialog)
const dialog = useDialog(HelloWorld)
dialog.open()
Breaking it down, it's three things:
- Adapter: Use the existing Dialog / Drawer / Popup in your project to generate
useDialog - Bind: Pass in a regular business content component
- Open: Call imperatively, no need to pile up layer templates and
visibleon the page anymore
vue-layerx's stance is clear:
It doesn't touch component libraries; it only provides an imperative API, separating the "layer shell" from the "business content" for orchestration.
Core Model: Container × Content
A single layer has only two roles:
| Responsibility | Example | |
|---|---|---|
| Container | Shell for visibility, mask, title bar, etc. | ElDialog, ElDrawer, project's BaseDialog |
| Content | Regular business component, reusable on pages | UserForm, detail panel |
Corresponding API pipeline:
- createLayer(Container) => configure container
- useDialog(Content) => configure content
- dialog.open() => single open
This separation brings new governance space:
- Container components can be unified at the infrastructure layer (width, mask, close strategy)
- Content components can be reused at the business layer (the same component used inside and outside dialogs)
- The calling page only cares about "who to open, what parameters to pass"
From Adapter to Open: Minimum Viable Path
1. Adapt an Existing Container
// dialog.ts
import { createLayer } from 'vue-layerx'
import { ElDialog } from 'element-plus'
export const useDialog = createLayer(ElDialog, {
props: {
// Configure some default properties
width: '480px',
destroyOnClose: true,
appendToBody: true,
}
})
Element Plus, Ant Design Vue, Vant, or a project's internal BaseDialog all work—as long as the component controls visibility via v-model (or a similar visibility prop).
2. Content is Just a Regular Component
<!-- HelloWorld.vue -->
<template>
<p>Hello World</p>
</template>
3. Open Imperatively When Used
<script setup lang="ts">
import HelloWorld from './HelloWorld.vue'
import { useDialog } from './dialog'
const dialog = useDialog(HelloWorld)
</script>
<template>
<button @click="dialog.open({ props: {
// You can pass parameters to the content here
message: `Hello`
} })">Open Layer</button>
</template>
No more Dialog boilerplate in the page template; no need to maintain visible variables.
Slots Continue Using Templates
The problem with imperative components is that after handing the component to the tool for rendering, there is no component in the template, and no place to insert slots.
Most solutions make you pass JSX nodes, but this increases project complexity.
In vue-layerx, you can use LayerTemplate:
<script setup lang="ts">
import { LayerTemplate } from 'vue-layerx'
import HelloWorld from './HelloWorld.vue'
import { useDialog } from './dialog'
const dialog = useDialog(HelloWorld)
</script>
<template>
<button @click="dialog.open()">Open Layer</button>
<LayerTemplate :to="dialog" name="footer" container>
I will appear in the dialog's footer slot
</LayerTemplate>
</template>
No mandatory JSX, lower learning cost for the team.
Content Components Can Also Pass Templates
It's a bit troublesome to have the caller pass footer content every time, and the business logic isn't self-contained.
vue-layerx also provides defineLayer, allowing content components to pass configuration or slot templates to the container.
<!-- HelloWorld.vue -->
<script setup lang="ts">
import { defineLayer, LayerTemplate } from 'vue-layerx'
const layer = defineLayer({
// Configure the dialog container from the content component
props: { width: '380px' }
})
</script>
<template>
<p>Hello World</p>
<LayerTemplate :to="layer" name="footer">
I will appear in the dialog's footer slot
</LayerTemplate>
</template>
Q: What's the difference between writing it this way and directly writing HelloWorldDialog.vue?
A: The difference is that HelloWorld.vue and ElDialog.vue are separated. HelloWorld.vue can be used as a regular component anywhere on a page, and can be arbitrarily combined with other container components like ElDrawer.vue.
For existing
HelloWorldDialog.vuefiles in a project, vue-layerx provides a progressive adoption plan! See: Container and Content Not Yet Separated
Enterprise-Oriented: Configurations Can Stack, and Can Be "Nailed Down" at the End
In real projects, the same configuration appears in multiple places:
createLayer: Infrastructure defaultsdefineLayer: Content contractuseDialog/open: Caller overrides
vue-layerx allows them to stack, with the convention that later ones have higher priority.
But sometimes a project doesn't want a "default value" but rather "this must be the case after merging"—for example, a site-wide ban on closing by clicking the mask.
In this case, use adapter: after configurations from all places are merged, modify the result one more time.
import type { LayerAdapter } from 'vue-layerx'
import { createLayer } from 'vue-layerx'
import { ElDialog } from 'element-plus'
const enforceMaskNotClosable: LayerAdapter = (config) => ({
...config,
container: {
...config.container,
props: {
...config.container?.props,
closeOnClickModal: false,
},
},
})
export const useDialog = createLayer(ElDialog, {
adapter: enforceMaskNotClosable,
})
Yes, vue-layerx provides AOP capabilities
Other Capabilities
- Ability to wait for dialog results:
await dialog.confirm() - Reactive configuration capability:
useDialog(HelloWorld, () => ({ props: { id: props.id } })) - Context breakage and repair of
Provide / Injectunder imperative calls - Reactive dependency tracking during cross-instance slot transmission with
LayerTemplate - Priority conflicts in configuration merging (infrastructure/content/caller/AOP) under high concurrency scenarios
- Vue devtools content display
Who It's For / Who It's Not For
More suitable for:
- Vue 3 business middle-platforms with many dialogs/drawers
- Already using Element / Ant / Vant / custom shells and don't want to switch libraries
- Wanting form components usable both on pages and opened imperatively
- Needing an infrastructure layer to unify layer strategies (close rules, width, field mapping)
Not necessary for now:
- Projects with only one or two simple Dialogs where the boilerplate is still acceptable
- Teams already deeply bound to a specific "custom Modal runtime" where migration cost outweighs the benefit
Quality Assurance
- Full TypeScript
- Zero runtime external dependencies (peer dependency only requires Vue)
- 100% statement / branch / function / line test coverage, 300+ test cases
- Supports SSR (can be safely introduced in Nuxt / Vite SSR)
- Documentation site + Playground, can click and try directly
I am also the author of vue-asyncx
- Juejin article: Leave Early: Write 40%+ Less Async Code in Vue3
vue-asyncx solves async boilerplate code.
This time, vue-layerx solves layer boilerplate—also compositional, minimally invasive, and reusing your existing code as much as possible.
Try It Now
pnpm add vue-layerx
The documentation site has many clickable examples, feel free to experience them