Buy Access to Course
13.

Paginación y ordenación por columnas

|

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 13! Hoy vamos a dejar de lado Stimulus y Turbo y trabajaremos únicamente con Symfony y Twig. Nuestro objetivo es añadir paginación y ordenación por columnas a esta lista.

Añadir paginación

Me gusta añadir paginación con Pagerfanta. Me encanta esta librería, aunque me pierdo un poco en su documentación. Pero oye: es de código abierto, si no estás contento, ¡ve y arréglalo!

Para utilizar Pagerfanta, instalaremos tres bibliotecas:

composer require babdev/pagerfanta-bundle pagerfanta/doctrine-orm-adapter pagerfanta/twig

¡Genial! Primero hagamos funcionar la parte PHP. Abre src/Controller/MainController.php. La página actual se almacenará en la URL como ?page=1 o ?page=2, así que necesitamos leer ese parámetro de consulta page. Lo haremos con un nuevo atributo#[MapQueryParameter]. Y en realidad, antes... Estaba haciendo demasiado trabajo. Si tu parámetro de consulta coincide con el nombre de tu argumento, no necesitas especificarlo ahí. Así que lo suprimiré en esos dos. Es diferente para searchPlanet: un parámetro que utilizaremos más adelante.

De todos modos, esto leerá el ?page= y lo pondremos por defecto a 1. Ah, y el orden de estos no importa:

39 lines | src/Controller/MainController.php
// ... lines 1 - 12
use Symfony\Component\Routing\Annotation\Route;
class MainController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function homepage(
// ... lines 19 - 20
#[MapQueryParameter] int $page = 1,
#[MapQueryParameter] string $query = null,
#[MapQueryParameter('planets', \FILTER_VALIDATE_INT)] array $searchPlanets = [],
): Response
{
// ... lines 26 - 37
}

A continuación, copia la línea $voyageRepository->findBySearch() y sustitúyela por un objeto Pager: $pager es igual a Pagerfanta::createForCurrentPageWithMaxPerPage():

39 lines | src/Controller/MainController.php
// ... lines 1 - 7
use Pagerfanta\Pagerfanta;
// ... lines 9 - 14
class MainController extends AbstractController
{
// ... line 17
public function homepage(
// ... lines 19 - 23
): Response
{
$pager = Pagerfanta::createForCurrentPageWithMaxPerPage(
// ... lines 27 - 29
);
// ... lines 31 - 36
}
}

El primer argumento es un adaptador: nuevo QueryAdapter luego pega el código de antes. Pero, eso no es del todo correcto: este método devuelve una matriz de viajes:

62 lines | src/Repository/VoyageRepository.php
// ... lines 1 - 17
class VoyageRepository extends ServiceEntityRepository
{
// ... lines 20 - 24
/**
* @return Voyage[]
*/
public function findBySearch(?string $query, array $searchPlanets, int $limit = null): array
{
$qb = $this->findBySearchQueryBuilder($query, $searchPlanets);
if ($limit) {
$qb->setMaxResults($limit);
}
return $qb
->getQuery()
->getResult();
}
// ... lines 40 - 60
}

pero ahora necesitamos un QueryBuilder. Afortunadamente, ya he configurado las cosas para que podamos obtener este mismo resultado, pero como un QueryBuilder a través de: findBySearchQueryBuilder:

62 lines | src/Repository/VoyageRepository.php
// ... lines 1 - 17
class VoyageRepository extends ServiceEntityRepository
{
// ... lines 20 - 40
public function findBySearchQueryBuilder(?string $query, array $searchPlanets, ?string $sort = null, string $direction = 'DESC'): QueryBuilder
{
$qb = $this->createQueryBuilder('v');
if ($query) {
$qb->andWhere('v.purpose LIKE :query')
->setParameter('query', '%' . $query . '%');
}
if ($searchPlanets) {
$qb->andWhere('v.planet IN (:planets)')
->setParameter('planets', $searchPlanets);
}
if ($sort) {
$qb->orderBy('v.' . $sort, $direction);
}
return $qb;
}
}

Pega ese nombre de método.

El siguiente argumento es la página actual - $page - y luego el máximo por página. ¿Qué te parece 10?

39 lines | src/Controller/MainController.php
// ... lines 1 - 6
use Pagerfanta\Doctrine\ORM\QueryAdapter;
// ... lines 8 - 14
class MainController extends AbstractController
{
// ... line 17
public function homepage(
// ... lines 19 - 23
): Response
{
$pager = Pagerfanta::createForCurrentPageWithMaxPerPage(
new QueryAdapter($voyageRepository->findBySearchQueryBuilder($query, $searchPlanets)),
$page,
10
);
// ... lines 31 - 36
}
}

Pasa $pager a la plantilla como la variable voyages:

39 lines | src/Controller/MainController.php
// ... lines 1 - 14
class MainController extends AbstractController
{
// ... line 17
public function homepage(
// ... lines 19 - 23
): Response
{
$pager = Pagerfanta::createForCurrentPageWithMaxPerPage(
new QueryAdapter($voyageRepository->findBySearchQueryBuilder($query, $searchPlanets)),
$page,
10
);
return $this->render('main/homepage.html.twig', [
'voyages' => $pager,
// ... lines 34 - 35
]);
}
}

Eso... debería funcionar porque podemos hacer un bucle sobre $pager para obtener los viajes.

Renderizar los enlaces de paginación

A continuación, en homepage.html.twig, ¡necesitamos enlaces de paginación! En la parte inferior, ya tengo un lugar para ello con los enlaces anterior y siguiente codificados:

90 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<div class="flex">
// ... lines 7 - 13
<section class="flex-1 ml-10">
// ... lines 15 - 82
<div class="flex items-center mt-6 space-x-4">
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Previous</a>
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Next</a>
</div>
</section>
</div>
{% endblock %}

La forma en que se supone que debes mostrar los enlaces Pagerfanta es diciendo {{ pagerfanta() }}y pasando voyages:

91 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<div class="flex">
// ... lines 7 - 13
<section class="flex-1 ml-10">
// ... lines 15 - 82
<div class="flex items-center mt-6 space-x-4">
{{ pagerfanta(voyages) }}
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Previous</a>
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Next</a>
</div>
</section>
</div>
// ... lines 90 - 91

Cuando probamos esto -permíteme aclarar mi búsqueda- la paginación tiene un aspecto horrible... ¡pero funciona! A medida que hacemos clic, los resultados van cambiando.

Entonces... ¿cómo podemos cambiar estos enlaces de paginación de "bla" a "ah"? Hay una plantilla Tailwind incorporada que puedes decirle a Pagerfanta que utilice. Eso implica crear un archivo babdev_pagerfanta.yaml y un poco de configuración. No he utilizado esto antes, así que ¡hazme saber cómo va!

babdev_pagerfanta:
    # The default Pagerfanta view to use in your application
    default_view: twig

    # The default Twig template to use when using the Twig Pagerfanta view
    default_twig_template: '@BabDevPagerfanta/tailwind.html.twig'

Porque... Voy a ser testaruda. Sólo quiero tener los botones anterior y siguiente... y quiero que tengan el mismo estilo que estos. Así que ¡vamos al lío!

Lo primero que tenemos que hacer es mostrar estos enlaces condicionalmente, sólo si hay una página anterior. Para ello, di if voyages.hasPreviousPage, then render. Y, si tenemos una página siguiente, renderiza esa:

94 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<div class="flex">
// ... lines 7 - 13
<section class="flex-1 ml-10">
// ... lines 15 - 82
<div class="flex items-center mt-6 space-x-4">
{% if voyages.hasPreviousPage %}
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Previous</a>
{% endif %}
{% if voyages.hasNextPage %}
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Next</a>
{% endif %}
</div>
</section>
</div>
{% endblock %}

Para las URL, utiliza un ayudante llamado pagerfanta_page_url(). Pásale el paginador,voyages, y luego la página a la que queremos ir: voyages.previousPage. Cópialo y repítelo a continuación con voyages.nextPage:

97 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<div class="flex">
// ... lines 7 - 13
<section class="flex-1 ml-10">
// ... lines 15 - 82
<div class="flex items-center mt-6 space-x-4">
{% if voyages.hasPreviousPage %}
<a href="{{ pagerfanta_page_url(voyages, voyages.previousPage) }}" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Previous</a>
{% endif %}
{% if voyages.hasNextPage %}
<a href="{{ pagerfanta_page_url(voyages, voyages.nextPage) }}" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Next</a>
{% endif %}
// ... lines 90 - 92
</div>
</section>
</div>
{% endblock %}

¡Estupendo! Vamos a probarlo. Refrescar. ¡Me encanta! Falta la página anterior, hacemos clic en siguiente y ya está. Vuelve a hacer clic en siguiente. La página 3 es la última. ¡Ya está!

Para obtener un crédito extra, imprimamos incluso la página actual. Añade un div... y luego imprimevoyages.currentPage, una barra y voyages.nbPages:

97 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<div class="flex">
// ... lines 7 - 13
<section class="flex-1 ml-10">
// ... lines 15 - 82
<div class="flex items-center mt-6 space-x-4">
{% if voyages.hasPreviousPage %}
<a href="{{ pagerfanta_page_url(voyages, voyages.previousPage) }}" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Previous</a>
{% endif %}
{% if voyages.hasNextPage %}
<a href="{{ pagerfanta_page_url(voyages, voyages.nextPage) }}" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Next</a>
{% endif %}
<div class="ml-4">
Page {{ voyages.currentPage }}/{{ voyages.nbPages }}
</div>
</div>
</section>
</div>
{% endblock %}

¡Buen trabajo, IA!

Y... ya está. Página 1 de 3. Página 2 de 3.

Ordenación por columnas

¿Qué pasa con la ordenación por columnas? Quiero poder hacer clic en cada columna para ordenar por ella. Para ello, necesitamos dos nuevos parámetros de consulta. Un nombre de columna sort ysortDirection. ¡Vuelve a PHP! Añade #[MapQueryParameter] a un argumento string llamado $sort. Pon por defecto leaveAt. Ése es el nombre de la propiedad de esta columna de salida. A continuación, vuelve a hacer #[MapQueryParameter] para añadir una cadena$sortDirection que por defecto sea ascendente:

45 lines | src/Controller/MainController.php
// ... lines 1 - 14
class MainController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function homepage(
// ... lines 19 - 21
#[MapQueryParameter] string $sort = 'leaveAt',
#[MapQueryParameter] string $sortDirection = 'ASC',
// ... lines 24 - 25
): Response
{
// ... lines 28 - 42
}
}

Dentro del método, pegaré 2 líneas aburridas que validan que sort es una columna real:

45 lines | src/Controller/MainController.php
// ... lines 1 - 14
class MainController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function homepage(
// ... lines 19 - 25
): Response
{
$validSorts = ['purpose', 'leaveAt'];
$sort = in_array($sort, $validSorts) ? $sort : 'leaveAt';
// ... lines 30 - 42
}
}

Probablemente podríamos hacer lo mismo para $sortDirection, pero me lo saltaré y pasaré afindBySearchQueryBuilder(). Esto ya está configurado para esperar los argumentos de ordenación. Así que pasa $sort y $sortDirection... ¡y debería estar contento!

45 lines | src/Controller/MainController.php
// ... lines 1 - 14
class MainController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function homepage(
// ... lines 19 - 25
): Response
{
$validSorts = ['purpose', 'leaveAt'];
$sort = in_array($sort, $validSorts) ? $sort : 'leaveAt';
$pager = Pagerfanta::createForCurrentPageWithMaxPerPage(
new QueryAdapter($voyageRepository->findBySearchQueryBuilder($query, $searchPlanets, $sort, $sortDirection)),
// ... lines 32 - 33
);
// ... lines 35 - 42
}
}

Por último, vamos a necesitar esta información en la plantilla para ayudar a mostrar los enlaces de ordenación. Pasa sort ajustado a $sort y sortDirection ajustado a $sortDirection:

45 lines | src/Controller/MainController.php
// ... lines 1 - 14
class MainController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function homepage(
// ... lines 19 - 25
): Response
{
// ... lines 28 - 35
return $this->render('main/homepage.html.twig', [
// ... lines 37 - 39
'sort' => $sort,
'sortDirection' => $sortDirection,
]);
}
}

Añadir los enlaces de ordenación de columnas

La parte más tediosa es transformar cada th en el enlace de ordenación adecuado. Añade una etiqueta a y divídela en varias líneas. Establece el href en esta página -la página principal- con un sort adicional establecido en purpose: el nombre de esta columna. Para sortDirection, esto es más complejo: si sort es igual a purposey sortDirection es asc, entonces queremos desc. En caso contrario, utiliza asc.

Por último, además de los parámetros de consulta sort y sortDirection, necesitamos mantener cualquier parámetro de consulta existente que pueda estar presente, como la consulta de búsqueda. Y hay una forma genial de hacerlo: ... y luego app.request.query.all:

136 lines | templates/main/homepage.html.twig
// ... lines 1 - 27
{% block body %}
<div class="flex">
// ... lines 30 - 36
<section class="flex-1 ml-10">
// ... lines 38 - 55
<div class="bg-gray-800 p-4 rounded">
<table class="w-full text-white">
<thead>
<tr>
<th class="text-left py-2">
<a href="{{ path('app_homepage', {
...app.request.query.all(),
sort: 'purpose',
sortDirection: sort == 'purpose' and sortDirection == 'asc' ? 'desc' : 'asc',
}) }}">
// ... line 66
</a>
</th>
// ... lines 69 - 78
</tr>
</thead>
// ... lines 81 - 119
</table>
</div>
// ... lines 122 - 132
</section>
</div>
{% endblock %}

¡Listo! Ah, pero después de la palabra Propósito, añadamos una bonita flecha hacia abajo o hacia arriba. Para ayudarte, pegaré una macro Twig. No suelo utilizar macros... pero esto nos ayudará a averiguar la dirección, y luego imprimir el SVG correcto: una flecha hacia abajo, una flecha hacia arriba, o una flecha hacia arriba y otra hacia abajo:

136 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% macro sortArrow(sortName, sort, sortDirection) %}
{% if sort == sortName %}
{% if sortDirection == 'asc' %}
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-4 h-4" width="24" height="24" 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>
<path d="M6 15l6 -6l6 6"></path>
</svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-4 h-4" width="24" height="24" 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>
<path d="M6 9l6 6l6 -6"></path>
</svg>
{% endif %}
{% else %}
<!-- up and down arrow svg -->
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-4 h-4 text-slate-300" width="24" height="24" 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>
<path d="M8 9l4 -4l4 4"></path>
<path d="M16 15l-4 4l-4 -4"></path>
</svg>
{% endif %}
{% endmacro %}
// ... lines 27 - 136

Aquí abajo... utiliza esto con {{ _self.sortArrow() }} pasando por 'purpose',sort y sortDirection:

136 lines | templates/main/homepage.html.twig
// ... lines 1 - 27
{% block body %}
<div class="flex">
// ... lines 30 - 36
<section class="flex-1 ml-10">
// ... lines 38 - 55
<div class="bg-gray-800 p-4 rounded">
<table class="w-full text-white">
<thead>
<tr>
<th class="text-left py-2">
<a href="{{ path('app_homepage', {
...app.request.query.all(),
sort: 'purpose',
sortDirection: sort == 'purpose' and sortDirection == 'asc' ? 'desc' : 'asc',
}) }}">
Purpose {{ _self.sortArrow('purpose', sort, sortDirection) }}
</a>
</th>
// ... lines 69 - 78
</tr>
</thead>
// ... lines 81 - 119
</table>
</div>
// ... lines 122 - 132
</section>
</div>
{% endblock %}

¡Uf! Repitamos todo esto para la columna de salida. Pega, cambia purposepor leaveAt, el texto por Departing... luego utiliza leaveAt en los otros dos puntos:

136 lines | templates/main/homepage.html.twig
// ... lines 1 - 27
{% block body %}
<div class="flex">
// ... lines 30 - 36
<section class="flex-1 ml-10">
// ... lines 38 - 55
<div class="bg-gray-800 p-4 rounded">
<table class="w-full text-white">
<thead>
<tr>
// ... lines 60 - 69
<th class="text-left py-2">
<a href="{{ path('app_homepage', {
...app.request.query.all(),
sort: 'leaveAt',
sortDirection: sort == 'leaveAt' and sortDirection == 'asc' ? 'desc' : 'asc',
}) }}">
Departing {{ _self.sortArrow('leaveAt', sort, sortDirection) }}
</a>
</th>
</tr>
</thead>
// ... lines 81 - 119
</table>
</div>
// ... lines 122 - 132
</section>
</div>
{% endblock %}

Así pues, todo un código bastante aburrido, aunque ha costado un poco de trabajo configurarlo. ¿Podríamos tener algunas herramientas en el mundo Symfony para que todo esto fuera más fácil de construir? Probablemente, sería genial que alguien trabajara en ello.

¡El momento de la verdad! Actualiza. Tiene buena pinta. ¡Y funciona de maravilla! Podemos ordenar por cada columna... podemos paginar. El filtrado mantiene nuestra página... y mantiene el parámetro de búsqueda. ¡Es todo lo que quiero! ¡Y todo ocurre mediante Ajax! ¡La vida es buena!

¿El único contratiempo ahora? Ese incómodo desplazamiento cada vez que hacemos algo. Quiero que esto parezca una aplicación independiente que no salta de un lado a otro. Mañana: puliremos esto gracias a Turbo Frames.