La pagination est le quatrième chapitre de l’Ouvrage de Peter Ullrich sur la construction de Table avec la vue en mode liste dans Phoenix et LiveView.
Nous avons déjà traité :
Nous allons maintenant voir la Pagination de la liste avec LiveView.
Préparation du poste de travail
Commençons par vérifier les outils installés sur notre poste de travail Windows :
- cd C:\CarbonX1\Phoenix\Public\meow
- code .
Quelles sont les versions des outils utilisés dans cet article :
- elixir -v ‘version d’elixir
- mix local.hex ‘mise à jour des outils hex
- mix archive.install hex phx_new ‘mise à jour de phoenix
- psql -V ‘version de postgres
C:\CarbonX1\Phoenix\Public\meow>elixir -v Erlang/OTP 26 [erts-14.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] Elixir 1.15.5 (compiled with Erlang/OTP 26) C:\CarbonX1\Phoenix\Public\meow>mix local.hex Found existing entry: c:/Users/broussel/.mix/archives/hex-2.0.6 Are you sure you want to replace it with "https://builds.hex.pm/installs/1.14.0/hex-2.0.6.ez"? [Yn] n C:\CarbonX1\Phoenix\Public\meow>mix archive.install hex phx_new Resolving Hex dependencies... Resolution completed in 0.111s New: phx_new 1.7.10 * Getting phx_new (Hex package) All dependencies are up to date Compiling 11 files (.ex) Generated phx_new app Generated archive "phx_new-1.7.10.ez" with MIX_ENV=prod Found existing entry: c:/Users/broussel/.mix/archives/phx_new-1.7.10 Are you sure you want to replace it with "phx_new-1.7.10.ez"? [Yn] n C:\CarbonX1\Phoenix\Public\meow>psql -V psql (PostgreSQL) 16.1 C:\CarbonX1\Phoenix\Public\meow>
Vérifions la présence de la base de données du projet :
- psql –version ‘connaitre la version de psql
- psql -U postgres ‘pour se connecter en tant qu’utilisateur username: postgres
- \c ‘pour vérifier la connexion
- \l ‘liste des base de données
- \q ‘quitter psql
C:\CarbonX1\Phoenix\Public\meow>psql -U postgres Mot de passe pour l'utilisateur postgres : psql (16.1) Attention : l'encodage console (850) diffère de l'encodage Windows (1252). Les caractères 8 bits peuvent ne pas fonctionner correctement. Voir la section « Notes aux utilisateurs de Windows » de la page référence de psql pour les détails. Saisissez « help » pour l'aide. postgres=# \c Vous êtes maintenant connecté à la base de données « postgres » en tant qu'utilisateur « postgres ». postgres=# \l Liste des bases de donnÚes Nom | PropriÚtaire | Encodage | Fournisseur de locale | Collationnement | Type caract. | Locale ICU | RÞgles ICU : | Droits d'accÞs --------------------------+--------------+----------+-----------------------+--------------------+--------------------+------------+--------------+----------------------- meow_dev | postgres | UTF8 | libc | French_France.1252 | French_France.1252 | | | postgres | postgres | UTF8 | libc | French_France.1252 | French_France.1252 | | | template0 | postgres | UTF8 | libc | French_France.1252 | French_France.1252 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | libc | French_France.1252 | French_France.1252 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres (7 lignes) postgres=# \q C:\CarbonX1\Phoenix\Public\meow>
Testons l’application dans son état initial :
- cd C:\CarbonX1\Phoenix\Public\meow
- mix phx.server
- http://localhost:4000/
L’application fonctionne telle que défini dans notre sernier article, filtrer la table avec LiveView.
La pagination de la liste LiveView en mode bloc
§Lorsque le volume de données est imposant, il est préférable de n’afficher qu’une partie de la liste. Nous pouvons afficher des blocs avec par exemple un vingtaine d’articles par page. La suite de la liste est affichée sur action de l’utilisateur avec des boutons. Cela permet de simplifier le chargement et surtout d’afficher les premiers résultats sans faire attendre l’utilisateur.
Nous avons deux paramètres pour lma pagination de la liste LiveView par bloc :
- le numero de la page ou du bloc
- le nombre d’item à placer dans la page
Comme pour les tries et les filtres, nous allons passer ces deux valeurs dans l’url.
Modification du context du projet
Nous allons suivre la même méthode que pour le projet de filtre. La première étape consiste à modifier le Contexte du projet dans meerkats.ex, en ajoutant la pagination.
MEOW/lib/meow/meerkats.ex :
Nous ajoutons une fonction list_meerkats_with_total_count (ligne 17 à 29) et la fonction associée, paginate.
defmodule Meow.Meerkats do @moduledoc """ The Meerkats context. """ import Ecto.Query, warn: false alias Meow.Repo alias Meow.Meerkats.Meerkat def list_meerkats(opts) do from(m in Meerkat) |> filter(opts) |> sort(opts) |> Repo.all() end def list_meerkats_with_total_count(opts) do query = from(m in Meerkat) |> filter(opts) total_count = Repo.aggregate(query, :count) result= query |> sort(opts) |> paginate(opts) |> Repo.all() %{meerkats: result, total_count: total_count} end defp sort(query, %{sort_by: sort_by, sort_dir: sort_dir}) when sort_by in [:id, :name] and sort_dir in [:asc, :desc] do order_by(query, {^sort_dir, ^sort_by}) end defp sort(query, _opts), do: query defp filter(query, opts) do query |> filter_by_id(opts) |> filter_by_name(opts) end defp filter_by_id(query, %{id: id}) when is_integer(id) do where(query, id: ^id) end defp filter_by_id(query, _opts), do: query defp filter_by_name(query, %{name: name}) when is_binary(name) and name != "" do query_string = "%#{name}%" where(query, [m], ilike(m.name, ^query_string)) end defp filter_by_name(query, _opts), do: query defp paginate(query, %{page: page, page_size: page_size}) when is_integer(page) and is_integer(page_size) do offset = max(page - 1, 0) * page_size # début de la liste query |> limit(^page_size) |> offset(^offset) end defp paginate(query, _opts), do: query end
Le principe est le suivant :
- nous effectuons la recherche sur la base avec le trie et le filtre
- nous prenons le nombre d’élements retourné dans la liste
- dans paginate nous reduisons la liste en prenant le nombre d’element de la page page_size en commençant par le premier element de cette page avec offset
- La fonction list_meerkats_with_total_count donne la liste des éléments de la page avec le nombre total d’élements de la requête total_count.
Comment fonctionne la pagination de la liste avec LiveView
Dans la fonction paginate, nous avons deux fonctions Ecto utilisées :
- limit/2 ‘donne le nombre maximum d’elements à retourner
- offset/2 ‘donne l’identifiant du premier element de la liste à retrourner
Le premier élément retourné sera celui qui suit la référence fourni par offset.
Un LiveComponent pour la pagination de la liste Liveview
Nous créons le pagination_component.
MEOW/lib/meow_web/live/pagination_component.ex :
defmodule MeowWeb.MeerkatLive.PaginationComponent do use MeowWeb, :live_component alias MeowWeb.Forms.PaginationForm def render(assigns) do ~H""" <div> <div> <%= for {page_number, current_page?} <- pages(@pagination) do %> <div phx-click="show_page" phx-value-page={page_number} phx-target={@myself} class={if current_page?, do: "active"} > <%= page_number %> </div> <% end %> </div> <div> <.form let={f} for={:page_size} phx-change= "set_page_size" phx-target= {@myself} > <%= select f, :page_size, [10, 20, 50, 100], selected: @pagination.page_size %> </.form> </div> </div> """ end end
Dans cette partie de code, nous avons deux <div> :
- une liste de boutons avec les numéros de pages
- un formulaire permettant de choisir le nombre d’item à afficher par page
Dans la liste des boutons avec les numéros de page, nous utilisons une fonction nous donnant le nombre de pages à considérer dans notre boucle for.
Chaque bouton porte en texte le numéro de la page page_number et actionne une fonction phx-click= »show_page » contenant le numéro de la page dans phx-value-page={page_number}. Cet évènement est à destination du composant lui-même avec phx-target={@myself}.
Le choix du nombre d’item par page est défini dans une liste dont les valeurs sont prédéfines : 10, 20, 50, 100.
Nous poursuivons avec la création de la fonction page qui donne la liste des numéros de page aec un booleen oui/non pour savoir si ce numéro de page est la page courante.
Nous ajoutons aussi les actions handle_event pour show_page et set_page_size déclenchées par les boutons définis dans render en ligne 11 et 22.
def pages(%{page_size: page_size, page: current_page, total_count: total_count}) do page_count = ceil(total_count / page_size) for page_number <- 1..page_count//1 do current_page? = page_number == current_page {page_number, current_page?} end end def handle_event("show_page", params, socket) do parse_param(params, socket) end def handle_event("set_page_size", %{"page_size" => params}, socket) do parse_param(params, socket) end defp parse_param(params, socket) do %{pagination: pagination} = socket.assigns case PaginationForm.parse(params, pagination) do {:ok, opts} -> send(self(), {:update, opts}) {:noreply, socket} {:error, _changeset} -> {:noreply, socket} end end
Chacune des deux actions show_page et set_page_size font appel à la fonction parse_param pour modifier l’url et forcer la mise à jour de la page.
Nous utilisons le changeset shemaless c’est à dire sans correspondance avec une table de base de données pour contrôler les valeurs.
MEOW/lib/meow_web/forms/pagination_form.ex :
defmodule MeowWeb.Forms.PaginationForm do import Ecto.Changeset @fields %{ page: :integer, page_size: :integer, total_count: :integer } @default_values %{ page: 1, page_size: 20, total_count: 0 } def parse(params, values \\ @default_values) do {values, @fields} |> cast(params, Map.keys(@fields)) |> validate_number(:page, greater_than: 0) |> validate_number(:page_size, greater_than: 0) |> validate_number(:total_count, greater_than_or_equal_to: 0) |> apply_action(:insert) end def default_values(overrides \\ %{}) do Map.merge(@default_values, overrides) end end
Le changeset est défini par les champs fields, et les valeures par defaut default_values. Nous avons la fonction parse qui contrôle la validité des donées avec l’ensemble des validate_number.
Utilisation du composant de pagination dans LiveView
Le composant PaginationComponent doit être intégré à la page MeerkatLive. Nous modifions le fichier meerkat_live.html.heex afin d’ajouter la pagination construite avec le PaginationComponent en bas de la page.
MEOW/lib/meow_web/live/meerkat_live.html.heex :
<div> <.live_component module={MeowWeb.MeerkatLive.FilterComponent} id="filter" filter={@filter} /> <div id="table-container"> <table> <thead> <tr> <th> <.live_component module={MeowWeb.MeerkatLive.SortingComponent} id={"sorting-id"} key={:id} sorting={@sorting} /> </th> <th> <.live_component module={MeowWeb.MeerkatLive.SortingComponent} id={"sorting-name"} key={:name} sorting={@sorting} /> </th> </tr> </thead> <tbody> <%= if assigns[:error_message] do %> <tr> <td colspan="6"><%= @error_message %></td> </tr> <% else %> <%= for meerkat <- @meerkats do %> <tr data-test-id={meerkat.id}> <td><%= meerkat.id %></td> <td><%= meerkat.name %></td> </tr> <% end %> <% end %> </tbody> </table> </div> <.live_component module={MeowWeb.MeerkatLive.PaginationComponent} id="pagination" pagination={@pagination} /> </div>
Pour que la valeur de @pagination existe, nous devons l’ajouter dans le code de meerkat_live.ex
MEOW/lib/meow_web/live/meerkat_live.ex :
defmodule MeowWeb.MeerkatLive do use MeowWeb, :live_view alias Meow.Meerkats # Add this alias: alias MeowWeb.Forms.SortingForm # Ajoute cet alias chapitre 3 alias MeowWeb.Forms.FilterForm # Ajoute cet alias chapitre 4 alias MeowWeb.Forms.PaginationForm def mount(_params, _session, socket), do: {:ok, socket} # Update handle_params/3 like this: def handle_params(params, _url, socket) do socket = socket |> parse_params(params) |> assign_meerkats() {:noreply, socket} end # Modifier cette fonction chapitre 3 def handle_info({:update, opts}, socket) do params = merge_and_sanitize_params(socket, opts) path = Routes.live_path(socket, __MODULE__, params) {:noreply, push_patch(socket, to: path, replace: true)} end # Modifier cette fonction chapitre 3: defp parse_params(socket, params) do with {:ok, sorting_opts} <- SortingForm.parse(params), {:ok, filter_opts} <- FilterForm.parse(params) do {:ok, pagination_opts} <- PaginationForm.parse(params) do socket |> assign_filter(filter_opts) |> assign_sorting(sorting_opts) |> assign_pagination(pagination_opts) else _error -> socket |> assign_filter() |> assign_sorting() |> assign_pagination() end end # Add this function: defp assign_sorting(socket, overrides \\ %{}) do opts = Map.merge(SortingForm.default_values(), overrides) assign(socket, :sorting, opts) end # Ajoute cette fonction Chapitre 3 : defp assign_filter(socket, overrides \\ %{}) do assign(socket, :filter, FilterForm.default_values(overrides)) end # Ajouter cette fonction chapitre 4 defp assign_pagination(socket, overrides \\ %{}) do params = merge_and_sanitize_params(socket) assign(socket, :pagination, PaginationForm.default_values(overrides)) end # Modifier cette fonction chapitre 3 defp assign_meerkats(socket) do params = merge_and_sanitize_params(socket) assign(socket, :meerkats, Meerkats.list_meerkats(params)) end # Ajoute cette fonction Chapitre 3 : defp merge_and_sanitize_params(socket, overrides \\ %{}) do %{sorting: sorting, filter: filter} = socket.assigns %{} |> Map.merge(sorting) |> Map.merge(filter) |> Map.merge(overrides) |> Enum.reject(fn {_key, value} -> is_nil(value) end) |> Map.new() end end
Nous avons réalisé ci-dessus les modifications suivantes :
- ajout de l’alias PaginationForm (ligne 12),
- nous modifions parse_params pour ajouter la pagination (ligne 36, 40 et 46),
- nous ajoutons assign_pagination (ligne 62).
Nous allons maintenant modifier le code de la fonction assign_meerkats(socket) pour prendre en compte la valeur total_count passée en paramètre grâce à la fonction Meerkats.list_meerkats_with_total_count qui sera utilisé à la place de la fonction Meerkats.list_meerkats utilisée jusqu’à présent (ligne 5 ci-dessous)
MEOW/lib/meow_web/live/meerkat_live.ex (ligne 68 à 71) :
# Modifier cette fonction chapitre 4 defp assign_meerkats(socket) do params = merge_and_sanitize_params(socket) %{meerkats: meerkats, total_count: total_count} = Meerkats.list_meerkats_with_total_count(params) socket |> assign(:meerkats, meerkats) |> assign_total_count(total_count) end
Nous modifions la fonction merge_and_sanitize_params pour ajouter la pagination dans les paramètres :
# Ajoute cette fonction Chapitre 3 modifier chapitre 4: defp merge_and_sanitize_params(socket, overrides \\ %{}) do %{sorting: sorting, filter: filter, pagination: pagination} = socket.assigns overrides = maybe_reset_pagination(overrides) %{} |> Map.merge(sorting) |> Map.merge(filter) |> Map.merge(pagination) |> Map.merge(overrides) |> Map.drop([:total_count]]) |> Enum.reject(fn {_key, value} -> is_nil(value) end) |> Map.new() end
Et nous ajoutrons les 2 fonctions assign_total_count et maybe_reset_pagination que nous venons d’utiliser dans assign_meerkats (ligne 9) et merge_and_sanitize_params (ligne 4).
# Ajoute cette fonction Chapitre 4 : defp assign_total_count(socket, total_count) do update(socket, :pagination, fn pagination -> %{ pagination | total_count: total_count } end) end # Ajoute cette fonction Chapitre 4 : defp maybe_reset_pagination(overrides) do if FilterForm.contains_filter_values?(overrides) do Map.put(overrides, :page, 1) else overrides end end
Visualisation de la page avec pagination de la liste LiveView
Nous pouvons maintenant consulter le site nouvellement modifié avec la pagination :
- cd C:\CarbonX1\Phoenix\Public\meow
- mix phx.server
- http://localhost:4000/
Nous voyons bien la page avec les boutons de navigation dans le bas de la page.
Nous voyons bien les boutons de pagination dans le bas, avec le choix sur le nombre d’item à présenter dans la liste. L’url donne les paramètres :
- de filtre (« cat »),
- de trie avec sort_by (« id ») et sort_dir (« asc »)
- et de pagination page (« 2 ») et page_size (« 10 »)
La mise en page peut être améliorée avec du CSS. Peter Ullrich met à disposition le css sur Github.