Let's think for a moment about modal dialogs. What is their usage pattern? What are they for, I mean conceptually...
Dialog unveiled
When we create a dialog it is usually to gather some feedback from the user. It might be either a simple Yes / No or some form that the user needs to fill in and return that input after some form of interaction with that dialog.
Unfortunately there is no direct support for this kinds of interactions in Vue.js (nor in any other reactive framework, to be honest). This means that we need to resort to stuff like this:
data() {
return {
isConfirmationDialogVisible: false
}
},
methods: {
showConfirmationDialog() {
this.isConfirmationDialogVisible = true
},
hideConfirmationDialog() {
this.isConfirmationDialogVisible = false
},
handleConfirm() {
this.hideConfirmationDialog()
// the dialog ended with "OK" - perform some action
},
handleCancel() {
this.hideConfirmationDialog()
// the dialog ended with "Cancel" - do nothing
}
}
The reason why we're doing all the state mutation nonsense in every place where we want to use a dialog is that the general approach in framework such as Vue.js is to base everything on state and we're completely ignoring the imperative nature of some of the processes. What is even more disturbing is that quite frankly the isConfirmationDialogVisible doesn't really belong with the place of use of the dialog. It should be an internal implementation detail of the dialog itself. But since we don't have implicit support for imperative programming with Vue.js it is sort of necessary to resort to stuff like that. But is it?
API is not just props and events
You might be tempted to think about the API of a component in terms of props that component accepts and events it emits. And even though they form a very important way of communication between parent and children it is only 2/3rd of the story. Each method you define in the methods block is essentially part of the API of a component.
Suppose we have a dialog component that has the following two methods:
methods: {
open() { ... },
close() { ... }
}
Now if we use that dialog component somewhere it is quite easy to call those methods:
<template>
<MyDialog ref="dialog" />
</template>
<script>
export default {
mounted() {
this.$refs.dialog.open()
},
beforeDestroy() {
this.$refs.dialog.close()
}
}
</script>
This means that we can imperatively steer when the dialog is open and when it closes. This way the state of visibility of that dialog is stored with that dialog and not in every place that uses that dialog which improves the usability quite a bit.
Promises, promises
Knowing that we can actually call methods on components let's move on to the concepts of modal dialogs.
Modal dialogs are dialogs that limit the possibility of user interaction to their content and usually finish with some result of that interaction. A good example is a popup that asks a question to which a user can say Yes or No or prompts the user to enter some data in which case there are usually two outcomes too: either the user entered the required information and approved his/her choice by pressing OK or resigns from proceeding, usually with the user of a Cancel button. It all bears a lot of resemblance to the alert() and confirm(), doesn't it?
The way it is usually handled in other frameworks (the Windows API, GTK just to name a few) is that the call to the framework method is blocking and once the user interaction is done it returns some result. In the browser a blocking code like that would result in everything going sideways. However, and this is where JavaScript really shines, there is a built-in concept of values that will be delivered later in time. This is the concept of Promises.
What if our dialog would expose a function like that:
methods: {
async show() {
return new Promise(resolve => {
this.resolve = resolve
this.show = true
})
},
onOkButtonClick() {
this.show = false
this.resolve && this.resolve('ok')
},
onCancelButtonClick() {
this.show = false
this.resolve && this.resolve('cancel')
},
},
data() {
return {
show: false,
resolve: null
}
}
Now that we have this we can use it in the code of a component that needs this kinds of interaction in a very nice way:
methods: {
async save() {
const confirmation = await this.$refs.dialog.show()
if (confirmation === 'ok') {
// do something, the user is OK with it :)
}
}
}
The most important part of that approach is that you're not multiplying state that doesn't need to be multiplied and as a bonus your code expresses the intent: show a modal and react to the result of its interaction with the user
Have fun!
Top comments (6)
I honestly this is a life-saver tip.
Where I work I implemented this kind of logic (even more complex and advanced, to avoid having also the modal in the template) built in in our component libraries in modals AND we want to add it in tooltips too.
Nice article
Hi,
Thanks for the article !
Just something i don't understand : the resolve is passed in the component with this.resolve = resolve. OK.
Why an only this.resolve("ok") isn't enough ?
Others words : what do
this.resolve $$ this.resolve("ok") ?
Great idea!
Although your save method should be async...
Fixed! Thanks!
Dude, this is nice.
Thank you 👍 Great article