Using the new Native Dialog API as a reusable component in Vue 3
Tags: webdev, vue, html, typescript
Published: 2023-06-15 01:51:58



## 📝 The Native HTML Dialog

We've all heard the buzz around browser native dialogs which are now widely supported across all modern browsers. Now you no longer need to depend on third party packages to achieve a user friendly dialog/popover experience.

Now if you are just here for the code, here it is below. However, if you are here to understand how we got here, stick to the end!

```vue
<script setup lang="ts">
import { ref } from 'vue';

const dialog = ref<HTMLDialogElement>();

const props = defineProps({
  classes: {
    type: String,
    default: "",
  },
});

const visible = ref(false);

const showModal = () => {
  dialog.value?.showModal();
  visible.value = true;
};

defineExpose({
  show: showModal,
  close: (returnVal?: string): void => dialog.value?.close(returnVal),
  visible,
});
</script>

<template>
  <dialog
    ref="dialog"
    @close="visible = false"
  >
    <form
      v-if="visible"
      method="dialog"
      :class="{
        [props.classes]: props.classes,
      }"
    >
      <slot />
    </form>
  </dialog>
</template>
```

## 👷 The Breakdown
The new Dialog API is pretty straightforward. As seen from the [MDN Page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog), all you need to get started is a simple `<dialog>` element. Any reference to this element will have programatic access to the function `.showModal()` to toggle the dialog, so you could do this with a button!.

### Vanilla HTML
Lets take a look at the provided example

```html
<dialog open>
  <p>Greetings, one and all!</p>
  <form method="dialog">
    <button>OK</button>
  </form>
</dialog>
```

Do note the additional comment on MDN,
> Because this dialog was opened via the open attribute, it is non-modal. In this example, when the dialog is dismissed, no method is provided to re-open it. Opening dialogs via HTMLDialogElement.show() is preferred over the toggling of the boolean open attribute.

So although this is a good demonstration of the result, programatic access is suggested.

Looking closer, other than the open attribute, we have

1. A form component, with `method="dialog"`. This is a new form method, which on form submission, automatically closes the dialog with a submit event.
2. Accessibility, pressing tab will toggle only through the form elements in the dialog.
3. The dialog content will be shown in a new layer, which can be visualized from the browsers dev tools.


### 🤖 Difference between show and showModal
There are two ways to open the dialog.

1. `.show()` will toggle open the dialog, but
    - Esc will not close the element.
    - Backdrop will not be darkened (default styling).
2. `.showModal()` will toggle open the dialog in the modal mode,
    - Esc will close the modal
    - Backdrop will be darkened in default styling. (The styling can be changed).

### ⛵️ Bringing in Vue
Lets be real, you are probably not working on a project with vanilla HTML, it is nice but does not scale. You might be using a Frontend library, probably something among React, Vue or Svelte. I will be covering Vue here, but the design pattern should be similar in Svelte. Pretty sure, React folk don't have to worry about not using Third Party packages :P.

A simple solution in Vue is to just slap a `<slot />`, have a two way binding to the `open` attribute via props and call it a day. Lets look at this example

```vue
<script setup lang="ts">
const props = defineProps<{
   open: boolean
}>();
</script>

<template>
<dialog :open="props.open">
  <form method="dialog">
    <slot />
  </form>
</dialog>
</template>
```

This is a good enough solution but lets see if we can do better! 

### 💪 Trying to do better
We will try to
1. Adhere to using programmatic access.
2. See if we can mount the child component only when the modal is open, so that any mount hooks run only when needed.

To achieve the first we shall use a `ref`. This way we get a ref to the dialog element, to be able to call the `showModal` function. We avoid ids, because we want this re-usable! Plus avoid `document.querySelector` makes it SSR safe?. Don't quote me on that.

```vue
<script setup lang="ts">
import { ref } from 'vue';

const dialog = ref<HTMLDialogElement>();
....
</script>
<template>
<dialog
    ref="dialog"
>
   ...
</dialog>
</template>
```

We then declare a function to call showModal(), then expose that function to parent components via `defineExpose()`.

```ts
const showModal = () => {
  dialog.value?.showModal();
};

defineExpose({
  show: showModal
});
```

The parent component can then bind a ref to the modal and toggle dialog as needed.

```ts
import Modal from 'path/to/Modal.vue';

const modal = ref<InstanceType<typeof Modal>>();

const showModal = () => modal.value?.show();
```

With that complete lets also get conditional loading so that mount hooks work proper (otherwise this is equivalent to a `v-show`)

We create a state called visible and toggle that when state of dialog changes. We shall set visible to true, when we show the modal, and use the close event to toggle it to false.

```vue
<script setup lang="ts">
...
const visible = ref(false);

const showModal = () => {
  dialog.value?.showModal();
  visible.value = true;
};
...
</script>
<template>
  <dialog
    ref="dialog"
    @close="visible = false"
  >
    <form
      v-if="visible"
      method="dialog"
    >
      <slot />
    </form>
  </dialog>
</template>
```

and there's that! Note that if you still want a cleaner approach, you can still use the `open` prop for conditional rendering as well as the `open` attribute but do note that some of the accessibility will be lost.

```vue
<script setup lang="ts">
const props = defineProps<{
   open: boolean
}>();
</script>

<template>
<dialog :open="props.open">
  <form v-if="props.open" method="dialog">
    <slot />
  </form>
</dialog>
</template>
```

the final result is the code at the very start of the guide! 

### 🎨 With DaisyUI and TailwindCSS!
[DaisyUI v3](https://daisyui.com/docs/changelog/#new-features-on-components) supports native modals! Here is a quick custom component with slots and styles to make life easier for you!

```vue
<script setup lang="ts">
import { ref } from 'vue';

const dialog = ref<HTMLDialogElement>();

const props = defineProps({
  confirmText: {
    type: String,
    default: 'Confirm',
  },
  cancelText: {
    type: String,
    default: 'Cancel',
  },
  hideConfirm: {
    type: Boolean,
    default: false,
  },
  showCancel: {
    type: Boolean,
    default: false,
  },
  classes: {
    type: String,
    default: '',
  },
});

const emit = defineEmits(['confirm', 'cancel']);

const cancel = () => {
  dialog.value?.close();
  emit('cancel');
};

const confirm = () => {
  dialog.value?.close();
  emit('confirm');
};

const visible = ref(false);

const showModal = () => {
  dialog.value?.showModal();
  visible.value = true;
};

defineExpose({
  show: showModal,
  close: (returnVal?: string): void => dialog.value?.close(returnVal),
  visible,
});
</script>

<template>
  <dialog
    ref="dialog"
    class="modal modal-bottom sm:modal-middle"
    @close="visible = false"
  >
    <form
      v-if="visible"
      method="dialog"
      :class="{
        'modal-box rounded-none p-4': true,
        [props.classes]: props.classes,
      }"
    >
      <slot />

      <div class="modal-action" v-if="!props.hideConfirm || props.showCancel">
        <slot name="footer" />
        <slot name="actionButtons">
          <button
            v-if="props.showCancel"
            value="false"
            class="btn"
            @click.prevent="cancel"
          >
            {{ props.cancelText }}
          </button>
          <button
            v-if="!props.hideConfirm"
            value="true"
            class="btn btn-primary"
            @click.prevent="confirm"
          >
            {{ props.confirmText }}
          </button>
        </slot>
      </div>
    </form>
    <form method="dialog" class="modal-backdrop">
      <button>close</button>
    </form>
  </dialog>
</template>
```

Here is a stackblitz for the same
{% stackblitz https://stackblitz.com/edit/vitejs-vite-uwpxdn?file=src%2Fcomponents%2FModal.vue
 %}

## Closing
Thanks so much for reading! If you have any improvements on the above do share in the comments. As for me, I really love the new native dialog, and the future things coming to vanilla HTML, like the [Popover element](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover) bring simpler tooltips and [page transitions](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) which may enable frameworks like astro to simplify page transitions via CSS where JS libraries like SWUP which takeover client side page navigation are the only way to transition.