The simplest way to deal with modal dialogs in Vue 3
Tags: javascript, vue, webdev, tutorial
Published: 2022-05-07 12:47:07

Modal dialogs are not such a difficult task to develop. They are often used to confirm user's action. For example, delete any data or perform authorization. This is a very monotonous and uninteresting job with repetitive logic that is sometimes copied from component to component with minor changes.

But what if you have dozens or even hundreds of similar dialogs on one page or even in the entire project? Or if you need to call dialogs in a chain depending on the user's choice? How to make a reusable function which also has readable and maintainable code?

It would be useful to create a function that would take a dialog component and control its rendering in the template. The function would return an object containing the state of the dialog and methods, so that it could be possible to work with it like with promises. For example, this plugin [vue-modal-dialogs](https://github.com/hjkcai/vue-modal-dialogs) implements this functionality. Unfortunately, it has not been updated for a long time and does not support Vue 3.

> Explore the new documentation site and let me know what you think about it! [VCD-docs](https://vcd-docs.netlify.app/)

In this guide I will introduce the [vuejs-confirm-dialog](https://github.com/harmyderoman/vuejs-confirm-dialog) plugin and show how to use it. I'll start with simple examples and end up with a function which completely abstracts the popup dialog to confirm an action and can be used in any component of your project. The examples are written in `JavaScript` for easy reading, but the plugin itself is in `TypeScript`. The plugin is fully typed and documented, which makes it much easier to work with it. Unlike `vue-modal-dialogs`, this plugin has additional functionality - special hooks: `onConfirm` and `onCancel`. They accept a callback and are called depending on the user's decision: `onConfirm` if the user agrees and `onCancel` if he refuses.

The end result can be seen in the [sandbox](https://codesandbox.io/s/vibrant-stonebraker-ml8tms?file=/src/App.vue). The code is slightly different from what is in the post.

## Installation

Let's start by creating a new Vue 3 project. In the console:

```bash
vue create dialogs-guide
 
// Pick a second option
 
? Please pick a preset:
  Default ([Vue 2] babel, eslint)
> Default (Vue 3) ([Vue 3] babel, eslint)
  Manually select features
```

We have received a standard template for a new project. Next, go to the project folder and install the library according to the documentation in [README.md](https://github.com/harmyderoman/vuejs-confirm-dialog/blob/main/README.md).

```bash
npm i vuejs-confirm-dialog
```

Replace the code in `main.js` with this:

```javascript
import { createApp } from 'vue'
import App from './App.vue'
import * as ConfirmDialog from 'vuejs-confirm-dialog'
 
createApp(App).use(ConfirmDialog).mount('#app')
```

## Usage

Now let's move on to the App.vue file. Let's fix the template code first. We have to add the `<DialogsWrapper/>` component to the template, to make the plugin work, and remove the `HelloWord`:

```html
<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <DialogsWrapper />
</template>
```

Now let's learn how to use the `createConfirmDialog` function. Use the new `setup` syntax for the `script` section. `createConfirmDialog` accept as the first argument a component that will be the modal dialog and the second will be an object which contains component props values. The function returns an object with methods for working with the modal window, so the `reveal` method renders the dialog box and `onConfirm` accepts the callback which will be called if the user clicks "agree". You can create a `HelloWord` component using the logo and pass the value of the `msg` prop:

```html
// App.vue
<template>
  <img alt="Vue logo" src="./assets/logo.png" @click="reveal">
  <DialogsWrapper />
</template>
 
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import { createConfirmDialog } from 'vuejs-confirm-dialog'
 
const { reveal } = createConfirmDialog(HelloWorld, { msg: 'Hi!'})
</script>
```

No additional logic is required. The component is rendered after calling the `reveal` function and disappears after the user responds to the dialog.

## Real life example

Now let's write something closer to real use.

Let's create a new component `SimpleDialog.vue` in the `components` folder:

```html
<template>
  <div class="modal-container">
    <div class="modal-body">
      <span class="modal-close" @click="emit('cancel')">🗙</span>
      <h2>{{ question }}</h2>
      <div class="modal-action">
        <button class="modal-button" @click="emit('confirm')">Confirm</button>
        <button class="modal-button" @click="emit('cancel')">Cancel</button>
      </div>
    </div>
  </div>
</template>
 
<script setup>
  import { defineProps, defineEmits } from 'vue'
 
  const props = defineProps(['question'])
  const emit = defineEmits(['confirm', 'cancel'])
</script>
 
<style>
  .modal-container {
    display: flex;
    justify-content: center;
    align-items: center;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    height: 100%;
    background-color: #cececeb5;
  }
  .modal-body {
    background-color: #fff;
    border: 2px solid #74a2cf;
    border-radius: 10px;
    text-align: center;
    padding: 20px 40px;
    min-width: 250px;
    display: flex;
    flex-direction: column;
  }
  .modal-action {
    display: flex;
    flex-direction: row;
    gap: 40px;
    justify-content: center;
  }
  .modal-button {
    cursor: pointer;
    height: 30px;
    padding: 0 25px;
    border: 2px solid #74a2cf;
    border-radius: 5px;
    background-color: #80b2e4;
    color: #fff;
  }
  .modal-close {
    cursor: pointer;
    position: relative;
    align-self: end;
    right: -33px;
    top: -17px;
  }
</style>
 
```

Notice that two incoming events need to be added to the modal dialog to work properly: `['confirm', 'cancel']`.

And now we use it to confirm an action, for example, to hide the logo. The logic of the code, which will be executed after the user's consent, will be placed in the `onConfirm` hook callback.

```html
<template>
  <img v-show="showLogo" alt="Vue logo" src="./assets/logo.png">
  <button @click="reveal">Hide Logo</button>
 
  <DialogsWrapper />
</template>
 
<script setup>
import SimpleDialog from './components/SimpleDialog.vue'
import { createConfirmDialog } from 'vuejs-confirm-dialog'
import { ref } from 'vue'
 
const showLogo = ref(true)
 
const { reveal, onConfirm } = createConfirmDialog(SimpleDialog, { question: 'Are you sure you want to hide the logo?'})
 
onConfirm(() => {
  showLogo.value = false
})
</script>
```

## Reusing

What if we have many cases where confirmation of some action is required? Do we need to call `createConfirmDialog` again every time?

No. It is possible to write a function which automates the process for us.

```javascript
// src/composables/useConfirmBeforeAction.js
import SimpleDialog from './../components/SimpleDialog'
import { createConfirmDialog } from 'vuejs-confirm-dialog'
 
const useConfirmBeforeAction = (action, props) => {
  const { reveal, onConfirm } = createConfirmDialog(SimpleDialog, props)
 
  onConfirm(action)
 
  reveal()
}
 
export default useConfirmBeforeAction
```

Now we use it to confirm the following external links:

```html
// App.vue
 
<template>
  <ul>
    <li v-for="(link, i) in LINKS" @click="goToLink(link)" :key="i">
      {{ link }}
    </li>
  </ul>
  <DialogsWrapper />
</template>
 
<script setup>
  import useConfirmBeforeAction from './composables/useConfirmBeforeAction'
 
  const LINKS = [
    'https://vuejs.org/',
    'https://github.com/',
    'https://vueuse.org/',
  ]
 
  const goToLink = (link) => {
    useConfirmBeforeAction(
      () => {
        window.location = link
      },
      { question: `Do you want to go to ${link}?` }
    )
  }
 
</script>
```

## Conclusion

The `createConfirmDialog` function makes it easier to work with modal windows, reuse logic, and create chains of sequential dialogs. It takes care of rendering the modal window, passing incoming parameters to the component, and receiving data from it. It is very flexible and easy to customize it according to your needs.

These are not all its possibilities. For example, if the concept of hooks is not close to you, you can replace them with a promise that returns `reveal`.

And of course, for a better dev experience use it with TypeScript!