version 0.6.0
filter arrays, special-casing inet arrays
upgrade to erlang 27.1.2 and elixir 1.17.3
Pagination library for Elixir.
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>
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>
make run.db
: start db (for tests)make rebuild.elixir
: build dev docker imagemake shell
, then mix deps.get
(then exit
): init elixir dev envmake check
: run linting and tests