Tablas de datos con Turbo Frames
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 SubscribeNuestra configuración similar a las tablas de datos funciona. Y es casi genial. Lo que no me gusta es cómo salta de un lado a otro. Cada vez que hacemos clic en un enlace, salta de nuevo a la parte superior de la página. Buf.
Eso es porque Turbo está recargando la página completa. Y cuando lo hace, se desplaza a la parte superior... ¡porque eso es lo que solemos querer! Pero esta vez no. Quiero que nuestra tabla de datos funcione como una pequeña aplicación: donde el contenido cambia sin desplazarse.
¿Turbo 8 Morphing?
Hay dos formas estupendas de resolver esto. En Turbo 8 -que aún no se ha lanzado, es la Beta 1 en el momento de grabar esto- hay una nueva función llamada refresco de página que aprovecha el morphing. Sin obsesionarme demasiado -y quiero hacerlo-, al navegar a la misma página -como hacen nuestro formulario de búsqueda, la ordenación por columnas y los enlaces de paginación- podemos decirle a Turbo que sólo actualice el contenido de la página que ha cambiado... y que conserve la posición de desplazamiento. Así que estate atento a esto.
Añadir un marco Turbo
La segunda forma -que hoy funciona fantásticamente- es rodear toda esta tabla con un <turbo-frame>. En homepage.html.twig, busca el table. Aquí está: este div representa la tabla. Encima, añade <turbo-frame id="voyage-list">. Pon sangría a este div... y también a estos enlaces de paginación: queremos que estén dentro del marco Turbo para que, cuando hagamos clic en ellos, hagan avanzar el marco y se actualicen:
| // ... lines 1 - 27 | |
| {% block body %} | |
| <div class="flex"> | |
| // ... lines 30 - 36 | |
| <section class="flex-1 ml-10"> | |
| // ... lines 38 - 55 | |
| <turbo-frame id="voyage-list"> | |
| <div class="bg-gray-800 p-4 rounded"> | |
| <table class="w-full text-white"> | |
| // ... lines 59 - 120 | |
| </table> | |
| </div> | |
| <div class="flex items-center mt-6 space-x-4"> | |
| // ... lines 124 - 132 | |
| </div> | |
| </turbo-frame> | |
| </section> | |
| </div> | |
| {% endblock %} |
Probemos esto. Borra esa búsqueda. Y oh... precioso. ¡Fíjate! Todo se mueve dentro del marco. Prueba la paginación. ¡Eso también! Todos nuestros enlaces apuntan a la página de inicio... y la página de inicio, por supuesto, tiene este marco.
Pero recuerda: ahora que esta tabla vive dentro de un marco Turbo, si tenemos algún enlace dentro, dejará de funcionar. De nuevo, para solucionarlo, en cada enlace, añadedata-turbo-frame="_top". O para ser más conservador, ve hasta el nuevo<turbo-frame> y añade target="_top". Si haces eso, también tendrás que encontrar los enlaces de ordenación y paginación que deben navegar por el marco y añadirdata-turbo-frame="voyage-list".
Pero los eliminaré... porque no tenemos ningún enlace en el cuadro.
Orientación de la búsqueda en el formulario
En este punto, ¡los enlaces de paginación y ordenación funcionan perfectamente! Pero... ¿la búsqueda? La búsqueda sigue siendo una recarga completa de la página. ¡Eso tiene sentido! No la puse dentro del marco. ¿Por qué? Porque, si lo hubiéramos hecho, al escribir y recargarse el marco, también se habría recargado el cuadro de búsqueda... lo que seguiría restableciendo la posición de mi cursor. Así que no queremos que el formulario se recargue.
¿Podemos... mantenerlo fuera del marco pero dirigirlo al marco cuando se envíe el formulario? Sí, podemos En el elemento form que se envía, añadedata-turbo-frame="voyage-list":
| // ... lines 1 - 27 | |
| {% block body %} | |
| <div class="flex"> | |
| // ... lines 30 - 36 | |
| <section class="flex-1 ml-10"> | |
| <form | |
| // ... lines 39 - 42 | |
| data-turbo-frame="voyage-list" | |
| > | |
| // ... lines 45 - 55 | |
| </form> | |
| // ... lines 57 - 135 | |
| </section> | |
| </div> | |
| {% endblock %} |
¿No es genial? Ahora cuando actualicemos: mira. ¡Es perfecto! La tabla se carga, pero mantengo el foco de entrada. Esto es precioso.
Añadir una pantalla de carga
¡Y ahora tenemos tiempo para hacer las cosas más extravagantes! ¿Qué tal un indicador de carga en la tabla mientras navega? Para hacerlo evidente, ve a nuestro controlador y añade un sleep() durante un segundo:
| // ... lines 1 - 14 | |
| class MainController extends AbstractController | |
| { | |
| ('/', name: 'app_homepage') | |
| public function homepage( | |
| // ... lines 19 - 25 | |
| ): Response | |
| { | |
| // ... lines 28 - 29 | |
| sleep(1); | |
| // ... lines 31 - 43 | |
| } | |
| } |
Ahora... es lento... y cuando hacemos clic o buscamos, ni siquiera recibimos ninguna señal de que el sitio esté haciendo algo.
¿Cómo podemos añadir un indicador de carga? Este es un punto en el que Turbo nos cubre las espaldas. Así que aquí está el elemento <turbo-frame>. Observa los atributos del final cuando navego. ¿Lo has visto? Turbo añadió un atributo aria-busy="true" mientras se cargaba. Está ahí por accesibilidad, ¡pero también es algo que podemos aprovechar dentro de Tailwind!
Sobre ese elemento <turbo-frame>, aquí está, digamos class="" conaria-busy:opacity-50.
Esta sintaxis especial dice que, si este elemento tiene un atributo aria-busy, aplica el opacity-50. Añade otro aria-busy: con blur-sm para difuminar el fondo. Y para ganar puntos extra, incluye transition-all para que la transición entre opacidad y desenfoque no se produzca de forma brusca:
| // ... lines 1 - 27 | |
| {% block body %} | |
| <div class="flex"> | |
| // ... lines 30 - 36 | |
| <section class="flex-1 ml-10"> | |
| // ... lines 38 - 56 | |
| <turbo-frame id="voyage-list" class="aria-busy:opacity-50 aria-busy:blur-sm transition-all"> | |
| // ... lines 58 - 134 | |
| </turbo-frame> | |
| </section> | |
| </div> | |
| {% endblock %} |
Tip
Para un efecto aún más bonito, también puedes cambiar la opacidad y el desenfoque sólo si la carga tarda más de, por ejemplo, 700 ms. Hazlo añadiendo una clase aria-busy:delay-700.
Refréscala y observa. ¡Qué bonito! Y todo ocurre gracias a 3 clases CSS. Y, aunque me encanta verlo, en MainController, quita la suspensión.
Avanzar el marco
¿Se ha cumplido esta misión? Casi. Hay un problema gigantesco y horrible... con una solución fácil. Cuando buscamos, ordenamos o paginamos, la URL no cambia. Ése es el comportamiento por defecto de los marcos Turbo: cuando navegan, no actualizan la URL. Sin embargo, podemos decirle a Turbo que queremos esto. En el marco Turbo, añade data-turbo-action="advance":
| // ... lines 1 - 27 | |
| {% block body %} | |
| <div class="flex"> | |
| // ... lines 30 - 36 | |
| <section class="flex-1 ml-10"> | |
| // ... lines 38 - 56 | |
| <turbo-frame id="voyage-list" data-turbo-action="advance" class="aria-busy:opacity-50 aria-busy:blur-sm transition-all"> | |
| // ... lines 58 - 134 | |
| </turbo-frame> | |
| </section> | |
| </div> | |
| {% endblock %} |
Avanzar significa que actualizará la URL y avanzará el historial del navegador, de modo que si pulsamos el botón "Atrás", irá a la URL anterior. También puedes utilizar replace para cambiar la URL, pero sin añadirla al historial.
Observa: esta vez cuando busquemos... ¡la URL se actualiza! Y cuando navegamos a la página dos o tres... se actualiza... y podemos pulsar atrás, atrás, y adelante, adelante.
Ahora tenemos una configuración de tablas de datos realmente fantástica... totalmente escrita sin ningún JavaScript personalizado dentro de Twig y Symfony. Qué momento para estar vivo.
El último pequeño problema son los "30 resultados". Cuando buscamos, esto nunca cambia: se queda en el número que había cuando se cargó la página original. Esto se debe a que vive fuera del marco Turbo. La solución más fácil sería moverlo al marco... ¡pero no lo quiero ahí! Visualmente, ¡lo quiero aquí arriba!
Vamos a dejarlo así por ahora. Pero lo arreglaremos dentro de unos días con Turbo Streams.
Mañana, ¡vamos a sumergirnos en una nueva función del navegador! Se llama Transiciones de vista, y nos permitirá aplicar transiciones visuales a cualquier navegación.
19 Comments
Hi ,
Thanks for video.
I don't see any error and I don't see any affects (4:50) - page loading styles.
Just in case I copied homepage.html.twig from your script.
Turbo - works , I don't see any errors in browser console.
How to investigate issue?
BR.
Hey @Ruslan
That's a complex question, these effects depend on the browser, and probably some of them will not work at all. Probably if you share what browser you are using, I will be able to help more.
Cheers.
What is the status on the morphing functionality? I have the latest turbo version but adding the meta tags in the head as per the documentation doesn't seem to do anything
Hey @Nick-F!
Morphing is alive and well on Turbo 8, though I admit that I don't have any real experience with it yet (not because it's bad or anything, I've just not been available!). If you have the
metatags, I can't think of a reason why you wouldn't see the morphing functionality... which I realize isn't much help, but maybe it'll give you more confidence to try again.Cheers and good luck!
Hello.. When I add
data-turbo-frame="voyage-list"to the <form> element (~3:22 in the video), I don't see any change when searching. It still does a full-page reload and the cursor resets to the beginning of the <input> field. I have triple checked everything, but don't know where to look. Any ideas?Hey @CDesign!
Sorry for my VERY slow reply! I would check a few things:
A) Is Turbo running otherwise - like do you see no page reloads during normal navigation? My guess is yes, but we need to check.
B) Watch your browser console closely. Do you see any errors? It's possible that the form is submitting into the frame... but then some error (e.g. a 500 error) is causing the response to NOT contain the matching frame... and then perhaps (I can't remember if it actually does this), the entire page is refreshing.
If you're still having the issue and this doesn't help, let me know!
Cheers!
Hey !
Thanks for the work !
A little suggestion : During this video you said that the table doesn't contain links. However, we have the planet links in the turbo-frame
planet-card-{id}.I don't know it's possible, but it could be cool to know : How can I force a page reload on a link who's come from another turbo-frame in the actual turbo-frame ?
PS : I don't remember, it can be myself who's added the planet link :D.
Hey Adn,
Hm, maybe you add it yourself :p Anyway, if you want to reload content in the specific frame - I believe you need something like this:
I.e.
data-turbo-action="replace"should do the trick, you just need to point that link to specific frame, and specific URL of course. It might be so that it will work with empty href though, i.e. it will be pointed to the same URL you're currently on, but I didn't try it :)I hope this helps!
Cheers!
Thanks for your reply !
My bad, a
target="_top"is enough...It still new for me ! Sorry for the inconvenience ! :D
just for clarification for others
in file _card.html.twig
Hey Adn,
Ah, ok, no problem :) Yeah, it's easy to stuck in new stack ;) thanks for sharing your final solution with others!
Cheers!
Hello!
I noticed when the sleep was removed, we see the quick flash when updating the table. Is there a way to say "only add aria-busy after waiting 300ms"?
Hey @kbond!
After talking with you on Slack, you basically figured this out on your own... then told ME. Yes! It's awesome! We can delay the CSS transition so it only happens if the page load is particularly slow. All in CSS:
I LIKE that
Thanks for this great tutorial!
Adding data-turbo-action="advance" makes the page jump back to the top of the page again when searching, clicking on the sorting or paging links. Removing it fixes that again. I thought I missed something, but copied your exact code and it still does it. Has something changed since?
Also, the planet popovers do have a link... which now have "content missing". they should have a data-turbo-frame="_top" I guess?
Using stimulus '3.2.2 and turbo 7.3.0
Update: the issue with the page jumping to top after using data-turbo-action="advance" happens in Brave browser, not on Firefox, Chrome or Edge.
Hey @escobarcampos!
Ah, fascinating! It works in every other browser? I would not have expected that detail to be browser-dependent...
Hello! I have a few questions about this "homemade datatable" :)
Requests are submitted via Ajax using Turbo Drive (if I understood correctly). But if our application is not ready to accommodate Turbo Drive (since all our Javascript must use Stimulus), what should we do?
This question follows on from the 1st. Do you think it is possible to shift all this logic into a LiveComponent? As well as the method that will return the results? I mean, can we in LiveComponent create a LiveAction that we can set the #[MapQueryParameter] to in order to make this all work?
This question follows on from the second. If it is possible to put all the logic in LiveComponent, if we don't use Turbo Drive (but we can use Turbo Frame since it doesn't require Turbo Drive, if I understood correctly. Or maybe there is be alternatives to make all of this work the same way without ever using Turbo), do you think it is possible to create a trait (like ComponentWithFormTrait but something like "ComponentWithDataTableTrait"), and this trait would define all the logic and we would have some abstract methods to define such as the targeted entity, the repository method to use, the sort columns, etc.. And everything would be done automatically?
Hey @Fabrice!
Sorry for the slow reply! But I'm happy to chat about this topic!
For this example of data tables, the Ajax is submitted via Turbo Frames - we put a frame around the
<table>. So, this only means that any JavaScript behavior inside the frame needs to be written in Stimulus. The rest of your site should be ok without it. So, you would disable Turbo Drive (like we talk about in the Drive chapter), but then use frames for this. It is possible that using theadvancemight not play nicely with a disabled Turbo drive... I'm not sure. It's worth a try!Yup! You'll need this PR - https://github.com/symfony/ux/pull/1230 - in order for the URL on the page to change, but I'll merge that really soon. And you might not even need a
LiveActionI would bind the search input to aLivePropmodel, then the navigation links would trigger a model change for apagemodel - e.g. changing it from 2 to 3.Yea! Definitely! That's a really cool idea. More broadly, I'd love to see a PHP library we could use to help build the "data table" and the sort column links, etc. But, anyway, if you built this all inside of a LiveComponent, indeed, I think there would be a lot of boilerplate code. You could have the
queryas aLiveProp(writable:true), a$page = 1as aLiveProp(writable: true)and also 2 for sort direction and sort column. You could have one abstract method - e.g.getQueryBuilder()- where you would use the$queryprop to create the query builder for that entity. Then a method - e.g.getResults()in your trait - that calls this method, sets the orderBy on it, initializes the pagination and returns it. You would usethis.resultsin your template to loop over. Finally, you might have some sort of helpers to help build the pagination links and column sorting headers.If you try this out, I'd love to know how it goes. It feels generic enough that it could at least be something shared publicly.
Cheers!
Thank you for your reply ! So I will try all this soon when I have time, and post the result to you!
thank you !
"Houston: no signs of life"
Start the conversation!