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 Subscribe¡Has llegado al último capítulo del tutorial de Doctrine! Este capítulo es... un bonus total. En lugar de hablar de Doctrine, vamos a aprovechar algo de JavaScript para convertir esta página en un "scroll eterno". Pero no te preocupes Hablaremos más de Doctrine en el próximo tutorial, cuando tratemos las Relaciones de Doctrine.
Éste es el objetivo: en lugar de enlaces de paginación, quiero que esta página cargue nueve resultados como los que vemos en la página 1. Luego, cuando nos desplacemos hasta el final, quiero hacer una petición AJAX para mostrar los nueve resultados siguientes, y así sucesivamente. El resultado es un "scroll eterno".
En el primer tutorial de esta serie, instalamos una biblioteca llamada Symfony UX Turbo, que habilitó un paquete de JavaScript llamado Turbo. Turbo convierte todos los clics de los enlaces y los envíos de formularios en llamadas AJAX, lo que nos proporciona una experiencia realmente agradable, similar a la de una aplicación de página única, sin hacer nada especial.
Aunque esto es genial, Turbo tiene otros dos superpoderes opcionales: Turbo Frames y Turbo Streams. Puedes aprender todo sobre ellos en nuestro tutorial de Turbo. Pero vamos a dar una muestra rápida de cómo podríamos aprovechar los Turbo Frames para añadir un desplazamiento eterno sin escribir una sola línea de JavaScript.
Los marcos funcionan dividiendo partes de tu página en elementos separados de turbo-frame
, que actúa de forma muy parecida a un iframe
... si eres lo suficientemente viejo como para recordar aquellos. Cuando rodeas algo en un <turbo-frame>
, cualquier clic dentro de ese marco sólo navegará por ese marco.
Por ejemplo, abre la plantilla de esta página - templates/vinyl/browse.html.twig
- y desplázate hasta donde tenemos nuestro bucle for
. Añade un nuevo elemento turbo-frame
justo aquí. La única regla de un turbo marco es que debe tener un ID único. Así que diid="mix-browse-list"
, y luego ve hasta el final de esa fila y pega la etiqueta de cierre. Y, sólo por mi propia cordura, voy a aplicar una sangría a esa fila.
... lines 1 - 2 | |
{% block body %} | |
... lines 4 - 27 | |
<turbo-frame id="mix-browse-list"> | |
<div class="row"> | |
{% for mix in pager %} | |
... lines 31 - 45 | |
{% endfor %} | |
... lines 47 - 48 | |
</div> | |
</turbo-frame> | |
... lines 51 - 52 | |
{% endblock %} |
Bien, entonces... ¿qué hace eso? Si actualizas la página ahora, cualquier navegación dentro de este marco se queda dentro del marco. ¡Fíjate! Si hago clic en "2"... eso ha funcionado. Hizo una petición AJAX para la página 2, nuestra aplicación devolvió esa página HTML completa -incluyendo la cabecera, el pie de página y todo-, pero luego Turbo Frame encontró el mix-browse-list
<turbo-frame>
correspondiente dentro de eso, cogió su contenido y lo puso aquí.
Y aunque no es fácil de ver en este ejemplo, la única parte de la página que está cambiando es este elemento <turbo-frame>
. Si yo... digamos... me meto con el título aquí arriba en mi página, y luego hago clic aquí abajo y vuelvo a la página 2... eso no actualiza esa parte de la página. Una vez más, funciona de forma muy parecida a los iframes, pero sin las rarezas. Podrías imaginar que esto se utiliza, por ejemplo, para alimentar un botón "Editar" que añada edición en línea.
Pero en nuestra situación, esto no es muy útil todavía... porque funciona más o menos igual que antes: hacemos clic en el enlace, vemos nuevos resultados. La única diferencia es que al hacer clic dentro de un <turbo-frame>
no se cambia la URL. Así que, independientemente de la página en la que me encuentre, si actualizo, soy transportado de nuevo a la página 1. Así que esto fue una especie de paso atrás
Pero sigue conmigo. Tengo una solución, pero implica unas cuantas piezas. Para empezar, voy a hacer que el ID sea único para la página actual. Añade un -
, y entonces podremos decir pager.currentPage
.
... lines 1 - 27 | |
<turbo-frame id="mix-browse-list-{{ pager.currentPage }}"> | |
... lines 29 - 49 | |
</turbo-frame> | |
... lines 51 - 54 |
A continuación, en la parte inferior, elimina los enlaces de Pagerfanta y sustitúyelos por otro Marco Turbo. Di {% if pager.hasNextPage %}
, y dentro de él, añade unturbo-frame
, igual que arriba, con ese mismo id="mix-browse-list-{{ }}"
. Pero esta vez, di pager.nextPage
. Permíteme dividir esto en varias líneas aquí... y también vamos a decirle qué src
debe utilizar para ello. Oh, déjame arreglar mi error tipográfico... y luego utiliza otro ayudante de Pagerfanta llamado pagerfanta_page_url
y pásale ese pager
y luego pager.nextPage
. Por último, añade loading="lazy"
.
... lines 1 - 27 | |
<turbo-frame id="mix-browse-list-{{ pager.currentPage }}"> | |
<div class="row"> | |
... lines 30 - 47 | |
{% if pager.hasNextPage %} | |
<turbo-frame id="mix-browse-list-{{ pager.nextPage }}" src="{{ pagerfanta_page_url(pager, pager.nextPage) }}" loading="lazy"></turbo-frame> | |
{% endif %} | |
</div> | |
</turbo-frame> | |
... lines 53 - 56 |
¡Woh! Deja que me explique, porque esto es un poco salvaje. En primer lugar, uno de los superpoderes de un <turbo-frame>
es que puedes darle un atributo src
y dejarlo vacío. Esto le dice a Turbo:
¡Oye! Voy a ser perezoso y empezar este elemento vacío... quizás porque es un poco pesado de cargar. Pero en cuanto este elemento sea visible para el usuario, haz una petición Ajax a esta URL para obtener su contenido.
Así, este <turbo-frame>
comenzará vacío... pero en cuanto nos desplacemos hasta él, Turbo hará una petición AJAX para la siguiente página de resultados.
Por ejemplo, si este marco se está cargando para la página 2, la respuesta Ajax contendrá un <turbo-frame>
con id="mix-browse-list-2"
. El sistema Turbo Frame lo tomará de la respuesta Ajax y lo pondrá aquí, al final de nuestra lista. Y si hay una página 3, incluirá otro Turbo Frame aquí abajo que apuntará a la página 3.
Todo esto puede parecer un poco loco, así que vamos a probarlo. Voy a desplazarme hasta la parte superior de la página, refresco y... ¡perfecto! Ahora desplázate hacia abajo y observa. Deberías ver que aparece una petición AJAX en la barra de herramientas de depuración de la web. Mientras nos desplazamos... aquí abajo... ¡ah! ¡Ahí está la petición AJAX! Vuelve a desplazarte hacia abajo y... hay una segunda petición AJAX: una para la página 2 y otra para la página 3. Si seguimos desplazándonos, nos quedamos sin resultados y llegamos al final de la página.
Si eres nuevo en Turbo Frames, este concepto puede haber sido un poco confuso, pero puedes aprender más en nuestro tutorial de Turbo. Y un saludo a una entrada del blog de AppSignal que introdujo esta genial idea.
¡Muy bien, equipo! ¡Enhorabuena por haber terminado el curso de Doctrine! Espero que te sientas poderoso. ¡Deberías estarlo! La única parte importante que le falta a Doctrine ahora es la de Relaciones de Doctrine: poder asociar una entidad a otra mediante relaciones, como las de muchos a uno y muchos a muchos. Cubriremos todo eso en el próximo tutorial. Hasta entonces, si tienes alguna duda o tienes una gran adivinanza que quieras plantearnos, estamos a tu disposición en la sección de comentarios. ¡Muchas gracias, amigos! ¡Y hasta la próxima vez!
Waow good job again! I have only one thing to say. When you talk about manyToMany entities and say "We'll cover all of that in the next tutorial." could i have the link of that tutorial? I could not see anything else under last tutorial. Only "tool tool tool" section
Hey @Frederic_H-F,
Thanks for kind words! And sorry, but that course is not out yet, so keep an eye on new courses.
PS while waiting, you can check "ManyToMany" chapters here: https://symfonycasts.com/screencast/doctrine-relations it is a bit older (Symfony5), but the main idea is pretty same =)
Cheers!
Hi,
when using "Turbo Frames":
If we click to display the details of a mix, the route "/browse" cannot display the data because it is a hyperlink
There will be no redirect from the "/browse" route to the "/mix/url_of_mix" route.
Hey @Sameone!
You're totally right! In the video, I had forgotten to add a target="_top"
to the turbo-frame
, which forces all links inside to behave like normal links. We DO have a note about this in the video - https://symfonycasts.com/screencast/symfony-doctrine/forever-scroll?playAt=254 - but somehow I forgot to put that note in the text! I'll add that now :).
Cheers!
I have one more question, let's say i want to open the details in new tab, i read somewehere that i can put _blank instead of _top which should work but it doesn't. Is there other solution?
Hey @Stanislaw!
Back from a vacation - sorry for the slow reply! So, doing this doesn't work?
<a href="..." target="_blank">
I would be very surprised by that...
Or do you mean this?
<turbo-frame target="_blank">
If so, I don't think (though I have never tested) that this works. But I would expect the <a target="_blank">
idea to work. Let me know what you find :).
Cheers!
Hi, there's any news about the next Doctrine tutorial?
Thanks a lot Weave Ryan for those amazing series (y).
Hey JnahDev,
Super-secret for now, but we're working on "Go Pro with Doctrine Queries" with the latest Symfony version :) It should be started releasing in a month or so
Cheers!
Hey, when will the next Doctrine tutorial be available?
Thanks a lot for your work!
Hey excentrist!
I'm shooting for January - I definitely want to get the Doctrine relations tutorial out!
Cheers!
Hey guys,
I was wondering, if using turbo frames in production, would that negatively impact a page's SEO score, considering the fact that the potential relevant search results would be initially hidden from the search engines?
What's your thought on this?
Thanks a lot.
Hi roenfeldt!
Excellent question! SEO is complex, but I can fairly confidently say no: turbo frames will NOT negatively impact SEO. In fact, I believe they're designed with this in mind.
Why? Let's look at an example:
<turbo-frame id="featured-product">
...
<a href="/products/blow-up-sofa-couch">See More</a>
</turbo-frame>
You and I understand that, thanks to Turbo Frames, when we click the See More
link, it will only load inside of THIS turbo-frame
. So, at first, it might seem like you have some potential content that a search engine crawler will never see!
But the beauty of Turbo-Frames is that the link URL - /products/blow-up-sofa-couch
- is a REAL URL to a REAL, full page. So if a crawler crawls your page and see this <a>
tag, it will look like a perfectly normal <a>
tag. And when they follow the link, they will load (and index) the full page that renders when you go to /products/blow-up-sofa/couch
. Turbo Frames are a nice bit of magic, but they fall back to real, functional, boring pages.
The only potential spot where SEO could be affected is if you use a "lazy" frame (where it starts empty and then loads after page load or once the item becomes visible). But, most crawlers are good now at letting some JavaScript load, and any application that uses JavaScript to load content a few ms later would share this problem.
Let me know if this helps!
Cheers!
Hello,
Are your turbo-frames nested?
<turbo-frame id="mix-browse-list-1">
<turbo-frame id="mix-browse-list-2">
<turbo-frame id="mix-browse-list-3">
...
</turbo-frame>
</turbo-frame>
</turbo-frame>
Hey Seb,
Yes, in this case, we embedded a turbo-frame containing the HTML of the subsequent pages into the main turbo-frame, which contains the first page results
Cheers!
Hey Diego,
I think it's a pity.
A better solution might be this: https://github.com/thoughtbot/hotwire-example-template/tree/hotwire-example-pagination.
What do you think ?
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.7", // v3.7.0
"doctrine/doctrine-bundle": "^2.7", // 2.7.0
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.12", // 2.12.3
"knplabs/knp-time-bundle": "^1.18", // v1.19.0
"pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
"pagerfanta/twig": "^3.6", // v3.6.1
"sensio/framework-extra-bundle": "^6.2", // v6.2.6
"stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
"symfony/asset": "6.1.*", // v6.1.0
"symfony/console": "6.1.*", // v6.1.2
"symfony/dotenv": "6.1.*", // v6.1.0
"symfony/flex": "^2", // v2.2.2
"symfony/framework-bundle": "6.1.*", // v6.1.2
"symfony/http-client": "6.1.*", // v6.1.2
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
"symfony/runtime": "6.1.*", // v6.1.1
"symfony/twig-bundle": "6.1.*", // v6.1.1
"symfony/ux-turbo": "^2.0", // v2.3.0
"symfony/webpack-encore-bundle": "^1.13", // v1.15.1
"symfony/yaml": "6.1.*", // v6.1.2
"twig/extra-bundle": "^2.12|^3.0", // v3.4.0
"twig/twig": "^2.12|^3.0" // v3.4.1
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"symfony/debug-bundle": "6.1.*", // v6.1.0
"symfony/maker-bundle": "^1.41", // v1.44.0
"symfony/stopwatch": "6.1.*", // v6.1.0
"symfony/web-profiler-bundle": "6.1.*", // v6.1.2
"zenstruck/foundry": "^1.21" // v1.21.0
}
}
Woah that trick is EPIC! Love it!
Great course once again!
Good Job @symfonycasts and @ryanweaver