Manejo de <dialog /> nativos

Las ventanas emergentes, comúnmente conocidas como “dialogs”, son parte del desarrollo habitual de interfaces desde hace años. Una ventana de diálogo es un tipo especial de cuadro emergente en una página web. Estos dialogs siempre fueron difíciles de crear o implican la instalación de librerías Javascript que conllevan mayor complejidad y aumentan el tamaño general de la aplicación en desarrollo.

Aquí es donde entra en juego el elemento HTML nativo de los navegadores llamado <dialog>, que permite generar estos diálogos sin necesidad de JavaScript o con un mínimo de código. Veamos brevemente cómo se usan y crearemos un script inspirado en Angular Material para su uso.

La etiqueta <dialog>

Un elemento <dialog> básico se vería de la siguiente forma, en dónde el contenido puede ser diverso:

<dialog open>
  <p>¡Este es un diálogo HTML!</p>
</dialog>

Por defecto, estos elementos se mantienen ocultos a menos que se les agregue el atributo open al dialog, como se ve en el HTML de arriba.

Sin embargo, open no es lo recomendado, ya que limita la funcionalidad de estos diálogos. En su lugar, se pueden emplear los métodos show() o showModal() con Javascript. Ambos métodos muestran el elemento, pero difieren en que uno permite interactuar con el fondo (show) y el otro no (showModal).

// Obtenemos el elemento "<dialog>" del documento
const dialog = document.querySelector("dialog");

// Lo abrimos
dialog.show();
dialog.showModal();

Es en esta última funcionalidad es en la que profundizaré. ¿Cómo podemos hacer si tenemos más de un dialog?, ¿Cómo diferenciamos un dialog con otro? Iré paso a paso respondiendo estas preguntas y resolviendo posibles problemas que nos vayamos encontrando en el proceso hasta dar con un pequeño <script> que nos permita abrir y cerrar distintos modales de forma sencilla.

Abriendo un dialog con muchos botones

Podemos empezar haciendo que un botón (u otro elemento) se encargue de abrir el dialog y ejecute el método que vimos anteriormente. De forma similar a la que buscamos un dialog podríamos buscar un <button>, pero no queremos que cualquier botón pueda abrir un diálogo. En su lugar, buscamos elementos que cumplan una condición específica.

La condición será la presencia del “nuevo atributo” que crearemos, este agregará la funcionalidad que deseamos al elemento que lo posea, este atributo será open-dialog.

// Buscamos todos los elementos que tendan el atributo "open-dialog"
const buttons = document.querySelectorAll("[open-dialog]");
const dialog = document.querySelector("dialog"); // Dialog

// A cada uno de ellos le asignamos la función de abrir el dialog al hacer "click"
buttons.forEach((button) => {
  button.addEventListener("click", () => {
    // Abrir dialog
    dialog.showModal();
  });
});

Ahora cualquier elemento que tengan el atributo open-dialog abrirá nuestra ventana… Pero, ¿Qué pasaría en un caso como este?

<button open-dialog>Abrir ventana</button>

<dialog>
  <p>¡Esta es la ventana UNO!</p>
</dialog>

<dialog>
  <p>¡Esta es la ventana DOS!</p>
</dialog>

No podemos abrir más de una ventana

De hecho, querySelector("dialog") nos devuelve el primer objeto que se encuentre en el documento, por lo que solo se abrirá la ventana “UNO”. Necesitamos una forma de diferenciar cada dialog y abrirlos por separado. ¿Y si les agregamos un id?

<dialog id="ventanaUNO">
  <p>¡Esta es la ventana UNO!</p>
</dialog>

Dándole a cada diálogo un id único, podemos diferenciarlos fácilmente. Pero esto no es suficiente. Necesitamos una forma de que el botón sepa qué diálogo debe abrir. Una solución posible es asignar un atributo adicional que llamaremos dialog-id con el valor del “id” de nuestro dialog objetivo. Es decir, estaremos buscando un botón con el atributo open-dialog y el atributo dialog-id="valor" para determinar qué dialog debe abrirse.

<!-- "dialog-id" coincide con el "id" del dialog anterior -->
<button open-dialog dialog-id="ventanaUNO">Abrir ventana</button>

Ahora, veamos cómo se modifica el código:

const buttons = document.querySelectorAll("[open-dialog]");
buttons.forEach((button) => {
  // Obtenemos el valor de "dialog-id"
  const dialogId = button.getAttribute("dialog-id");

  // Buscamos el dialog que tenga la misma "id" que "dialog-id"
  const dialog = document.querySelector(`#${dialogId}`);

  button.addEventListener("click", () => {
    dialog.showModal();
  });
});

Notarás que he movido la búsqueda del dialog dentro del botón, ya que necesitamos el atributo dialog-id del botón para buscar el diálogo adecuado.

Como resultado, el botón con open-dialog y dialog-id="ventanaUNO" nos abrirá el diálogo que tiene id="ventanaUNO". Para abrir otro, simplemente podemos cambiar el dialog-id del botón o crea varios botones que abran el mismo diálogo.

Nuestro <script> está casi listo, pero aún debemos ver tres problemas importantes:

  • ¿Qué sucede si el botón no tiene dialog-id?
  • ¿Qué pasa si el dialog no existe?
  • ¿Cómo podemos cerrar el dialog?

Manejo de errores

Si el botón no tiene un atributo dialog-id, o este es inválido, el método getAttribute(“dialog-id”) devolverá un valor nulo. Por lo tanto, podemos validar su existencia y en caso de que no exista terminar con la ejecución. Opcionalmente, podemos agregar un mensaje de error.

const dialogId = button.getAttribute("dialog-id");

// Si "dialogId" no existe...
if (!dialogId) {
  // Mensaje de error
  console.error("open-dialog Buttons must have a valid dialog-id attribute");
  // Termina el proceso
  return;
}

Una vez que tenemos dialogId, podemos buscar el diálogo sin problemas. Pero, si este no existe, tendremos un error. Para evitarlo, de forma similar a la que validamos la existencia de dialog-id, podemos validar la búsqueda del dialog.

const dialog = document.querySelector(`#${dialogId}`) as HTMLDialogElement;

// Si el dialog no existe o no es un elemento dialog.
if (!dialog || !(dialog instanceof HTMLDialogElement)) {
  console.error(`There is no <dialog> with id: "${dialogId}"`);
  return; // Termina el proceso
}

Dado que querySelector() puede devolver cualquier tipo de elemento, es importante verificar que el elemento encontrado sea realmente un elemento de tipo <dialog> con dialog instanceof HTMLDialogElement.

Cerramos el caso

Por último, podemos cerrar la ventana presionando la tecla ESC. Pero podemos hacer algo más amigable y crear un botón que se encargue de esta acción.

Este botón generalmente se ubicará dentro del dialog, ya que suele ser difícil acceder a elementos fuera de este. Y al igual que antes, no queremos que cualquier botón cierre el modal, así que buscaremos un botón con el atributo close-dialog (esto es similar a la implementación de Angular Material en sus diálogos).

// El "querySelectorAll()" dentro del dialog nos dará todos los elementos que tengan el atributo "dialog-close"
const closeButtons = dialog.querySelectorAll("[dialog-close]");

// Haremos que cada elemento cierre el dialog al hacer "click"
closeButtons.forEach((close) =>
  close?.addEventListener("click", () => dialog.close()),
);

En el caso de cerrar el diálogo, no es necesario especificar un dialog-id, ya que estamos buscando el botón dentro del diálogo, donde el diálogo ya existe y no es necesario buscarlo nuevamente.

Ejemplo

Veamos cómo se utilizan nuestras ventanas de diálogo:

<!-- Este botón abrirá solo el dialog que tenga la id "ventanaUNO" -->
<button open-dialog dialog-id="ventanaDOS">Abrir ventana dos</button>

<!-- dialog con id "ventanaUNO" -->
<dialog id="ventanaDOS">
  <p>¡Esta es la ventana DOS!</p>

  <!-- Botón que cierra el dialog en el que se contiene -->
  <button close-dialog>Cerrar ventana</button>
</dialog>

Conclusión

Con este pequeño script, hemos logrado “mejorar” la funcionalidad de algunos elementos HTML existentes al crear los atributos open-dialog, dialog-id y close-dialog, permitiéndonos controlar qué elementos abren ventanas de dialogo de manera efectiva. Además, pudimos encargarnos de algunos errores que pueden surgir durante la implementación del script.

Esto nos permitió simplificar la creación y gestión de ventanas emergentes (dialogs) eliminando la necesidad de librerías Javascript que pueden resultar pesadas en algunas ocasiones.

El script se puede modificar según las necesidades, por ejemplo, haciendo que al hacer click fuera del marco del dialog este se cierre.