README.md

# Pageantry

Pagination library for Elixir.

## Basic Usage

If you have an Ecto schema like the following:

```
defmodule Example.Product do
  use Ecto.Schema

  schema "products" do
    field :product_name
    field :quantity_per_unit
    field :unit_price, :decimal
    field :units_in_stock, :integer
    field :units_on_order, :integer
    field :reorder_level, :integer
    field :discontinued, :boolean
    timestamps()
  end
end
```

You can query a sliced and sorted page of the `products` table with the following `Page.query/4` call:

```
defmodule Example.Context do
  alias Example.Product
  alias Example.Repo
  alias Pageantry.{Field, Page}

  def page_products(page \\ Page.new()) do
    Page.query(page, Repo, Product, products_validation())
  end

  defp products_validation() do
    fields = [
      id: %Field{field: :id, all: false},
      name: %Field{field: :product_name, filter: :like},
      quantity: %Field{field: :quanity_per_unit, sort: false, filter: false},
      price: %Field{field: :unit_price},
      in_stock: %Field{field: :units_in_stock},
      on_order: %Field{field: :units_on_order},
      reorder: %Field{field: :reorder_level},
      created: %Field{field: :inserted_at},
      updated: %Field{field: :updated_at}
    ]

    %Validation{schema: Product, fields: fields, base_sort: [asc: :id]}
  end
end
```

Which you might call from a controller with the pagination input parsed from the request params:

```
defmodule ExampleWeb.ProductController do
  use ExampleWeb, :controller
  alias Example.Context
  alias Pageantry.Page

  def index(conn, params) do
    page =
      Page.new()
      |> Page.parse(params)
      |> Context.page_products()

    render(conn, :index, page: page)
  end
end
```

And render with an HTML table like the following:

```
<table>
  <thead>
    <tr>
      <th><a href="?sort=id">ID</a></th>
      <th><a href="?sort=name">Product Name</a></th>
      <th>Quantity per Unit</th>
      <th><a href="?sort=price">Unit Price</a></th>
    </tr>
  </thead>
  <tbody>
<%= for item <- @page.output.items do %>
    <tr>
      <td><%= item.id %></td>
      <td><%= item.product_name %></td>
      <td><%= item.quantity_per_unit %></td>
      <td><%= item.unit_price %></td>
    </tr>
<% end %>
  </tbody>
</table>

<div class="pagination">
  <a href={"?off=#{@page.input.off - @page.input.max}"}>Prev</a>
  <a href={"?off=#{@page.input.off + @page.input.max}"}>Next</a>
  <span>
    <%= @page.input.off + 1 %> to <%= @page.input.off + @page.input.max %>
    of <%= @page.output.total %>
  </span>
  <form>
    <input name="q">
    <button type="submit">Filter</button>
  </form>
</div>
```

## Join Usage

If a Product can belong to a Category, like the following:

```
defmodule Example.Category do
  use Ecto.Schema
  alias Example.Product

  schema "categories" do
    field :category_name
    field :description
    field :picture, :binary
    has_many :products, Product
  end
end
```

You can add a `query_builder/0` function to the Product schema to define a custom query builder module for it:

```
defmodule Example.Product do
  use Ecto.Schema
  alias Example.Category

  schema "products" do
    field :product_name
    field :quantity_per_unit
    field :unit_price, :decimal
    field :units_in_stock, :integer
    field :units_on_order, :integer
    field :reorder_level, :integer
    field :discontinued, :boolean
    belongs_to :category, Category
    timestamps()
  end

  def query_builder, do: Example.Product.ProductQueryBuilder
end
```

And use the query builder to define 1) the alias to use for the `products` table; and 2) the alias and join expression to use to for the `categories` table:

```
defmodule Example.Product.ProductQueryBuilder do
  import Ecto.Query
  alias Example.{Category, Product}

  def from, do: from(x in Product, as: :products)

  def join(query, Category, qualifier) do
    join(query, qualifier, [{:products, x}], y in assoc(x, :category), as: :categories)
  end
end
```

Then if you define additional validation fields for a Product using its Category relation:

```
defmodule Example.Context do
  import Ecto.Query
  alias Example.Product
  alias Example.Repo
  alias Pageantry.{Field, FieldRelation, Page}

  def page_products(page \\ Page.new()) do
    query = from(p in Product, as: :products, preload: :category)
    Page.query(page, Repo, query, products_validation())
  end

  defp products_validation() do
    fields = [
      id: %Field{field: :id, all: false},
      name: %Field{field: :product_name, filter: :like},
      quantity: %Field{field: :quanity_per_unit, sort: false, filter: false},
      price: %Field{field: :unit_price},
      in_stock: %Field{field: :units_in_stock},
      on_order: %Field{field: :units_on_order},
      reorder: %Field{field: :reorder_level},
      created: %Field{field: :inserted_at},
      updated: %Field{field: :updated_at},
      category: %Field{
        field: :category_name,
        relation: %FieldRelation{association: :category}
      },
      category_description: %Field{
        field: :description,
        relation: %FieldRelation{association: :category},
        filter: :like
      },
    ]

    %Validation{schema: Product, fields: fields, base_sort: [asc: :id]}
  end
end
```

You can sort and filter on the joined Category fields:

```
<form>
  <input name="field" value="category_description" type="hidden">
  <label>
    Category description:
    <input name="q">
  </label>
  <button type="submit">Filter</button>
</form>

<table>
  <thead>
    <tr>
      <th><a href="?sort=id">ID</a></th>
      <th><a href="?sort=name">Product Name</a></th>
      <th>Quantity per Unit</th>
      <th><a href="?sort=price">Unit Price</a></th>
      <th><a href="?sort=category">Category Name</a></th>
      <th>Category Description</th>
    </tr>
  </thead>
  <tbody>
<%= for item <- @page.output.items do %>
    <tr>
      <td><%= item.id %></td>
      <td><%= item.product_name %></td>
      <td><%= item.quantity_per_unit %></td>
      <td><%= item.unit_price %></td>
      <td><%= item.category.category_name %></td>
      <td><%= item.category.description %></td>
    </tr>
<% end %>
  </tbody>
</table>
```

## Development

1. `make run.db`: start db (for tests)
2. `make rebuild.elixir`: build dev docker image
3. `make shell`, then `mix deps.get` (then `exit`): init elixir dev env
4. `make check`: run linting and tests


## Resources

* Source code: https://git.sr.ht/~arx10/elixir-pageantry
* Documentation: https://hexdocs.pm/pageantry


## License

[The MIT License](https://git.sr.ht/~arx10/elixir-pageantry/tree/main/item/LICENSE)