It's time to get SAD: Self-destructing Awaitable Dialogs
Tags: javascript, webdev, webcomponents, dialog
Published: 2020-08-20 17:56:36

```javascript
const answer = await MyDialog.ask();
console.log(`The user clicked: ${answer}`);
```

Lately I've been using a self-contained Dialog component I made that:

⏱ Awaits user input.
🚿 Cleans itself up.
🌎 Can be requested from anywhere.

It provides an elegant solution to a common problem. You're deep in your app's UI hierarchy, and you need a user input. The user needs to confirm their purchase. Or they need to name their document before saving. And you need a dialog that somehow both (a) covers the whole app and (b) makes the user input available to the deep UI logic.

My solution's API is an async one-liner that you can await:

```javascript
const answer = await MyDialog.ask();
```

It creates the dialog, returns the user's input, and removes the dialog. It's a joy to use.

If you want to jump straight to the full implementation, check out the [simple solution](https://glitch.com/edit/#!/sad---0?path=script.js) or the [extended solution](https://glitch.com/edit/#!/sad---extended?path=script.js).

## The component

I'm going to make the dialog as a web component. This provides a couple of advantages:

- The dialog's styles and markup are encapsulated. I won't affect other elements in the DOM, which is easy to do accidentally when reaching into a different part of my UI.
- Web components are standardized, so I won't need any new dependencies, and the dialog will be compatible with other frameworks.

```javascript
// Make a template with the markup for the dialog.
// The styles will only apply to these elements.
const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host, #overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; }
    #overlay { background: rgba(0,0,0,0.5); }
    #box { background: powderblue; width: 80%; max-width: 200px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 10px; }
    #buttons { text-align: right; }
  </style>
  <div id="overlay"></div>
  <div id="box">
    <div>Do a thing?</div>
    <div style="text-align: right">
      <button id="yes">Yes</button>
      <button id="no">No</button>
    </div>
  </div>
`;

// Make and register the MyDialog class using the template.
class MyDialog extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('my-dialog', MyDialog);
```

Now we can attach a `<my-dialog>` to the document body without worrying about accidentally applying styles to other elements.

## The user input

We'll add the 'self-destructing', and 'awaitable'-ness in a static method on this class.

Here's the signature of the method we'll be working in:

```javascript
class MyDialog extends HTMLElement {
  // We'll be filling in this method.
  static async ask() { }
}
```

The rest of the code will go in the `ask` method.

First, we should ensure `my-dialog` has been registered as a custom element. Once that's been established, let's proceed with creating a `my-dialog` element and adding it to the DOM. I put it in the  body, but we could give the developer more control by parameterizing the parent element.

```javascript
await customElements.whenDefined('my-dialog');
const dialog = document.createElement('my-dialog');
document.body.prepend(dialog);
```

Now, let's get references to the input targets we want to listen for. These grab the "yes" and "no" buttons in the Dialog.

```javascript
const yes = dialog.shadowRoot.querySelector('#yes');
const no = dialog.shadowRoot.querySelector('#no');
```

Now, construct a `Promise` that resolves once a 'Yes' or 'No' button is clicked. Take this opportunity to remove the dialog instance as well.

```javascript
return new Promise(resolve => {
  yes.addEventListener('click', () => {
    document.body.removeChild(dialog);
    resolve('yes');
  });
  no.addEventListener('click', () => {
    document.body.removeChild(dialog);
    resolve('no');
  });
});
```

And there you have it. You can now say

```javascript
await MyDialog.ask();
```

anywhere in your UI logic.

## Extension

I've implemented the self-destructing awaitable dialog, along with some extensions, in this [glitch.me demo](https://glitch.com/edit/#!/sad---extended). The following extension ideas are included in the implementation. Check it out!

### More resolution states

Usually when the user clicks outside of the dialog box, the dialog closes. In this case, we could reject the promise with the reasoning "user cancelled". `keydown` listeners could also be added that resolve the promise in predictable manners.

### disconnectedCallback

What would happen if the `my-dialog` DOM node is removed by some other part of our program? The promise never resolves. To handle this, add a `disconnectedCallback` that fires a `CustomEvent`. Add a listener for that event in the promise, and reject if received.