Feature

Table Pagination with LiveView

Preview

A table with Phoenix LiveView pagination

NOTE I do a manual pagination from a list of 100 products. They are in memory and not read from the database. I could have used a pagination package for Ecto.

    all_products = Products.list_products()

    paginated_products = all_products |> Enum.chunk_every(@page_size)
    products_for_page = paginated_products |> Enum.at(page_number - 1)
    total_entries = all_products |> Enum.count()
    total_pages = paginated_products |> Enum.count()
lib/phoenix_features_web/live/components/table_simple.ex
defmodule PhoenixFeaturesWeb.Components.TableSimple do
  use PhoenixFeaturesWeb, :live_component

  alias PhoenixFeatures.Products

  def update(assigns, socket) do
    page =
      case assigns[:params] do
        %{"page" => page} -> String.to_integer(page)
        _ -> 1
      end

    {:ok,
      socket
      |> assign(assigns)
      |> assign(get_products(page))
    }
  end

  @page_size 10
  def path(socket, page) do
    Routes.demos_show_path(socket, :show, "table_simple", page: page)
  end

  def current_page?(page_number, idx) do
    page_number == idx
  end

  def first_page?(page_number) do
    page_number <= 1
  end

  def last_page?(page_number) do
    page_number >= @page_size
  end

  defp get_products(page_number) do
    all_products = Products.list_products()

    paginated_products = all_products |> Enum.chunk_every(@page_size)
    products_for_page = paginated_products |> Enum.at(page_number - 1)
    total_entries = all_products |> Enum.count()
    total_pages = paginated_products |> Enum.count()

    [
      products: products_for_page,
      page_number: page_number,
      page_size: @page_size,
      total_entries: total_entries,
      total_pages: total_pages
    ]
  end
end
<div id="<%= @id %>">
  <div class="flex items-baseline justify-between">
    <h3 class="ml-4 text-lg font-semibold text-gray-600">
      Products
    </h3>

    <nav class="flex border-t border-gray-200">
      <%= if first_page?(@page_number) do %>
        <%= live_redirect "Prev", to: path(@socket, @page_number), class: "mr-1 pointer-events-none bg-gray-100 inline-block text-sm bg-white p-2 rounded shadow text-gray-600 border border-gray-200" %>
      <% else %>
        <%= live_redirect "Prev", to: path(@socket, @page_number - 1), class: "mr-1 inline-block text-sm bg-white p-2 rounded shadow text-gray-800 border border-gray-200" %>
      <% end %>

      <%= for idx <-  Enum.to_list(1..@total_pages) do %>
        <%= if current_page?(@page_number, idx) do %>
          <%= live_redirect idx, to: path(@socket, idx), class: "mr-1 pointer-events-none bg-gray-100 inline-block text-sm bg-white p-2 rounded shadow text-gray-600 border border-gray-200" %>
        <% else %>
          <%= live_redirect idx, to: path(@socket, idx), class: "mr-1 inline-block text-sm bg-white p-2 rounded shadow text-gray-800 border border-gray-200" %>
        <% end %>
      <% end %>

      <%= if last_page?(@page_number) do %>
        <%= live_redirect "Next", to: path(@socket, @page_number), class: "pointer-events-none bg-gray-100 inline-block text-sm bg-white p-2 rounded shadow text-gray-600 border border-gray-200" %>
      <% else %>
        <%= live_redirect "Next", to: path(@socket, @page_number + 1), class: "inline-block text-sm bg-white p-2 rounded shadow text-gray-800 border border-gray-200" %>
      <% end %>
    </nav>
  </div>

  <table class="w-full mt-4 bg-white border border-gray-200 rounded-lg shadow-lg">
    <thead class="bg-gray-100 rounded-t-lg border-b border-gray-200 text-left">
      <tr>
        <th class="text-xs p-3 text-gray-600">Name</th>
        <th class="text-xs p-3 text-gray-600">Description</th>
        <th class="text-xs p-3 text-gray-600">Price</th>
      </tr>
    </thead>
    <tbody>
      <%= for product <- @products do %>
        <tr id="product-<%= product.id %>" class="border-b border-gray-100">
          <td class="px-2 py-3 text-sm font-semibold text-gray-800"><%= product.name %></td>
          <td class="px-2 py-3 text-sm text-gray-500"><%= product.description %></td>
          <td class="px-2 py-3 text-sm font-semibold text-gray-800"><%= product.price %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>
defmodule PhoenixFeaturesWeb.Components.CalendarSimpleTest do
  use PhoenixFeaturesWeb.ConnCase
  import Phoenix.LiveViewTest

  describe "TableSimple" do
    test "shows the component", %{conn: conn} do
      {:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "table_simple"))

      assert view |> element("#table_simple") |> has_element?()
    end

    test "click the next and prev buttons paginates to the table", %{conn: conn} do
      {:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "table_simple"))

      first_product = view |> element("table tbody tr:first-child td:first-child") |> render()

      assert first_product =~ "<td class="

      {:ok, view, _html} =
        view
        |> element("a", "Next")
        |> render_click()
        |> follow_redirect(conn, Routes.demos_show_path(conn, :show, "table_simple", page: 2))

      refute view |> element("table tbody tr:first-child td:first-child") |> render() == first_product

      {:ok, view, _html} =
        view
        |> element("a", "Prev")
        |> render_click()
        |> follow_redirect(conn, Routes.demos_show_path(conn, :show, "table_simple", page: 1))


      assert view |> element("table tbody tr:first-child td:first-child") |> render() == first_product
    end
  end
end