← All series

Series contents

Log in to track your progress

  1. 1. A worker to log stats from a YouTube’s API Episode
  2. 2. Querying Alexa Site API and parsing XML Episode
  3. 3. Adding Ecto and a Postgres database to StatWatch Episode
  4. 4. Putting an Elixir OTP app in our Elixir OTP app Episode
  5. 5. Launching StatWatch.me (side project) Episode
  6. 6. Planning a Phoenix web app for StatWatch Episode
  7. 7. From backend OTP app to Phoenix app Episode
  8. 8. Adding sessions and logins to StatWatch Episode
  9. 9. Password hashing and Ecto associations for StatWatch Episode
  10. 10. Permalinks and an admin auth plug for StatWatch Episode
  11. 11. Creating Ecto associations between profiles and stats Episode
  12. 12. Zero Dependency Pagination in Phoenix (Part 1) Episode
  13. 13. Zero Dependency Pagination in Phoenix (Part 2) Episode

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

Open standalone page ↗

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:

   ** (Ecto.QueryError) cannot preload associations in subquery in query:

I've fixed this with a more costly query:

    count =
      query
      |> Ui.Repo.aggregate(:count, :id)

Do you have a better solution for this?

Thanks,

Adrián

- Ribao · 5 years ago