Pagination de la liste avec LiveView

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.

Pagination de la liste d'une table LiveView#1-Page avec le filtre et la pagination de la liste dans la table LiveView
Pagination de la liste d’une table LiveView#1-Page avec le filtre et la pagination de la liste dans la table LiveView

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.

Si vous avez aimé l'article vous êtes libre de le partager :-)

Laisser un commentaire