Subscribe to access all episodes. View plans →

#142: Pagination with Phoenix LiveView

Published April 18, 2022

Phoenix 1.6

LiveView 0.17

Scrivener.Ecto 2.7

Follow along with the episode starter on GitHub


Here we have an Elixir application that’s using Phoenix LiveView to display this long list of different albums. Now instead of having them all displayed on the page at once like this, let’s add some simple pagination to limit the number displayed on each page. To help us handle pagination we’ll use Scrivener, specifically the Scrivener.Ecto package.

Now I covered Scrivener back in episode 4, but I thought it would be nice to cover it again within the context of Phoenix LiveView. Alright, let’s open Hex and we’ll grab the config. Then let’s open our application’s Mixfile and we’ll paste in scrivener_ecto.

mix.exs

...
defp deps do
...
{:scrivener_ecto, "~> 2.7"},
...
end
...

With that, we can go to the command line and download it with mix deps.get.

$ mix deps.get
...
New:
  scrivener 2.7.2
  scrivener_ecto 2.7.0

Now that it’s installed, we’ll configure our application to paginate our albums. Let’s open our Repo module and add use Scrivener including the page_size option. Let’s set our page size to 5.

lib/teacher/repo.ex

defmodule Teacher.Repo do
  use Ecto.Repo,
    otp_app: :teacher,
    adapter: Ecto.Adapters.Postgres

  use Scrivener, page_size: 5
end

Now we need a function we can call to retrieve the paginated albums. Let’s open our Recordings context module. And we’ll add a new function called paginate_albums that will take some parameters. The params here will be the params map passed down from the Phoenix LiveView. We’ll pass them into the paginate function Scrivener gives us. We’ll call Repo.paginate which expects an Ecto query as the first argument, because we just want to return all of our albums, we’ll pass in Album, and then our params.

lib/teacher/recordings.ex

...

def paginate_albums(params) do
  Repo.paginate(Album, params)
end

...

Alright, now let’s open the LiveView that handles displaying these albums - the AlbumLive.Index and here in the mount callback, we’re calling Recordings.list_albums() to return all the albums. Let’s update that to call Recordings.paginate_albums passing in the params, which we’ll need to stop ignoring above by removing the leading underscore.

Now instead of returning all albums, this will instead return a Scrivener.Page struct that will contain our paginated albums and pagination data like the page_number, page_size and the total number of entries, so let’s rename our albums variable to page and then we’ll update our render function.

render(conn, “index.html”, page: page)

lib/teacher_web/live/album_live/index.ex

...

@impl true
def mount(params, _session, socket) do
  page = Recordings.paginate_albums(params)
  {:ok, assign(socket, :page, page)}
end

...

Now we just need to go to the corresponding index.html.heex template and we’ll access our albums from @page.entries so let’s update that. Because our albums are now paginated let’s add a simple pagination navigation to the page here.

If we’re on the first page we don’t want our link to show so let’s only display it if our page number is greater than 1. To create our link we’ll use the live_patch helper. And since we’re patching our current LiveView let’s include the page number in the query string subtracting 1 from the current page_number so we go back a page. And unless we’re on the last page, let’s add a link to go to the next page, which we can do by using the same path only adding 1 to the page_number. I’ll fix this class name here and then call to get the page number.

Template path: lib/teacher_web/templates/album/index.html.heex

...
<%= for album <- @page.entries do %>
  ...
<% end %>
...
<div class="pagination">
  <%= if @page.page_number > 1 do %>
    <%= live_patch "<< Prev Page",
      to: Routes.album_index_path(@socket, :index, page: @page.page_number - 1),
      class: "pagination-link" %>
  <% end %>

  <%= if @page.page_number < @page.total_pages do %>
    <%= live_patch "Next Page >>",
      to: Routes.album_index_path(@socket, :index, page: @page.page_number + 1),
      class: "pagination-link" %>
  <% end %>
</div>

Alright now because we’re using live_patch here we need to make one change to our AlbumLive.Index live view and the handle_params callback, which will be invoked when our pagination links are clicked. It will take the params, which we’ll want for pagination, the URI which we can ignore, and the socket. Inside the function, we need to do two things. Grab the paginated albums and then update the socket, which is exactly what we’re doing in the mount callback so let’s go ahead and grab that same Recordings.paginate_albums function, and paste it in. Then we need to return a noreply tuple with the updated socket.

Now looking at our code here, we’re querying the database twice to get the same set of albums with Recordings.paginate_albums. Here in handle_params and again in mount. This is because when our LiveView is invoked, it will first execute the code in mount and then handle_params so we have an extra call to the database that we don’t need. In fact, if we take away Recordings.paginate_albums from mount we don’t want to include that callback here, so let’s go ahead and remove it. We’ll just let handle_params load our albums. Great now when users click to the next page, handle_params should update the LiveView with the albums for that page.

lib/teacher_web/live/album_live/index.ex

...

@impl true
def handle_params(params, _uri, socket) do
  page = Recordings.paginate_albums(params)
  {:noreply, assign(socket, :page, page)}
end

...

Now let’s go to the command line and start up our server.

$ mix phx.server
...

When we go back to the album page and scroll to the bottom - great we see our pagination is loading the albums for the different pages.

Our pagination for our Phoenix LiveView is working.

© 2024 HEXMONSTER LLC