Tostadas más elegantes: Auto-cierre y Desvanecimiento
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 SubscribeAyer preparamos un bonito sistema de notificaciones Toast que funciona completamente con CSS y el sistema flash normal de Symfony. Vale, y sólo un poquito de JavaScript para, boop, cerrarlo.
Hoy vamos a llevar esto al siguiente nivel. Quiero que estos brindis sean increíbles.
Añadir cierre automático
La primera función que vamos a añadir es el cierre automático: un clásico en el mundo de las tostadas, en el que el mensaje aparece en nuestra pantalla y se cierra automáticamente al cabo de unos segundos. Pero también quiero que nuestro controlador de cierre sea reutilizable. Puede haber otras partes del sitio en las que queramos poder cerrar algo... pero no que se cierre automáticamente.
Así que necesitamos una forma de activar el cierre automático caso por caso. La forma de pasar información a un controlador es mediante valores. Añade iguales a static values... e inventaré uno nuevo llamado autoClose, que será un Number:
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| static values = { | |
| autoClose: Number, | |
| }; | |
| // ... lines 7 - 18 | |
| } |
A continuación, añade un método connect(). La idea es que si tenemos this.autoCloseValue -así es como se hace referencia a eso-, entonces... ¡en realidad es perfecto! UtilizaremossetTimeout para cerrar después de esa cantidad de milisegundos:
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 7 | |
| connect() { | |
| if (this.autoCloseValue) { | |
| setTimeout(() => { | |
| this.close(); | |
| }, this.autoCloseValue); | |
| } | |
| } | |
| // ... lines 15 - 18 | |
| } |
Para terminar, vamos a donde utilizamos este controlador - _flashes.html.twig - para pasar el nuevo valor autoClose. Lo hacemos en el mismo elemento que el data-controller. Añadimos data-closeable-auto-close-value igual y utilizamos 5.000 para 5 segundos:
| {% for message in app.flashes('success') %} | |
| <div | |
| // ... lines 3 - 6 | |
| data-closeable-auto-close-value="5000" | |
| > | |
| // ... lines 9 - 26 | |
| </div> | |
| {% endfor %} |
El formato es data- el nombre del controlador, auto-close - que es el nombre del valor autoClose... pero como estamos en un atributo HTML, utilizamos el "dash case" - luego la palabra value equals y finalmente lo que queremos pasar. Este formato es más difícil de recordar que sólo data-controller. Pero como has visto, si tienes este plugin Stimulus para PhpStorm, lo autocompleta, lo que ayuda mucho.
¡Vamos a hacerlo! Edita este registro, guarda y 1, 2, 3, 4, 5... ¡zas! Desaparece.
Cerrar automáticamente la barra del temporizador
¿Qué es lo siguiente en nuestra búsqueda de la grandeza de las tostadas? ¿Qué tal una barra temporizadora que muestre cuándo se cerrará la tostada? Una pequeña barra que se vaya haciendo cada vez más pequeña y que finalmente desaparezca justo cuando la tostada se cierra automáticamente.
¡Suena genial! Este es el plan: vamos a añadir un elemento aquí abajo y luego animaremos su anchura del 100% al 0% durante esos 5 segundos. Para poder encontrar ese elemento, dentro del controlador, vamos a utilizar un objetivo. Añadestatic targets = ['timerbar']:
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 7 | |
| static targets = ['timerbar'] | |
| // ... lines 9 - 26 | |
| } |
Luego abajo en connect(), comprueba que: si this.hasTimerbarTarget, entoncesthis.timerbarTarget.style.width = 0:
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 9 | |
| connect() { | |
| if (this.autoCloseValue) { | |
| // ... lines 12 - 15 | |
| if (this.hasTimerbarTarget) { | |
| // ... line 17 | |
| this.timerbarTarget.style.width = 0; | |
| // ... line 19 | |
| } | |
| } | |
| } | |
| // ... lines 23 - 26 | |
| } |
Suponiendo que hemos añadido una transición CSS a este elemento, eso debería animar el cambio de ancho completo a 0. Ah, pero otro detalle: añade un setTimeout y pon esto dentro con un retardo de 10 milisegundos:
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 9 | |
| connect() { | |
| if (this.autoCloseValue) { | |
| // ... lines 12 - 15 | |
| if (this.hasTimerbarTarget) { | |
| setTimeout(() => { | |
| this.timerbarTarget.style.width = 0; | |
| }, 10); | |
| } | |
| } | |
| } | |
| // ... lines 23 - 26 | |
| } |
Esto permitirá que el elemento se establezca en la página con una anchura completa del 100%, antes de cambiarlo a 0. Se trata de un truco de transición CSS. Si añades o desocultas un elemento e inmediatamente cambias su anchura a 0... la transición CSS no funcionará. Tienes que dejar que el elemento esté en la página con una anchura del 100% durante 1 fotograma de animación, y luego cambiarlo.
Muy bien, con el escenario preparado, es hora de añadir la barra del temporizador. En la parte inferior de_flashes.html.twig, la pegaré:
| {% for message in app.flashes('success') %} | |
| <div | |
| // ... lines 3 - 7 | |
| > | |
| // ... lines 9 - 27 | |
| <div | |
| class="absolute bottom-0 left-0 h-1 bg-green-500 w-full transition-all duration-[5000ms] ease-linear" | |
| // ... line 30 | |
| ></div> | |
| </div> | |
| {% endfor %} |
Esto tiene una posición absoluta en la parte inferior, a la izquierda del padre, con una altura y un fondo verde. También tiene una anchura explícita: es w-full. Esto es importante para la transición.
Para hacer de esto un objetivo, añade data-closeable-target="timerbar":
| {% for message in app.flashes('success') %} | |
| <div | |
| // ... lines 3 - 7 | |
| > | |
| // ... lines 9 - 27 | |
| <div | |
| class="absolute bottom-0 left-0 h-1 bg-green-500 w-full transition-all duration-[5000ms] ease-linear" | |
| data-closeable-target="timerbar" | |
| ></div> | |
| </div> | |
| {% endfor %} |
¡Vale! Veamos qué aspecto tiene. Pulsa editar, guardar, y se abre... pero sin animación. ¡Hagamos un poco de depuración! No hay errores en mi consola. Ah... aquí está el problema: debería haber hecho caso a mi editor: timerbarTarget.
Cerremos esto. Guarda y... ¡eso es lo que quiero ver! Y justo cuando llega a 0, boop, se cierra.
Vale, me encanta cómo queda esto. Pero nuestro brindis se merece un último detalle: un elegante fundido de salida... en lugar de esta salida abrupta.
Transición CSS al cerrar
Desvanecer las cosas es un poco complicado. Puedes utilizar transiciones CSS -y lo haremos- para pasar de la opacidad 100 a la 0. Pero entonces también necesitas algo de JavaScript para esperar a que termine esa transición CSS y poder eliminar finalmente el elemento de la página o, al menos, establecer su visualización en none.
Para ayudarnos con esto, vamos a utilizar una biblioteca llamada stimulus-use. Los componentes de Stimulus -como vimos antes- son una lista de controladores de Stimulus reutilizables.stimulus-use es un grupo de comportamientos que puedes añadir a tus controladores de Stimulus. Y aquí hay un montón de herramientas interesantes.
La que vamos a utilizar se llama useTransition. Así que primer paso, vamos a instalarlo. Ejecuta:
php bin/console importmap:require stimulus-use
¡Genial! Luego, en el controlador, impórtalo conimport { useTransition } from 'stimulus-use':
| // ... line 1 | |
| import { useTransition } from 'stimulus-use'; | |
| // ... lines 3 - 36 |
Para activar un comportamiento, lo llamas desde connect(): useTransition(this) y le pasas las opciones que necesites. Voy a pegar unas cuantas:
| // ... lines 1 - 3 | |
| export default class extends Controller { | |
| // ... lines 5 - 10 | |
| connect() { | |
| useTransition(this, { | |
| leaveActive: 'transition ease-in duration-200', | |
| leaveFrom: 'opacity-100', | |
| leaveTo: 'opacity-0', | |
| transitioned: true, | |
| }); | |
| // ... lines 18 - 29 | |
| } | |
| // ... lines 31 - 34 | |
| } |
Esto es lo que significa Mientras este elemento esté "saliendo" u ocultándose, la biblioteca añadirá estas tres clases. Esto establece que, en caso de que cambie alguna propiedad CSS en este elemento, queremos tener una transición de 200 milisegundos. El leaveFrom significa que, en el momento en que empiece a ocultarse, la biblioteca le dará esta clase: establecer su opacidad en 100. Después, un milisegundo más tarde, eliminará esta clase y añadirá opacity-0. Ese cambio desencadenará la transición de 200 milisegundos. Por último, transitioned true es una forma de decirle a la biblioteca que empezamos en estado visible... porque también puedes utilizar esta biblioteca para empezar oculto y luego hacer la transición para que tu elemento sea visible.
Ahora que hemos inicializado el comportamiento, nuestro controlador tiene mágicamente dos nuevos métodos: leave() y enter(). Aquí abajo, en close(), en lugar de eliminar el elemento nosotros mismos, digamos this.leave():
| // ... lines 1 - 3 | |
| export default class extends Controller { | |
| // ... lines 5 - 31 | |
| close() { | |
| this.leave(); | |
| } | |
| } |
¡Probemos esto! Gira, actualiza y guarda. Observa. Ah, ha sido rápido, ¡pero es exactamente lo que queríamos! Nuestra notificación de tostada está pulida y lista.
La aventura de mañana: sumergirnos en la tercera y última parte de Turbo: Los flujos. Éstos son la navaja suiza de Turbo, y nos permitirán resolver toda una nueva serie de problemas.
18 Comments
While time showing up the message is perfectly configurable (data-closeable-auto-close-value="2000") the time for the timebar is hardcoded.
`<div
What's you recommendation for solving that issue?
Hey @lid
An option would be to pass a Twig variable from the controller and generate the CSS class dynamically
{{ 'duration-['~time~'ms]' }}Cheers!
Thanks for your suggestion. Unfortunately, I would prefer a solution where configuration needs to be done at one position only. The notification is a component, a small one - at the client-side.
I managed to get it working this way:
In closeable_controller.js
Does something speak against it?
Thank you very much!
It requires a bit more JS code but if you plan to re-use that controller in many places I think it's a better approach than mine
Cheers!
Hi and thank you for this great tutorial.
I find that this "simple" approach to setting up flash messages is great, however, it seems to be limited in the case of different asset configurations. That is, if there is a "Backend" and a "Frontend" part, each with its own asset system, we have to put a data-turbo-track attribute for the page to completely reload.
So, if we emit a flash message from the backend for it to display on the frontend, there's a kind of double page refresh, and therefore the flash message is "lost" in between.
I don't know if there's a solution to manage this.
Hey @labfordev
Can you tell me why it would trigger a double-page refresh? I think that's more related to how your frontend and backend communicate with each other.
Actually, I'm using Vite.js to manage my assets, and it behaves and functions quite similarly to Webpack Encore. I have an asset configuration for the backend and an asset configuration for the frontend. Each with its own stimulus controllers, etc...
On the import of the JS entry file into my HTML, I added, for both the back and front parts, a data-turbo-track="reload" attribute so that the browser reloads the page when I switch from the backend to the frontend and vice versa.
When I launch a flash message from the backend, for example, or even more concretely from an event listener at the moment of user logout, it's impossible to display this flash message on the frontend (landing page following the logout).
It seems like turbo makes an ajax call to the frontend, and since it sees the data-turbo-track attribute, it reloads the page completely. Consequently, the flash message seems to have already been consumed in the meantime.
I'm not sure if this is very clear... Sorry for my very Frenchy English ^^
I think it closely resembles these issues:
Hey @labfordev!
Yes, I understand! Interesting - and that first issue, indeed, seems to be exactly what you're talking about! That's tricky. Honestly, I can't think of a great workaround :/. From our Symfony app, we have no idea that the first page request is about to be ignored and then a 2nd will follow... so we can't add any intelligence that would say "please don't remove the flash messages yet". The only ideas I have are:
A) Disable Turbo on forms submits where you might know this will happen. This, I admit, is not a super great solution.
B) In theory (?), you could check the
Referer. And if the current request is for the frontend, and theRefererwas for the backend, you could add some special code to "keep" the flashes in the container for an extra request. This... seems like it would work... but it also feels like there could be cases I'm not thinking about. To "keep" the flash messages for an extra request, you could... possibly create a custom Twig function -is_switching_areas()- that detects this situation. Then usepeekon the flashbag instead ofget().Sorry I can't be more helpful, but I am interested how you solve this :).
Cheers!
Hey @weaverryan!
Thank you for your great response! Indeed, it's quite a unique "problem" :p.
I'll try to look into it when I have a bit more time, following what you just said.
I'll get back to you if I find a solution.
Cheers!
Yea... those issues seem related to your problem. I don't know what's causing it. I'll see if Ryan can help us out
Thank you very much :)
Small typo in the script:
Should be autoClose?
Thanks for the mention @Rudi-T! All fixed now
Hi @weaverryan
can you please share, which Plugin(s) do you use for the autocomplete (on 1:04 by example) ?
Hey @Chris-56789 ,
Good question! That's a GitHub Copilot PhpStorm's plugin: https://github.com/features/copilot - a kind of AI autocompletion.
Cheers!
Thanks
Nice! I'd add a
data-turbo-temporaryattribute to the notification message itself so it's not added to the page cache if the user leaves the page before the notification is closed/removed (it would cause a weird behavior if they get back to the page again and the notification is in the page cache, they'd see it for a short moment as the live page version is being fetched and replaces the cached version). This means the notification will be removed from the document whenever the user goes to another page before the notification closes. To avoid losing the notification when a user leaves the page, you could make the notifications wrapper elementdata-turbo-permanentand give it an ID, but unfortunately, this has a weird side-effect of not adding new notifications to the wrapper element if you're doing that with regular flash+redirects (since the permanent elements won't be touched)... maybe that's also fixed with Turbo Streams?Hey @tonysm!
Absolutely! I do this too. I forgot to do it in the video, but added it later... and meant to add a note to this chapter, you just reminded me to add! :).
Hmm, interesting! Indeed, streams would fix this. I've never done it before, but instead of actually rendering the streams inside of the
<div id="flash-container">, you could render them right below/above this as a stream:This should work. It looks a bit odd - instead of physically rendering into
flash-container... you render an element that says "render this intoflash-container", but it should work. I'd love to know how it works in practice!Cheers!
"Houston: no signs of life"
Start the conversation!