Diálogo HTML para Módulos
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeBienvenido 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:
| // ... 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:
| // ... 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:
| // ... 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">:
| // ... 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":
| // ... 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":
| // ... 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():
| // ... 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:
| // ... 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'):
| // ... 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:
| // ... 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:
| // ... 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():
| // ... 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:
| // ... 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():
| // ... 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:
| // ... 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:
| // ... 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
| // ... 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:
| {% 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:
| // ... 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.
32 Comments
As an alternative of hardcoding (Tailwind) classes into the Stimulus controller, you can also use Stimulus CSS Classes.
They use a logical name approach, but can be a bit tricky to understand and write.
For example:
Or alternatively use the Twig helper:
And add to
modal_controller.js:And use the class:
I admit this might not the best use-case to leverage this, because you probably want the overflow-hidden behaviour everytime you're using the
modal_controller.But still wanted to share the solution. Especially when using multiple classes this can help out.
Cool! I was not aware of that feature. Thank you for sharing it!
If anyone has issues getting the modal to close by adding
data-turbo-temporary, check you came to the page via a Turbo navigation. If you've landed on the page without a Turbo transition then going back & forward again won't remove the dialog.This took me a while to work out. I hope I save someone the same.
I think that this might have happened because I initially navigated to the page before I commented out the
if (shouldPerformTransition()) Turbo.cache.exemptPageFromCache();inapp.js.Yo @ToG!
That might make sense. When I read your first comment, I wasn't sure I understood: the element should be removed not matter how you got to the pae. So this explanation, indeed, makes more sense to me :).
Cheers!
Regarding the "blur on backdrop" topic... got that working with CSS:
Check out the navigation menu: https://www.bikesport-schindler.de/
Hey @barbieswimcrew ,
That's cool! Thanks for sharing it with others :)
Cheers!
I use Encore to build the project. And I added the line in app.js.
After running 'npm run build' in the console, I get the following error:
does anyone use Encore? And can someone help me with the problem?
Hey @Bart-V
It is telling you the package
turbo-view-transitionswas not found (for some reason). Can you double-check that it's installed?You can install it by running
php bin/console importmap:require turbo-view-transitionsCheers!
Great tutorial, thanks !
Related to the Turbo Page Cache issue discuss at the end of the video,
data-action="turbo:before-cache@window->modal#close"was not working for me. After reading the documentation (https://turbo.hotwired.dev/reference/events#document) I replacedwindowbydocumentand it's working, sodata-action="turbo:before-cache@document->modal#close". I don't know if it's due to a recent update or something else, just wanted to give the information.I have an other issue however. When I am on the page containing the button and the modal, then I go to another page, then I decide to go back to the previous page using the back button of my browser, nothing happen when I click on the button to open the modal. I must refresh the page to be able to open the modal via the button. In other words, navigate to the page containing the modal via the back navigation deactivate somehow the button. Does anyone know how to solve this ?
Hey @BigBenJr
Thank you for the observation. I'm not sure why "window" is not working for you - I've used it before and it works
About the modal, when you go back, does the
openattribute remains on thedialogelement? I'm guessing theclose()method is missing something to make this work on all scenariosCheers!
Thanks for your reply @MolloKhan !
I just realized that because I added
data-turbo-temporaryto the dialog element, when I navigate to the page containing the modal via the back navigation, because the page I see is from the cache, the dialog element is not anymore on the page. If I don't adddata-turbo-temporaryto the dialog I don't have this bug anymore. Anyway, as I useturbo:before-cache@document->modal#closeI don't needdata-turbo-temporaryanymore.EDIT: I just realized as well that the 2 problems were related. Since I removed
data-turbo-temporarynow I may usewindowinstead ofdocument.Ha! I love it when a single change fixes the whole thing. Cheers!
I'm having inconsistencies in the fadein animation "'fade-in': 'fadeIn .5s ease-out;'," between local and production.
I'm hosting a containerized symfony app on Azure.
The problem is the animation duration. It works fine on my local environment, and the fade in animation is correctly defined on the dialogue element as "fadeIn 0.5s ease-out" in the chrome dev tools.
But when the container is pushed to production, I still see the animate-fade-in class on the dialogue but the definition changed to just "fadeIn ease-out", with the duration missing and so no animation actually occurs.
Hey Nick,
Hm, probably something is missing. It's impossible to know for sure, so I may just give you some tips on where to look.
Make sure it's not a cache problem on production, try to open the website in a different browser, in incognito mode, and try hard reloading in your browser. If you're not using hashes for your CSS files, browsers can cache assets that lead to this behavior. But still, even if you're using hashes for assets, try to use hard reload, incognito mode, and a different browser. Such services like Azure may have some layers of caching for assets, probably it's a good idea to make sure that cache is not the reason, I bet there should be some mechanisms to refresh it.
Also, probably the Symfony prod mode causes this problem, try to clear all the cache and run your project locally in Symfony
prodmode. Do you still have that 0.5s animation? Probably you include some extra CSS files only in prod Symfony mode and that overrides the default behaviour that you have in dev mode.And finally, double-check that nothing in your CSS files can override the default behavior, probably you have the same declaration in a few files that overrides each other, the files' load order may sometimes be different on a different OS causing a weird behavior, like on some it works and on some it does not.
I would also suggest you to double-check your build process locally and on production, probably you have different versions of some tools that are installed, and that version difference may cause different behavior.
I hope that helps!
Cheers!
Hi Ryan,
I'm using the dialog on page that contains a form, and it turns out that when the modal closes, it submit the form. I had to add an event.preventDefault() in the close method.
It seems the <form method="dialog"> conflicts with the main on. I removed it and tied the close button to a close() method in the controller. I'll follow the next course and see what happen when posting the form in the modal... but i fear this will mix up badly.
Hey Denis,
Are you trying to follow this course on your personal project? We do recommend you download the course code and follow this course from the start/ directory to have the exact code the course author has in the videos. Yes, you can follow it on a personal project, or a fresher (latest) Symfony version but it may cause more work from your side. In your case, I suppose you need to improve the business logic that's responsible for sending the form, probably using a more specific CSS selector to send the form that is only in the dialog in case it's trying to send every form on the page, etc.
I hope this helps!
Cheers!
Feeling a bit ashamed for asking, since I haven't paid yet for any course. In future, while hopefully beeing wealthier, I will.
For the form to work within a dialog, the method needs to be set as 'dialog'
Normally I'm using twig helpers for creating forms. But that doesn't seem to work in this case, am I write?
Doing so, according to the docs and as expected, when using something different than 'get' and 'post' as method, 'post' is used and the specified method gets put in a hidden form field like this:
Do I just have to write the starting of the form by hand in that case?
Hey Nexo,
If you have a Symfony form type where you want to add a hidden field - I would recommend you to add that field right in the form type, see the
HiddenTypefor that specific case: https://symfony.com/doc/current/reference/forms/types/hidden.htmlThen you will be able to render your form with Twig helper, e.g.
{{ form(form) }}.But in case you want/need to put that field manually in the form - then you can render your form with
{{ form_start(form) }}and{{ form_end(form) }}methods, between which you will place your custominput. But putting extra fields into your form manually may lead to an error in Symfony forms, so better to do it in the form type and put manually only buttons - those are safe to be added to the form.Cheers!
Hi Victor,
Thank you for the answer. I might have been not clear enough. I was talking about when setting the form type in the controller other than get/put, this hidden field will be generated by Symony. In my case it was 'dialog', because that's what I thought is necessary for using forms in <dialog>. I couldnt make the modal dialog getting automatically closed while using a form created in a controller. So I thought, I need to write the form start by hand, instead of using {{form_star(form) }}, for having 'dialog' as form method instead of a hidden field.
While using method 'dialog' would clearly close the dialog, the form didnt got submitted. Back using 'put' as form method, the form got submitted, but the dialog kept beeing open. Eventually I found my problem.
I messed up the empty <turbo-frame id="modal"> while extending twig templates.
Greetings!
Hey Nexo,
Oh, I see now... Glad you were able to find the problem youself, sometimes it's very simple problem but difficult to be spotted, and you need to double-check everything. Thanks for sharing the evential solution with others btw! It definitely might be useful or someone
Cheers!
What if we would need to fire this dialog by clicking on different elements on the page, e.g. by buttons in header and footer so we couldn't surround and put it into same button container/controller?
Should we avoid then using Stimulus, or make some application controller with global, custom events firing or what would be the pattern?
Thank you!
Hi @Kirill!
Three possibilities on this:
A) If you load what's in the modal via Ajax, you can use our AJAX-loading system that we build in the next few days to load from anywhere.
B) In many cases, if you have multiple button that open the same modal (and it is not an Ajax modal), then it's often simple enough to embed that modal in all 3 places. I don't see a huge problem with that, though I'm sure you will have some cases where you have a big modal and really want to avoid this.
C) You can always create some small system to do what you're saying. I have created a
modal-open-controller.jsbefore that I put in the<button>that should open a modal. I pass in a value calledidto the controller. The controller then finds a the modal on the page with thatidand opens it (it opens it by firing and event on the modal - e.g.modal:open- which I list for from mymodal-controller.js... though even easier might just be to put theidon the<dialog>itself, then calldocument.getElementById(this.idValue).showModal().The point is: we have a few options and they're all pretty great and are fairly simple.
Cheers!
Hi! Thank you!
In
model_controller:clickOutside()I expected you to callthis.close()instead ofthis.dialogTarget.close(). I understand that callingdialogTarget.close()triggersmodel_controller.close()but couldthis.close()have been used instead and had the same result?Yo @kbond!
Yup, definitely. The way we've written things (which I really like), we can call
this.close()directly... orthis.dialogTarget.close()(which will then trigger ourthis.close()). Probablythis.close()would be even a bit simpler, I admit :).Cheers!
I searched for this question in the comments, I had the same reasoning ! Thanks @kbond !
I have mixed feelings about that. In close() method, we called the dialog element close() method.
But, with the same syntax we call the close() method of our controller. Did I understand good ?
Hey @Adn-B!
Let me see if I understand what you're asking. Let's look at the 2 situations of what we could do inside of
clickOutside():this.dialogTarget.close(). In this case, the<dialog>will, of course, close. That will trigger the browser to dispatch thecloseevent on the<dialog>. Because of ourdata-actionon the<dialog>, that will call theclose()method on our controller. This time, because the<dialog>is already closed, it will not close again. But we CAN run our cleanup code.this.close(). This case should have the same result, but it's a bit simpler: ourclose()method closes the dialog and cleans things up. Closing the dialog should also trigger theclosemethod from the browser... and that should causeclose()to be called again. But because the dialog is already closed, we can't close it again. I think our "cleanup clode" inclose()may be called twice, but it's not a big deal.Let me know if that helps... or if I just answered the completely wrong question ;).
Cheers!
I'm seeing an error with the adding of the
turbo:before-cache@window->modal#closeaction.This is what I see in the console…
Anyone else had this & worked out why?
OK. So I've updated the
close()method as follows as it appears that Firefox Developer Edition may remove the dialog before calling theclose()handler…I wonder if this is something to do with the View Transitions support in FF?
Same problem here, thanks @ToG
@julien_bonnier was it also on FF for you? If so, normal version of dev edition?
I'll add a note about this either way - just curious to learn more.
Thanks!
"Houston: no signs of life"
Start the conversation!