# 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)