DEV Community

Ron Newcomb
Ron Newcomb

Posted on

Modal Dialogs as a Promise Versus Inline

There are two major ways a UI framework implements modal dialogs. One is writing the modal into the component template that will pop it, and adding show/hide logic into that component's code.

<InvoicesPage>
  <ActionsRow>
    <CopyButton />
    <DeleteButton />
  </ActionsRow>
  <InvoicesList />
  <ConfirmDeleteModal show={prop} />
</InvoicesPage>
Enter fullscreen mode Exit fullscreen mode

Sometimes the entire contents of the modal are also inline, such as with a tag that promotes a piece of template into being a modal by adding the necessary CSS.

<InvoicesPage>
  <ActionsRow>
    <CopyButton />
    <DeleteButton />
  </ActionsRow>
  <InvoicesList />
  <AsModal show={prop}>
    <h3>Are you sure you want to delete?</h3>
    <button>OK</button> <button>Cancel</button>
  </AsModal>
</InvoicesPage>
Enter fullscreen mode Exit fullscreen mode

The other way uses an imperative function that returns a Promise, where the promised value is in most cases the button that was pressed to dismiss it.

const onDeleteClick = async itemId => {
  const clickedOKbutton = await askIf('Are you sure?');
  if (!clickedOKbutton) return;
  /* else they are sure */
Enter fullscreen mode Exit fullscreen mode

Sometimes a modal's job is to create an object, say, from a form. If so then that is the promised value.

const onCreateClick = async ownerId => {
  const newOrder = await createOrderModal(ownerId);
  if (!newOrder) return; /* cancelled */
  /* else we have a valid object */
Enter fullscreen mode Exit fullscreen mode

Typically, React uses the former method and Angular uses the latter method. But there are clear use-cases for each method, and sticking to either method exclusively brings unnecessary complexity when using the wrong method for the other's best case.

Let's look at some of these use-cases which favor one method over the other.

Use-case #1: Zooming In

Sometimes you just need extra space. Most apps have a main menu, at least one sidebar, and possibly a footer eating up a portion of the screen. The main content area frequently has a heading or breadcrumbs or action buttons taking up another row or two of space.

Let's say our user is filling in a form where one of the form fields requires a lot of space, say, which email template to use for a scheduled customer communication. Email templates start with words like, "Dear CUSTOMERNAME" and go on for three to five paragraphs of just the right tone and messaging. We want our user to be able to see and read the templates before committing to one, so a simple <select> of template titles won't do. Rather, we want to pop a large modal that shows the complete email template in a box of significant width and height, and allow the user to flip through them reading each.

This is a poor use-case for promise-based modals. Despite being presented as a modal, we're still in an actual <form> with live validation, and that validation may change which email templates we're allowed to choose or vice-versa. Keeping that validation intact when the modal contents are elsewhere would be obnoxious. By defining the modal inline, the selection remains part of the form at all times.We're just using the language of modals to "zoom-in" on the details of one field while our user manipulates it.

An even simpler example: let's say we have a dashboard with several charts and graphs. Each chart is rather small to see much detail on it, so we decide that clicking a chart brings it up in a modal that's a good deal larger. The chart library will re-render itself when it detects the resize, no longer omitting labels since it now has the space, and making smaller pie slices visible.

The implementation of such is trivial for an inline modal. Just wrap the chart's <div> with something like <ClickForZoom isPoppedOut={prop}>. Modals don't get much simpler than toggling the size and position of a div on each click. The contents of the div don't change at all in this case.

Use-case #2: Simple Questions

The other kind of very simple modal has more to do with its job than its implementation. Asking simple questions for simple answers is one of the commonest use-cases for modal dialogs, so much so that a page with many action buttons might be able to pop three or four different questions.

Writing such modals inline means writing their show/hide logic in that same page, and more logic to set the question and available answers each time. For a busy page the lines of code required for multiple inline modals can begin to mask the page's own logic through sheer amount.

Here the promise-based modal is a natural fit. Confirmation as a function which accepts the question and returns the promised boolean answer gets the job done with minimal intrusiveness.

const onDeleteClick = async () => {
  const ok = await ask("Are you sure you wish to delete this?");
  if (!ok) return;
  /* else delete it... */
Enter fullscreen mode Exit fullscreen mode

Multiple-choice instead promises one of the passed-in strings (or its numeric index, per your taste).

const onCustomizeClick = async () => {
  const theme = await ask("Choose a theme", ["Light", "Dark", "High Contrast"]);
  if (!theme) return;
  /* else apply the choice */
Enter fullscreen mode Exit fullscreen mode

Here the promise setup puts the logic and everything in a re-usable manner elsewhere so it cannot clutter the page with show/hide logic, nor duplicate <ConfirmModal/> tags all over the codebase's various consumer components.

Use-case #3: Modeless Dialogs

Modal dialogs are called such because they put the software into a special "mode" which persists, with explicit entry and exit. But there are modeless dialogs, which float around like a persistant toolbar.

One case is a CRM that assists our user, a customer support representative, with making phone calls to her customers. While she's on the phone a modeless dialog appears with the customer's name, info and quick-links to their recent orders and invoices. She can roam all over our app while this dialog shows, and can click links in the dialog to make the rest of the app navigate there without affecting the dialog. Only by clicking its Hang Up button will the dialog then dismiss itself.

The primary difference between the modeless and modal dialogs is that the modeless isn't trying to gather an answer for any particular reason. Since there's no answer to wait upon, an awaitable function returning the Promise of an answer wouldn't have much purpose here.

A Hybrid Approach

Consider an awaitable function which accepts elements to be inserted in the modal's body.

const theme = await ask(<BlinkingText>Choose any of our three themes</BlinkingText>, ["Light", "Dark", "High Contrast"]);
Enter fullscreen mode Exit fullscreen mode

This hybrid approach is a mixed bag of limitation and feature. It encapsulates the show/hide state away from the calling page but not the details of the modal body. If the passed elements are more than half a line of code it will look out of place in an event handler code block. Also, someone will eventually stretch that too far and ask how to put state into it.

We can try to salvage the idea with a version which accepts a single element, the component-as-promise.

const theme = await ask(<BlinkingThemeSelectModal />);
Enter fullscreen mode Exit fullscreen mode

The issue with this is it would be fewer import statements and fewer keystrokes if BlinkingThemeSelectModal wrapped the above in a dedicated function and exported that instead.

const theme = await askBlinkingThemeSelectModal();
Enter fullscreen mode Exit fullscreen mode

But that brings us back to ordinary modals-as-a-service again. Hence I wouldn't recommend the hybrid approach of passing elements to the promise function.

Stacking Contexts and Position Unfixed

Sometimes you can't use inline modals when you want to.

First of all, z-index isn't global, it's relative to the current stacking context. In each stacking context, the whole z-index numberline starts over. This is the case where a z-index of 1 is still shown on top of a z-index of ninety bagillion. Although you can try to put the entire app in a single stacking context by adding z-index:1 to document.body, you'll then find yourself trawling through your third party libraries for tooltips, drag-drops, leaflet maps, and other popovers and discovering that they some use z-index values of tens while others use thousands, so they won't place nicely together anymore. The usual solution instead is to use document source order, which means placing the modal's HTML near the end of the document, which is how the promise-modal method works anyway.

Second is the way the modal itself is constructed. Nowadays we use position:fixed to pull a <div> out of document flow to center it onscreen, usually with left:50%; top:50%; transform: translate(-50%, -50%);. If our app also has, say, a side drawer that slides out from the right side of the screen, we might position and move it in a similar way. And if we did, then one day we discover that if we try to do an inline modal from within the drawer, position:fixed doesn't work. The modal and its backing overlay only cover the drawer, and is centered within the drawer. This is because transform creates a new context which acts as a viewport itself. The usual solution is, again, document source order.

Finally, modals aren't always the top element anyway. Small, non-blocking notifications that self-dismiss after a few seconds like toastr should appear on top of modals. Auto-logout warnings which give the user 15 seconds to press the supplied "I'm still here" button should appear on top of them as well. A good promise-modal system allows one to put a placeholder near the end of the root <App> so the placeholders for these other things can be put immediately after.

<App>
  /* most of the app here... */
  <ModalDialogsContainer />
  <AutoLogoutWarning />
  <NotificationsContainer />
</App>
Enter fullscreen mode Exit fullscreen mode

Abort, Retry, Ignore?

I hope you found this long deliberation on the simple modal dialog enlightening. The imperative promise-based method is best for some use cases and the declarative inline method is best for others, so I would expect both methods to appear in a codebase of any complexity. For the middle ground where either work, go with the framework's or the team's favorite. Just be wary of anyone demanding the two methods can't or shouldn't coexist.

Latest comments (0)