DEV Community

Cover image for The simplest way to deal with modal dialogs in Vue 3

The simplest way to deal with modal dialogs in Vue 3

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 implements this functionality. Unfortunately, it has not been updated for a long time and does not support Vue 3.

In this guide I will introduce the 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. 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:

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

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.

npm i vuejs-confirm-dialog
Enter fullscreen mode Exit fullscreen mode

Replace the code in main.js with this:

import { createApp } from 'vue'
import App from './App.vue'
import * as ConfirmDialog from 'vuejs-confirm-dialog'

createApp(App).use(ConfirmDialog).mount('#app')
Enter fullscreen mode Exit fullscreen mode

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:

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <DialogsWrapper />
</template>
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

<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>

Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

Now we use it to confirm the following external links:

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

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!

Discussion (0)