Zero Dependency Pagination in Phoenix (Part 1)
Ecto is powerful enough that we just don't need to pull in external dependencies to do pagination in Phoenix.
This episode covers making a Pagination module, and using from controllers and contexts. We'll build it for our StatWatch project, but the same strategy works for any Phoenix app. We'll paginate two different views, first a simple index view and then a more complex one where the paginated part is pre-loaded into another schema.
Core.ex
Here's what our Core context module looks like at the end:
defmodule StatWatch.Core do
  @moduledoc """
  The Core context.
  This contains logic for schemas imported from non-web version of StatWatch
  """
  import Ecto.Query, warn: false
  alias StatWatch.{Repo, Pagination, Profile, Stat}
  alias StatWatch.Accounts.User
  @days_per_page 30
  @profiles_per_page 5
  def list_profiles do
    Repo.all(Profile)
  end
  def list_profiles(a, page \\ 1, per_page \\ @profiles_per_page)
  def list_profiles(:paged, page, per_page) do
    Profile
    |> order_by(desc: :name)
    |> Pagination.page(page, per_page: per_page)
  end
  def list_profiles(user = %User{}, page, per_page) do
    Profile
    |> where(user_id: ^user.id)
    |> order_by(desc: :name)
    |> Pagination.page(page, per_page: per_page)
  end
  def get_profile!(id) do
    stat_query = from(s in Stat, order_by: [desc: s.inserted_at])
    Repo.get!(Profile, id)
    |> Repo.preload([:user, stats: stat_query])
  end
  def get_profile_by_name!(name) do
    stat_query = from(s in Stat, order_by: [desc: s.inserted_at])
    Repo.get_by!(Profile, %{name: name})
    |> Repo.preload([:user, stats: stat_query])
  end
  def get_profile_by_name!(name, page, per_page \\ @days_per_page) do
    profile =
      Repo.get_by!(Profile, %{name: name})
      |> Repo.preload([:user])
    stats = page_of_stats(profile.id, page, per_page)
    Map.put(profile, :paginated_stats, stats)
  end
  def create_profile(attrs \\ %{}) do
    %Profile{}
    |> Profile.changeset(attrs)
    |> Repo.insert()
  end
  def update_profile(%Profile{} = profile, attrs) do
    profile
    |> Profile.changeset(attrs)
    |> Repo.update()
  end
  def delete_profile(%Profile{} = profile) do
    Repo.delete(profile)
  end
  def delete_profile_by_name(name) do
    Repo.get_by!(Profile, %{name: name})
    |> Repo.preload(:stats)
    |> Repo.delete()
  end
  def change_profile(%Profile{} = profile) do
    Profile.changeset(profile, %{})
  end
  def page_of_stats(profile, page \\ 1, per_page \\ @days_per_page)
  def page_of_stats(%Profile{} = x, y, z), do: page_of_stats(x.id, y, z)
  def page_of_stats(profile_id, page, per_page) do
    Stat
    |> order_by(desc: :inserted_at)
    |> where(profile_id: ^profile_id)
    |> Pagination.page(page, per_page: per_page)
  end
end
Next episode will add some view helpers to get auto-generated links and listing information.
(Full repo available for premium members)
Learn how to build a pagination links helper to drop into your EEx templates in Part 2
    
2 Comments
Thanks for this screencast! I have a problem with this. If I preload in a query, the subquery for counting the results fails:
I've fixed this with a more costly query:
Do you have a better solution for this?
Thanks,
Adrián
These are relevant: