Buy Access to Course
19.

Diálogo HTML para Módulos

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Bienvenido al día 19. Hoy tenemos la suerte de jugar con un elemento HTML poco conocido que es absolutamente genial cuando se trata de construir modales. El elemento <dialog>. Si tienes prisa por la magnificencia de los modales, puedes saltar más adelante para engancharte al marcado final y al controlador Stimulus. Pero te prometo que el viaje de hoy va a ser divertido.

Abre templates/voyage/index.html.twig. En h1, voy a pegar algo de contenido nuevo:

48 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
class="flex justify-between"
>
<h1 class="text-xl font-semibold text-white mb-4">Voyages</h1>
<button
class="flex items-center space-x-1 bg-blue-500 hover:bg-blue-700 text-white text-sm font-bold px-4 rounded"
>
<span>New Voyage</span>
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 inline" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M9 12h6" /><path d="M12 9v6" /></svg>
</button>
</div>
// ... lines 18 - 45
</div>
{% endblock %}

Esto añade un botón "Nuevo viaje".

En la parte inferior, eliminaré el botón antiguo. Este nuevo código no tiene nada de especial: es sólo... un botón. Y cuando vayamos a la página correcta... ¡ahí está! Pero todavía no hace nada.

Hola <dialog>

De vuelta en la plantilla, justo después del botón, añade un elemento <dialog>. Dentro proclamaré "Soy un diálogo". Añade también un atributo open:

52 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
class="flex justify-between"
>
// ... lines 10 - 17
<dialog open>
I am a dialog!
</dialog>
</div>
// ... lines 22 - 49
</div>
{% endblock %}

Pulsa actualizar y contempla el elemento dialog. Es... interesante. El dialog está absolutamente posicionado en la página, centrado horizontalmente y cerca, pero no arriba verticalmente. Eso es porque el elemento <dialog> está diseñado para modales... o realmente para cualquier diálogo, como una alerta desechable o cualquier subventana. Es un elemento HTML normal, pero con un montón de superpoderes que vamos a experimentar.

Hacer un diálogo bonito

Pero primero, tenemos que hacerlo más bonito. De vuelta a la plantilla, pegaré encima ese diálogo:

77 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
class="flex justify-between"
>
// ... lines 10 - 18
<dialog
open
class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%]"
>
<div class="flex grow p-5">
<div class="grow overflow-auto p-1">
<div class="text-white space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-xl font-bold">Create new Voyage</h2>
<button class="text-lg absolute top-5 right-5">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12"/><path d="M6 6l12 12"/></svg>
</button>
</div>
<p class="text-gray-400">
Join us on an exciting journey through the cosmos! Discover the
mysteries of the universe and explore distant galaxies.
</p>
<div class="flex justify-end">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Let's Go!
</button>
</div>
</div>
</div>
</div>
</dialog>
</div>
// ... lines 47 - 74
</div>
{% endblock %}

Esto es una adaptación de Flowbite con algo de ayuda de la IA. Y un diseñador podría crear esto sin problemas. Porque, no hay nada especial: seguimos teniendo un dialog, sigue siendo open... e incluso las clases Tailwind son bastante aburridas. Establezco una anchura... y redondeo las esquinas. Pero la mayoría de los detalles de posicionamiento ya están incorporados en el elemento. Y la mayor parte del código es contenido modal ficticio para empezar.

El resultado... es impresionante. Aunque... ¡el botón de cerrar aún no hace su trabajo! No te preocupes: ¡ésta es una gran oportunidad para mostrar uno de los superpoderes de diálogo!

Busca el botón de cerrar. A su alrededor, añade un <form method="dialog">:

79 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
class="flex justify-between"
>
// ... lines 10 - 18
<dialog
open
class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%]"
>
<div class="flex grow p-5">
<div class="grow overflow-auto p-1">
<div class="text-white space-y-4">
<div class="flex justify-between items-center">
// ... line 27
<form method="dialog">
<button class="text-lg absolute top-5 right-5">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12"/><path d="M6 6l12 12"/></svg>
</button>
</form>
</div>
// ... lines 34 - 43
</div>
</div>
</div>
</dialog>
</div>
// ... lines 49 - 76
</div>
{% endblock %}

Este es un botón normal: naturalmente enviará el formulario cuando lo pulsemos, pero el botón no tiene nada especial.

Pero ahora, cuando hagamos clic en X... ¡se cerrará!

Abrir con un Controlador de Stimulus modal

Para hacer brillar realmente el elemento <dialog>, necesitamos un poco de JavaScript. Dirígete a assets/controllers/ y crea un nuevo archivo llamado modal_controller.js. Haré trampas, robaré algo de contenido de otro controlador... y lo limpiaré. Este controlador será sencillo. Empieza añadiendo un static targets = ['dialog']para que podamos encontrar rápidamente el elemento <dialog>. A continuación, añade un método open. Aquí, digamos this.dialogTarget.show():

import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['dialog'];
open() {
this.dialogTarget.show();
}
}

Éste es otro superpoder del elemento <dialog> ¡tiene un método show()! Integrada en el elemento <dialog> está esta idea central de mostrar y ocultar.

Para utilizar el nuevo controlador, en index.html.twig, busca el div que contiene el button y el dialog y añade data-controller="modal". Luego, en el botón, di data-action="modal#open":

81 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
data-controller="modal"
class="flex justify-between"
>
// ... lines 11 - 12
<button
data-action="modal#open"
class="flex items-center space-x-1 bg-blue-500 hover:bg-blue-700 text-white text-sm font-bold px-4 rounded"
>
// ... lines 17 - 18
</button>
// ... lines 20 - 49
</div>
// ... lines 51 - 78
</div>
{% endblock %}

Por último, tenemos que establecer el <dialog> como objetivo. Elimina el atributo open para que empiece cerrado y sustitúyelo por data-modal-target="dialog":

81 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
data-controller="modal"
class="flex justify-between"
>
// ... lines 11 - 20
<dialog
class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%]"
data-modal-target="dialog"
>
// ... lines 25 - 49
</div>
// ... lines 51 - 78
</div>
{% endblock %}

¡Me gusta! Aquí empieza cerrado. Y cuando hagamos clic, ¡abre! Cerrar, abrir, ¡cerrar!

Abrir como modal

Un elemento <dialog> tiene dos modos: el modo normal que hemos estado utilizando y un modo modal... que es mucho más útil. Para utilizar el modo modal, en lugar de show(), utiliza showModal():

10 lines | assets/controllers/modal_controller.js
// ... lines 1 - 2
export default class extends Controller {
// ... lines 4 - 5
open() {
this.dialogTarget.showModal();
}
}

Ahora, cuando hacemos clic, se sigue abriendo, pero hay algunas diferencias sutiles. La primera es que podemos cerrarlo pulsando Esc. ¡Genial! La segunda es que tiene un fondo. Observa: cuando haga clic, la pantalla se oscurecerá un poco. ¿Lo has visto? Esto también me impide interactuar con el resto de la página. Y esto nos sale gratis gracias a <dialog>. Eso es enorme.

Estilizar el telón de fondo

Inspecciona y busca el elemento <dialog> - ahí está. El telón de fondo se añade a través de un pseudoelemento llamado backdrop. Así que se encarga de añadirlo por nosotros... pero es un elemento real al que se le puede aplicar estilo. ¡Y quiero darle estilo!

De vuelta a la plantilla, busca el elemento dialog. Gracias a Tailwind, podemos aplicar estilo directamente al pseudoelemento telón de fondo. Añade backdrop:bg-slate-600 ybackdrop:opacity-80:

81 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
data-controller="modal"
class="flex justify-between"
>
// ... lines 11 - 20
<dialog
class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%] backdrop:bg-slate-600 backdrop:opacity-80"
data-modal-target="dialog"
>
// ... lines 25 - 48
</dialog>
</div>
// ... lines 51 - 78
</div>
{% endblock %}

Observa el efecto. Esto empieza a ser muy, muy suave.

Eliminar el desplazamiento de página de fondo

Una cosa que el elemento dialog no maneja automáticamente es que... la página del fondo sigue desplazándose. No afecta a nada... pero no es el comportamiento que esperamos.

Para solucionarlo, en el método open(), di document.body para obtener el elemento body, .classList.add('overflow-hidden'):

11 lines | assets/controllers/modal_controller.js
// ... lines 1 - 2
export default class extends Controller {
// ... lines 4 - 5
open() {
// ... line 7
document.body.classList.add('overflow-hidden');
}
}

Y ahora... ¡eso es lo que queremos!

Limpieza al cerrar

Aunque... si cerramos, ¡todavía no puedo desplazarme! Tenemos que eliminar esa clase.

Para ello, copia el método open(), pégalo y llámalo close(). Para cerrar el diálogo, llama a close()... y luego elimina overflow-hidden:

16 lines | assets/controllers/modal_controller.js
// ... lines 1 - 2
export default class extends Controller {
// ... lines 4 - 10
close() {
this.dialogTarget.close();
document.body.classList.remove('overflow-hidden');
}
}

¡Me gusta! Sólo hay un pequeño problema: ¡no estamos llamando al método close()! Si pulsamos X o Esc, el diálogo se cierra, sí, pero sigo sin poder desplazarme porque nada llama a este método close() en nuestro controlador.

Afortunadamente, el elemento dialog nos cubre las espaldas. Cada vez que un elemento dialog se cierra -por cualquier motivo-, envía un evento llamado close. Podemos escucharlo.

En el elemento <dialog>, añade un conjunto data-action a close->modal#close:

82 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
data-controller="modal"
class="flex justify-between"
>
// ... lines 11 - 20
<dialog
// ... lines 22 - 23
data-action="close->modal#close"
>
// ... lines 26 - 49
</dialog>
</div>
// ... lines 52 - 79
</div>
{% endblock %}

Así, independientemente de cómo se cierre dialog -presionaré Escape-, ahora podemos desplazarnos porque se ha llamado al método close() de nuestro controlador.

Difuminar el fondo

Tip

Gracias a la ayuda de Rob Meijer, puedes hacer esto en CSS puro. En el elemento <dialog> utiliza backdrop:bg-opacity-80 en lugar de backdrop:opacity-80 y luego añade backdrop:backdrop-blur-sm. ¡No necesitas JS!

Vale, estoy emocionado. ¿Qué más podemos hacer? ¿Qué tal difuminar el fondo? Puedes intentar hacerlo difuminando el fondo. Yo lo he intentado... pero no he conseguido que funcione. No pasa nada. Lo que podemos desenfocar es el cuerpo. Añade una clase más: blur-sm y elimina la blur-sm en close():

16 lines | assets/controllers/modal_controller.js
// ... lines 1 - 2
export default class extends Controller {
// ... lines 4 - 5
open() {
// ... line 7
document.body.classList.add('overflow-hidden', 'blur-sm');
}
close() {
// ... line 12
document.body.classList.remove('overflow-hidden', 'blur-sm');
}
}

Veamos cómo queda. ¡Esto sí que mola!

Cerrar al hacer clic fuera

Pero si intento hacer clic fuera del modal, no se cierra. Esa es otra cosa que el elemento dialog no maneja. Afortunadamente, hay una solución rápida.

Arriba, en el elemento raíz de nuestro controlador... En realidad, podemos ponerlo aquí abajo, en el elemento dialog. Añade una nueva acción: click->modal#clickOutside:

82 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
data-controller="modal"
class="flex justify-between"
>
// ... lines 11 - 20
<dialog
// ... lines 22 - 23
data-action="close->modal#close click->modal#clickOutside"
>
// ... lines 26 - 49
</dialog>
</div>
// ... lines 52 - 79
</div>
// ... lines 81 - 82

Apuesto a que parece raro -se llamará cada vez que hagamos clic en cualquier parte del diálogo-, así que vamos a escribir ese método. Digamos clickOutside(), dale un argumento event, luego si event.target === this.dialogTarget, this.dialogTarget.close():

22 lines | assets/controllers/modal_controller.js
// ... lines 1 - 2
export default class extends Controller {
// ... lines 4 - 15
clickOutside(event) {
if (event.target === this.dialogTarget) {
this.dialogTarget.close();
}
}
}

Tip

Para que el "clic fuera" funcione perfectamente, en lugar de añadir relleno directamente a dialog, añade un elemento dentro y dale el relleno. Ya lo hemos hecho, pero es un detalle importante.

event.target será el elemento real que ha recibido el clic. Resulta que la única forma de hacer clic exactamente en el propio elemento dialog es si haces clic en el fondo. Si haces clic en cualquier otro lugar del interior, event.target será uno de estos elementos. Así que son tres ingeniosas líneas de código, pero el resultado es perfecto. Haz clic aquí, sin problemas. Haz clic ahí, cerrado.

Animación CSS para el fundido de entrada

Llegados a este punto, ¡estoy contento! Pero este tutorial no trata de hacer cosas buenas, sino cosas geniales. Siguiente paso: Quiero que el elemento dialog se desvanezca. Podríamos hacerlo con una transición CSS. Pero otra opción es una animación CSS. Lo sé, transiciones, animaciones... CSS tiene muchas.

Una animación es algo que aplicas a un elemento y... simplemente... hará esa animación para siempre. O puedes hacer que se anime sólo una vez. Por ejemplo, podemos hacer que este botón se anime arriba y abajo para siempre. Una de las cosas buenas de las animaciones es que puedes hacer que una animación sólo ocurra una vez... y no empezará hasta que el elemento se haga visible en la página. Por ejemplo, podríamos crear una animación de opacidad 0 a opacidad 100, que se ejecutaría en cuanto nuestro dialog se hiciera visible.

Tailwind tiene algunas animaciones incorporadas, pero no una para el desvanecimiento. Así que la añadiremos. Abajo, en tailwind.config.js, pegaré sobre la tecla theme:

29 lines | tailwind.config.js
// ... lines 1 - 3
module.exports = {
// ... lines 5 - 9
theme: {
extend: {
animation: {
'fade-in': 'fadeIn .5s ease-out;',
},
keyframes: {
fadeIn: {
'0%': { opacity: 0 },
'100%': { opacity: 1 },
},
},
},
},
// ... lines 23 - 27
}

Esto es principalmente material de animación CSS: añade una nueva llamada fade-in que pasará de opacidad 0 a 100 en 1/2 segundo.

Para utilizarlo, busca el elemento dialog y añade animate-fade-in:

82 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
data-controller="modal"
class="flex justify-between"
>
// ... lines 11 - 20
<dialog
class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%] animate-fade-in backdrop:bg-slate-600 backdrop:opacity-80"
// ... lines 23 - 24
>
// ... lines 26 - 49
</dialog>
</div>
// ... lines 52 - 79
</div>
{% endblock %}

Pruébalo. ¡Precioso! ¿Podríamos desvanecerlo? Claro, pero en realidad me gusta que se cierre inmediatamente. Así que voy a omitirlo.

Módulos y caché de página turbo

Vale, tengo un último detalle antes de despedirte por hoy. Cuando añadimos las transiciones de vista, en app.js, desactivamos una función de Turbo llamada caché de página... porque aparentemente no siempre funciona bien con las transiciones de vista. Cuando las transiciones de vista sean estándar en Turbo 8, supongo que esto no será un problema.

De todos modos, cuando la caché está activada

42 lines | assets/app.js
// ... lines 1 - 20
document.addEventListener('turbo:load', () => {
// View Transitions don't play nicely with Turbo cache
// if (shouldPerformTransition()) Turbo.cache.exemptPageFromCache();
});
// ... lines 25 - 42

en el momento en que haces clic fuera de una página, Turbo toma una instantánea de la página antes de navegar fuera. Cuando volvemos a hacer clic, es instantáneo: ¡boom! En lugar de hacer una petición a la red, utiliza la versión en caché de esta página. Hay más cosas, pero captas la idea.

Con el almacenamiento en caché activado, una cosa de la que tenemos que preocuparnos es de eliminar cualquier elemento temporal de la página antes de que se tome la instantánea, como mensajes tostados o modales. Porque, cuando hagas clic en "Atrás", no querrás que haya una notificación tostada aquí arriba.

La forma en que solemos resolver esto, por ejemplo en _flashes.html.twig, es añadir un atributo data-turbo-temporary:

34 lines | templates/_flashes.html.twig
{% for message in app.flashes('success') %}
<div
// ... lines 3 - 4
data-turbo-temporary
// ... lines 6 - 7
>
// ... lines 9 - 31
</div>
{% endfor %}

Que le dice a Turbo que elimine este elemento antes de tomar la instantánea.

Probemos a añadir esto a nuestro dialog para que no aparezca en la instantánea. Para ver qué ocurre, abre el modal y haz clic atrás. Eso acaba de tomar una instantánea de la página anterior. Ahora haz clic hacia adelante. Vaya. Estamos en un estado extraño. Parece que el diálogo ha desaparecido... pero no podemos desplazarnos y la página está borrosa.

Eso es porque necesitamos hacer algo más que ocultar el dialog: necesitamos eliminar estas clases del cuerpo. Básicamente, antes de que Turbo tome la instantánea, ¡necesitamos algo que llame al método close()!

¡Y podemos hacerlo! En index.html.twig, en el elemento controlador raíz -aunque esto podría ir en cualquier sitio- añade un data-action="". Justo antes de que Turbo tome su instantánea, envía un evento llamado turbo:before-cache. Podemos escucharlo y luego llamar a modal#close. El único detalle es que el evento turbo:before-cache no se envía a un elemento específico. Así que escucharlo en este elemento no funcionará. Se envía por encima de nosotros, a la ventana. Es un evento global.

Afortunadamente, Turbo nos proporciona una forma sencilla de escuchar los eventos globales añadiendo@window:

83 lines | templates/voyage/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="m-4 p-4 bg-gray-800 rounded-lg">
<div
// ... line 8
data-action="turbo:before-cache@window->modal#close"
// ... line 10
>
// ... lines 12 - 51
</div>
// ... lines 53 - 80
</div>
{% endblock %}

Es un poco técnico, pero con este único arreglo, podemos abrir el modal, retroceder, avanzar, y la página queda preciosa.

¡Guau! Hoy ha sido un día enorme, ¡pero mira lo que hemos conseguido! Un bonito sistema modal sobre el que tenemos un control total. Mañana va a ser igual de grande, ya que daremos vida a este modal con contenido y formularios dinámicos reales. Hasta entonces.