Subscribe to access all episodes. View plans →
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.