Data Table
An advanced data table with sorting, pagination, and search functionality.
How to access
Access options
This comes with Petal Pro. Purchase a membership to get access to this package. It can be used with any Phoenix project. Post-expiration, you'll retain access but won't be eligible for updates from newer versions.
Schema setup
Add @derive Flop.Schema to your Ecto schema to declare which fields are sortable and filterable.
defmodule MyApp.Widgets.Widget do
use Ecto.Schema
@derive {
Flop.Schema,
filterable: [:title, :views],
sortable: [:id, :title, :views, :inserted_at]
}
schema "widgets" do
field :title, :string
field :views, :integer
timestamps()
end
end
LiveView wiring
Three callbacks to wire up:
-
mount/3- initial setup (no query needed here) -
handle_params/3- runs the Flop query and assignsitems+meta -
handle_event("update_filters", ...)- patches the URL when a filter changes
defmodule MyAppWeb.WidgetsLive do
use MyAppWeb, :live_view
alias PetalProWeb.DataTable
@data_table_opts [
default_limit: 10,
default_order: %{
order_by: [:inserted_at],
order_directions: [:desc]
}
]
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(params, _url, socket) do
{items, meta} = DataTable.search(MyApp.Widgets.Widget, params, @data_table_opts)
{:noreply, assign(socket, items: items, meta: meta)}
end
@impl true
def handle_event("update_filters", %{"filters" => filter_params}, socket) do
query_params = DataTable.build_filter_params(socket.assigns.meta, filter_params)
{:noreply, push_patch(socket, to: ~p"/widgets?#{query_params}")}
end
end
Sortable columns
Add sortable to any column to make its header clickable for ascending/descending sort. The field must also be in the sortable list of your @derive Flop.Schema.
<:col field={:title} sortable />
<:col field={:views} sortable />
<:col field={:inserted_at} sortable label="Created" />
Filterable columns
Pass a list of filter operators to filterable. A filter input appears in the column header.
If you pass more than one operator, the user can switch between them.
<%!-- Case-insensitive text search --%>
<:col field={:title} filterable={[:ilike]} />
<%!-- Exact match --%>
<:col field={:status} filterable={[:==]} />
<%!-- Multiple operators the user can switch between --%>
<:col field={:views} filterable={[:==, :>=, :<=]} type={:integer} />
Filter operators
| Operator | SQL |
|---|---|
:== |
column = value |
:!= |
column != value |
:ilike |
column ILIKE '%value%' |
:like |
column LIKE '%value%' |
:ilike_and |
column ILIKE '%a%' AND column ILIKE '%b%' |
:ilike_or |
column ILIKE '%a%' OR column ILIKE '%b%' |
:empty |
column IS NULL |
:not_empty |
column IS NOT NULL |
:<= |
column <= value |
:< |
column < value |
:>= |
column >= value |
:> |
column > value |
:in |
column = ANY(values) |
:not_in |
column NOT IN (values) |
:contains |
value = ANY(column) |
Filter input types
The type attr controls which input is rendered for the filter.
<%!-- Number input --%>
<:col field={:views} filterable={[:>=]} type={:integer} />
<%!-- Dropdown --%>
<:col
field={:status}
filterable={[:==]}
type={:select}
options={[{"Active", "active"}, {"Inactive", "inactive"}]}
prompt="All statuses"
/>
<%!-- Checkbox --%>
<:col field={:published} filterable={[:==]} type={:boolean} />
Cell renderers
Use renderer to control how a cell value is displayed.
<%!-- Default - plain string --%>
<:col field={:title} />
<%!-- Checkbox for booleans --%>
<:col field={:published} renderer={:checkbox} />
<%!-- Date with optional strftime format string --%>
<:col field={:inserted_at} renderer={:date} />
<:col field={:inserted_at} renderer={:date} date_format="%d %b %Y" />
<%!-- Datetime --%>
<:col field={:updated_at} renderer={:datetime} />
<%!-- Money (requires the `money` hex package) --%>
<:col field={:price} renderer={:money} currency="USD" />
Custom cell content
Use :let to get the row item and render whatever you want inside the cell.
<:col :let={user} field={:name} label="User" sortable>
<div class="flex items-center gap-2">
<.avatar src={user.avatar_url} size="xs" />
<span>{user.name}</span>
</div>
</:col>
Actions column
Use align_right and omit field for an actions column that doesn’t sort or filter.
<:col :let={post} label="Actions" align_right>
<div class="flex justify-end gap-2">
<.button
size="xs"
variant="outline"
link_type="live_redirect"
label="Edit"
to={~p"/posts/#{post}/edit"}
/>
<.button
size="xs"
color="danger"
variant="outline"
label="Delete"
phx-click="delete"
phx-value-id={post.id}
data-confirm="Are you sure?"
/>
</div>
</:col>
Empty state
The <:if_empty> slot renders when the query returns no results.
<DataTable.data_table meta={@meta} items={@items}>
<:if_empty>No results found</:if_empty>
<:col field={:title} sortable />
</DataTable.data_table>
Join fields
To sort or filter on a field from a joined table, declare it in @derive Flop.Schema
and make sure your base query includes the named join.
# In your schema
@derive {
Flop.Schema,
filterable: [:user_email],
sortable: [:user_email],
join_fields: [user_email: {:user, :email}]
}
# In your LiveView - the query must name the join with `as:`
starting_query =
from(p in Post,
join: u in assoc(p, :user),
as: :user,
preload: [:user]
)
{items, meta} = DataTable.search(starting_query, params, @data_table_opts)
<:col :let={post} field={:user_email} label="Email" sortable filterable={[:ilike]}>
{post.user.email}
</:col>
Attributes reference
# <DataTable.data_table>
attr :meta, Flop.Meta, required: true # from DataTable.search/3 or Flop.validate_and_run/3
attr :items, :list, required: true # the list of records to display
attr :page_size_options, :list # defaults to [10, 20, 50]
attr :class, :string # CSS class on the wrapper div
# <:col>
attr :field, :atom # schema field name
attr :label, :string # column header (defaults to humanised field name)
attr :class, :string # CSS class on each <td>
attr :sortable, :boolean # enable click-to-sort on the header
attr :filterable, :list # filter operators e.g. [:ilike] or [:==, :>=, :<=]
attr :type, :atom # :integer | :float | :boolean | :select
attr :options, :list # [{label, value}] for :select type
attr :prompt, :string # placeholder for :select filter
attr :renderer, :atom # :plaintext | :checkbox | :date | :datetime | :money
attr :date_format, :string # strftime format for :date/:datetime renderer
attr :currency, :string # ISO currency code for :money renderer e.g. "USD"
attr :align_right, :boolean # right-align the column
slot :if_empty # rendered when items is []