Promise based Dialog in Vue 2

hardyng

Adam Kalinowski

Posted on May 18, 2021

Promise based Dialog in Vue 2

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()
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'
    }
Enter fullscreen mode Exit fullscreen mode

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!

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
hardyng
Adam Kalinowski

Posted on May 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About