Disclaimer: The Confirmed module was written by the author of this article.
Modal Dialogs are a very complex topic but most all apps usually find a need for them. In this article I hope to describe some patterns to help manage them and make their use easier in an Ember application.g
First, Modals can be implemented in many ways. In fact there are several Modal addons already out there. However each has their own nuances and quirks. It helps to wrap what ever implementation you use in an interface that remains consistent through out your app. In this way the addon used or the way a modal is displayed can change without a major app wide rewrite.
A good first step to to articulate what a modal dialog is and how it functions. Fist and for most it is a full interrupt based workflow. It is designed to break the current mode and present a new mode. There are several ways to accomplish this (routes, if/else blocks, etc.) In the case of this article however we are talking specifically the kind of break that happens when a dialog box overlays the current mode and blocks the user from interacting with the previous (under the dialog) mode. In all the cases for this design there is two basic interfaces one for the act of opening and one for the act of closing. However the nuances of those two interfaces can get a little more complex.
In most all cases where a modal dialog is desired it is triggered from some form of an event. Unlike being shown/hidden based on data. In a naive approach this can lead to managing a flip-bit (boolean flag) to force the modal to be shown or hidden. It might seem intuitive at first but then you quickly realize that it hinders flexibility. Managing a set of flip-bits to mimic a stack is complex and something of a time sink when likely there are more pressing business concerns worth spending time on.
The alternative is to have the act of opening a modal driven from an action instead of data. In this case the API could be a service, helper, or component action. Doing it this way would look very similar to how ember-concurrency starts performing a task. It is told to do so. Unfortunately, this has the trade-off that it is not 100% in the template.
The middle ground is a JavaScript API which can encapsulate the scoped data that would drive the template to show/hide a modal. Perhaps it is best to dive into concrete examples.
GIVEN we have a button which will open a modal WHEN a user clicks that button THEN the modal is shown AND the status of the modal (opened/closed) is able to block/unblock code logic
This last point is important as will hopefully be evident as we continue.
If you haven't already imagined a similar API, Promises offer the kind of programming ergonomics that seems well suited to the idea of a modal dialog in the UI as the act of the user interacting with it is an asynchronous task. A Promise could represent that it is pending and when done that the dialog has been fulfilled. Here is what that might look like:
export default class MyComponent extends Component {
@tracked showModal = false;
@action async openModal() {
try {
return new Promise((resolve, reject) => {
this.showModal = true;
this.onClose = resolve;
this.onError = reject;
});
} finally {
this.showModal = false;
}
}
@action closeModal() {
this.onClose();
}
}
In this example we have encapsulated the asynchronous nature of user interaction into a Promise and setup an escape hatch for how to determine when the user has closed the modal.
When it comes to modal dialogs and more specifically confirmations there are usually four states the result can be: Confirmed, Cancelled, Rejected, and Error. I will talk about each.
@action closeModal(reason, value) {
this.onClose({ reason, value });
}
Presumably the interruption is meant to gather some kind of information from the user. Be it just an acknowledgement that the user understands something (EULAs, Warnings, "Are you sure?" checks, etc.) to data entry forms (create new things) to complex wizard steps (series of steps with back/next buttons).
In all these cases our code is likely interested in the result. Maybe we need the form data after they click the same button. Or we need to know the user agrees to the EULA. This is what I would consider the confirmed state.
this.onClose({ reason: 'confirmed', value: formData });