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

Back to index