The data table's entire backend contract in one struct.
Flop-shaped on purpose, Flop-free on purpose: anything that can produce
this struct can drive <.data_table>, and anything that can consume it
can execute the query - the in-memory Engine.List for plain lists,
or (Petal Pro) a Flop adapter for Ecto.
%State{
order_by: [{:email, :asc}],
filters: [%{field: :email, op: :contains, value: "d"}],
page: 1,
page_size: 10,
total: 74 # nil = cursor/unknown mode
}from_params/2 and to_params/1 round-trip the struct through URL
params, so URL-as-state (shareable sorts/filters, working back button)
is a one-liner in handle_params rather than a hand-rolled encoding.
Filter operators
Text: :contains, :eq, :starts_with - number: :eq, :neq,
:gt, :lt, :between - select: :in - date: :before, :on,
:after. Engines may support a subset; unknown ops are an engine
concern, not a state concern.
Security
from_params/2 never creates atoms from user input: :fields is a
required whitelist and anything outside it is dropped, ops outside the
known set are dropped, and page/page_size are clamped (:max_page_size,
default 100).
Summary
Functions
Removes every filter, resetting to page 1.
Builds a State from URL/event params.
Replaces the filter for field (or removes it when value is nil/empty), resetting to page 1.
Encodes the state as a flat params map suitable for push_patch
query strings. Defaults (page 1, empty sorts/filters, the default
page size) are omitted so URLs stay clean; total never round-trips -
it is a result, not a request.
Returns the state with field as the primary sort: cycles
asc -> desc -> removed on repeated calls (the header-click grammar),
and always resets to page 1 - a reordered page 7 is meaningless.
Total pages when total is known, else nil (cursor/unknown mode).
Types
@type order() :: {atom(), :asc | :desc}
@type t() :: %PetalComponents.DataTable.State{ filters: [filter()], order_by: [order()], page: pos_integer(), page_size: pos_integer(), total: non_neg_integer() | nil }
Functions
Removes every filter, resetting to page 1.
Builds a State from URL/event params.
Options:
:fields(required) - the whitelist of sortable/filterable fields, as atoms. Params referencing any other field are silently dropped.:page_size- default page size when the params carry none (10).:max_page_size- clamp ceiling for user-supplied sizes (100).
Accepted param shapes (all optional, all strings - what to_params/1
emits and what hand-written URLs naturally produce):
"order_by"-"email"or"email:desc"or"email:desc,name""filters"- a list (or Phoenix-style indexed map) of%{"field" => f, "op" => op, "value" => v}"page","page_size"- integers as strings
Replaces the filter for field (or removes it when value is nil/empty), resetting to page 1.
Encodes the state as a flat params map suitable for push_patch
query strings. Defaults (page 1, empty sorts/filters, the default
page size) are omitted so URLs stay clean; total never round-trips -
it is a result, not a request.
Pass the same :page_size default given to from_params/2 so the
two stay symmetric (an omitted size decodes back to that default).
Returns the state with field as the primary sort: cycles
asc -> desc -> removed on repeated calls (the header-click grammar),
and always resets to page 1 - a reordered page 7 is meaningless.
Total pages when total is known, else nil (cursor/unknown mode).