Introduction
A confirmation dialogue is a UI pattern where the user will be given a choice to continue with their action or to cancel it. It is commonly used with destructive or irreversible actions, to make sure that the user would indeed want to proceed.
In this article, we'll be implementing a reusable and modular confirmation dialogue in Vue.js.
Creating a Reusable Popup Component
Let's start by creating a reusable base component for any kind of popup components. That way we don't have to re-implement the popup mechanics multiple times. This can later be reused to create anything from an alert box to a newsletter popup.
Let's start off with the template:
<!-- components/PopupModal.vue -->
<template>
<transition name="fade">
<div class="popup-modal" v-if="isVisible">
<div class="window">
<slot></slot>
</div>
</div>
</transition>
</template>
Notice we added an empty <slot></slot>
tag to the template. This tag allows us to insert any content into the PopupModal
element at the <slot></slot>
tag. To read more about how slots works, see the Vue guide on slots.
We've also added the <transition name="fade">
tag to the template. We'll use this in the next section to animate a fade-in/fade-out effect on the dialogue.
Then, we'll add the data()
, open()
and close()
event functions:
<!-- components/PopupModal.vue -->
<script>
export default {
name: 'PopupModal',
data: () => ({
isVisible: false,
}),
methods: {
open() {
this.isVisible = true
},
close() {
this.isVisible = false
},
},
}
</script>
And finally, let's add some style to it:
<!-- components/PopupModal.vue -->
<style scoped>
/* css class for the transition */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.popup-modal {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 0.5rem;
display: flex;
align-items: center;
z-index: 1;
}
.window {
background: #fff;
border-radius: 5px;
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
max-width: 480px;
margin-left: auto;
margin-right: auto;
padding: 1rem;
}
</style>
Confirmation Dialogue Animation
In the template tag you will see a transition tag <transition name="fade">
. This is used to animate simple enter/leave states. Anything inside this tag will animate if it was added or removed from the tag.
We are using a conditional v-if="isVisible"
to hide and show the popup. You can read more about it at the Vue guide on transitions.
To specify how the content transitions, we've called our animation fade
. To implement this transition in CSS, we'll add classes prefixed with the name fade
, matching our name
attribute of the <transition>
tag.
All it does is animate the opacity when the popup closes and opens:
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
Inheriting the Popup Component
To create our confirmation dialogue, we will inherit the PopupModal
by composition, and customize the reusable modal window to become a confirmation dialogue.
Let's create a new file, components/ConfirmDialogue.vue
and define a template inside of it:
<!-- components/ConfirmDialogue.vue -->
<template>
<popup-modal ref="popup">
<h2 style="margin-top: 0">{{ title }}</h2>
<p>{{ message }}</p>
<div class="btns">
<button class="cancel-btn" @click="_cancel">{{ cancelButton }}</button>
<span class="ok-btn" @click="_confirm">{{ okButton }}</span>
</div>
</popup-modal>
</template>
Because we defined the <slot></slot>
tag in the popup-modal
component, everything that we put between its component tags (<popup-modal></popup-modal>
) will be rendered between its <slot>
tags instead.
We also added a ref="popup"
to the popup-modal
tag. By setting that attribute, we can now access the popup-modal
instance with this.$refs.popup
. We'll use that reference to call open()
and close()
on the popup modal.
Then, let's implement the methods from the parent component:
<!-- components/ConfirmDialogue.vue -->
<script>
import PopupModal from './PopupModal.vue'
export default {
name: 'ConfirmDialogue',
components: { PopupModal },
data: () => ({
// Parameters that change depending on the type of dialogue
title: undefined,
message: undefined, // Main text content
okButton: undefined, // Text for confirm button; leave it empty because we don't know what we're using it for
cancelButton: 'Go Back', // text for cancel button
// Private variables
resolvePromise: undefined,
rejectPromise: undefined,
}),
methods: {
show(opts = {}) {
this.title = opts.title
this.message = opts.message
this.okButton = opts.okButton
if (opts.cancelButton) {
this.cancelButton = opts.cancelButton
}
// Once we set our config, we tell the popup modal to open
this.$refs.popup.open()
// Return promise so the caller can get results
return new Promise((resolve, reject) => {
this.resolvePromise = resolve
this.rejectPromise = reject
})
},
_confirm() {
this.$refs.popup.close()
this.resolvePromise(true)
},
_cancel() {
this.$refs.popup.close()
this.resolvePromise(false)
// Or you can throw an error
// this.rejectPromise(new Error('User canceled the dialogue'))
},
},
}
</script>
Finally, let's add some style to it to make it look a bit nicer:
<!-- components/ConfirmDialogue.vue -->
<style scoped>
.btns {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.ok-btn {
color: red;
text-decoration: underline;
line-height: 2.5rem;
cursor: pointer;
}
.cancel-btn {
padding: 0.5em 1em;
background-color: #d5eae7;
color: #35907f;
border: 2px solid #0ec5a4;
border-radius: 5px;
font-weight: bold;
font-size: 16px;
text-transform: uppercase;
cursor: pointer;
}
</style>
Using the Confirmation Dialogue
To use the confirmation dialogue, you need to include only the components/ConfirmDialogue.vue
component. For example, let's make a page with a "Delete" button that makes sure if you really want to delete another page:
<template>
<div>
<h1>Delete Page</h1>
<button class="delete-btn" @click="doDelete">Delete Page</button>
<confirm-dialogue ref="confirmDialogue"></confirm-dialogue>
</div>
</template>
<script>
import ConfirmDialogue from '../components/ConfirmDialogue.vue'
export default {
components: { ConfirmDialogue },
methods: {
async doDelete() {
const ok = await this.$refs.confirmDialogue.show({
title: 'Delete Page',
message: 'Are you sure you want to delete this page? It cannot be undone.',
okButton: 'Delete Forever',
})
// If you throw an error, the method will terminate here unless you surround it wil try/catch
if (ok) {
alert('You have successfully delete this page.')
} else {
alert('You chose not to delete this page. Doing nothing now.')
}
},
},
}
</script>
<style scoped>
.delete-btn {
padding: 0.5em 1em;
background-color: #eccfc9;
color: #c5391a;
border: 2px solid #ea3f1b;
border-radius: 5px;
font-weight: bold;
font-size: 16px;
text-transform: uppercase;
cursor: pointer;
}
</style>
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Since we are using await
in our method to get the result of the confirmation dialogue, we need to add async
to our method definition.
Alternatively, you may prefer the promise style approach:
this.$refs.confirmDialogue.show({
title: 'Delete Page',
message: 'Are you sure you want to delete this page? It cannot be undone.',
okButton: 'Delete Forever',
}).then((result) => {
if (ok) {
alert('You have successfully delete this page.')
} else {
alert('You chose not to delete this page. Doing nothing now.')
}
})
To see why we've suggested throwing an error if the user cancels the confirmation dialogue, see how fluent the following code is:
await this.$refs.confirmDialogue.show({
title: 'Delete Page',
message: 'Are you sure you want to delete this page? It cannot be undone.',
okButton: 'Delete Forever',
})
alert('Deleting this page.')
Since cancellation requires no action, there is simply no need to handle that state at all. And if you decide to handle a cancel request, simply wrap that code with a try/catch
.
Conclusion
In this article, we've defined a reusable modal popup component in Vue.js, and inherited it to implement a confirmation dialogue. Then, we've added animations to it for aesthetic purposes and ran a couple of examples of how to use the component to prompt users for input.