Promise based Dialog in Vue 2
Tags: vue, javascript
Published: 2021-05-18 20:30:40

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:
```js
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.

```vue
<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.
```js
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.

```vue
<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. 
```vue
<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:
```js
    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!