Dialogs visually exist "outside" application, and because of it, never really felt right for me to include them in places where they don't belong to. HTML regarding Dialogs is often placed in the root of the application or in the components where they are called from, and then, usually by portals, transferred to the top. Logic, which is controlling which dialog should pop up and when, is also, either in store or component, or maybe have its own service. Sometimes logic meant to control dialogs is lacking in features, and then, oops, we cannot open dialog inside another dialog. Too bad if we need it.
I feel like we can solve all the issues with simply handling dialogs as a function. We want dialog? Let's call it, and as a parameter put the component we want to display. We can wrap it in a promise, so we know exactly when the dialog is closed and with what result, and then make some calls based on that.
To visualize how I imagine working with that I made snippet below:
const success = await openDialog(ConfirmDialog)
if (success) {
this.fetchData()
}
The benefit of doing all the logic regarding dialogs by ourselves is that we have full control over this, we can add new features based on our needs, and make our dialogs look however we want. So, let's build it.
First, we need to create Dialog Wrapper component. Its purpose is to provide basic styles and some logic for closing the dialog.
<template>
<div class="dialog-container">
<span class="dialog-mask" @click="$emit('close')"></span>
<component :is="dialogComponent" @close="response => $emit('close', response)"
v-bind="props"/>
</div>
</template>
<script>
export default {
name: 'DialogWrapper',
props: ['dialogComponent', 'props']
}
</script>
<style>
.dialog-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1001;
}
.dialog-mask {
position: fixed;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
}
</style>
You can change styles so it fits you. You can also add additional logic, we can add animations and other features, but I wanted to keep it simple. You will be getting two props, dialogComponent
and props
(confusing, I know).
- dialogComponent is Vue component which will be rendered inside
- props are props passed to
dialogComponent
You close dialog by emitting event close, and if you want to pass a value which will be used when resolving a promise - you pass data with the event, e.g. $emit('close', 'success!')
.
Now let's make a function.
export function openDialog (dialogComponent, props) {
return new Promise((resolve) => {
const Wrapper = Vue.extend(DialogWrapper)
const dialog = new Wrapper({
propsData: {
dialogComponent,
props,
},
router, // optional, instance of vue router
store, // optional, instance of vuex store
}).$mount()
document.body.appendChild(dialog.$el);
dialog.$on('close', function (value) {
dialog.$destroy();
dialog.$el.remove();
resolve(value)
})
})
}
It will create a new Vue instance and append it to document.body
. It will use DialogWrapper
as main component, and will pass function parameters as props by using propsData
property. It will also listen for close
event to know where to destroy itself.
It's important to add router
and store
property when initializing component, if you're using it, because otherwise your components will have no access to $store
and $router
.
So we have our dialog function working, but I cut a lot of code I'm using for conveniance of this article, and leave only the core logic. It's good idea to create another component - let's call it DialogLayout
, which will create actual white box with some padding. You can, if you want, put some more effort in that; for example, adding dialog title or close button.
<template>
<div class="dialog-content">
<slot></slot>
</div>
</template>
<style scoped>
.dialog-content {
width: 60%;
position: relative;
margin: 100px auto;
padding: 20px;
background-color: #fff;
z-index: 20;
}
</style>
Now, we can move into testing part of the article.
Let's create example component which we will later pass as a openDialog
parameter.
<template>
<DialogLayout>
<button @click="$emit('close', 'wow! success')">Close dialog</button>
</DialogLayout>
</template>
It has button which will close the dialog with resolved value 'wow! success
. It also uses DialogLayout
for some basic styling.
Somewhere in our application we can call our function:
async onBtnClick () {
const result = await openDialog(DialogExample)
// dialog is now closed
console.log(result) // 'wow! success'
}
Although it requires some initial configuration, payback is huge. I'm using it for years now and it fits my needs perfectly. It's also easy to extend with additional features.
It's important to note, that this dialog will not be animated. Animation can be added quite easily, but it's beyond scope of this article.
Thanks a lot for reading, and in case of any questions, please write comment or send me an email - iam.adam.kalinowski@gmail.com. Have a nice day!
Top comments (4)
Thanks for the article, got one question though: why not using teleport feature? v3.vuejs.org/guide/teleport.html
I just recently used the teleport feature for the first time and yah it solves all these issues. The only issue I had was the veuter plugin didn't have support for vue3 yet and eslint made a red wall of my code. Easily fixed with this though: dev.to/mikhailkaran/setting-up-esl...
I guess it comes down to preference, but with teleport you still have to maintain state regarding visibility of modals. Additionally, in Vue 2 you also don't have built-in teleport feature, so you will have to install additional dependency. But the main reason for me - it just feels easier to open dialogs through special function, than to work with teleports. The code feels cleaner too, as I really hate seeing HTML for dialogs in some random places.
Thanks for the article.
I just released a small Vue 3 library that makes it easy to deal with dialogs using promises : github.com/rlemaigre/vue3-promise-... . Some people landing here from Google might find it useful.
And I agree with the author, once you start working with dialogs in this manner, there is no going back.