Components Data Table

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

Petal Pro

Data Table

An advanced data table with sorting, pagination, and search functionality.
Id
Title
Views
37
Voluptatibus dignissimos qui quasi et rerum nemo eos ad minima?
10
12
Ab blanditiis ut qui error dolor.
23
30
Pariatur sequi dignissimos voluptate molestiae dignissimos animi sequi.
58
34
Sint dolorum aliquid temporibus et dolorem nobis repellat rerum quia?
66
19
Vero voluptates non laboriosam laborum dolores!
66
87
Deserunt explicabo officia ut ut ut dolore reprehenderit ut rerum.
73
59
Molestiae recusandae doloribus praesentium qui est facere sed.
74
58
Repellendus facere deleniti iusto sunt libero dolor itaque?
86
10
Consequuntur harum illum quaerat aut.
87
67
Qui quia at est nam ratione quod.
87
43
Neque nobis aut delectus quibusdam quis et?
96
91
Porro dicta in ea ad in.
115
99
Iste omnis asperiores culpa et culpa?
134
62
Eum sunt magnam laboriosam iure molestiae illum mollitia.
137
4
Eligendi similique quisquam eveniet repellat voluptatem incidunt rerum doloribus quas?
144
79
Quis voluptatibus id animi et eius non!
145
9
Repudiandae alias culpa voluptates assumenda et illum.
147
28
Harum maxime consequatur sit ea eos magnam hic enim iste?
152
45
Praesentium odio voluptatem molestias consequatur.
157
25
In ut iste consequatur animi voluptatibus dolor non.
169
27
Non vel suscipit incidunt itaque voluptas ipsum et officia consequuntur!
182
39
Quaerat eligendi reprehenderit et quo quidem unde fuga enim dolores.
182
88
Nam beatae beatae numquam ex dolores omnis cumque minima.
188
90
Ut eveniet animi aperiam molestiae.
193
70
Sapiente velit magnam modi quis repudiandae quas sunt!
206
85
Sed eligendi debitis iste ipsam nam qui quidem sed.
213
29
Dolorem laboriosam et officia et dolorum numquam dignissimos voluptas.
229
38
Impedit et sint quia quis et quas consequatur.
233
61
Quo inventore unde recusandae qui ut voluptates quasi et.
236
89
Ullam eaque fugit et qui adipisci ea soluta impedit quis.
244
11
Odio dolor animi atque magni nisi et id voluptatibus illo.
252
17
Recusandae voluptatum alias dolorem consequatur.
297
76
Voluptas nulla quisquam provident ut eius.
300
15
Rerum voluptas minus in nulla atque dolorem quas.
301
31
Omnis illo nesciunt nobis magni ipsam iusto commodi odit quibusdam.
308
41
Consequatur repudiandae sint voluptas reprehenderit.
309
66
Laborum cum vel ea tempora est expedita architecto.
309
42
Velit vero quia itaque laboriosam facere ut voluptas in!
328
71
Aut ipsa soluta modi occaecati ea pariatur fuga?
340
64
Quasi molestias repellat tempora similique optio aut assumenda rerum!
343
54
Eum eum aut laudantium est eius eligendi.
358
73
Voluptas minus laborum rerum debitis?
361
74
Cumque doloremque et corporis quia rem esse eum voluptas.
392
68
Quaerat praesentium dolor distinctio illo quibusdam non temporibus tempora!
392
22
Dolorum nobis beatae quaerat et facere deserunt reprehenderit?
394
49
Fugit officiis consequatur veniam quod et aut eligendi.
433
7
Est non voluptatem asperiores quos cupiditate!
434
86
Reiciendis libero omnis provident exercitationem et?
443
14
Consequatur dolores tempora repudiandae sed dolorum dolor.
492
60
Modi expedita pariatur facilis ratione id.
494
Showing 1-50 of 101 rows
Rows per page:
10 20
50

How to access

Membership
$299
-> 
See plans

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.

elixir
          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 assigns items + meta
  • handle_event("update_filters", ...) - patches the URL when a filter changes
elixir
          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.

heex
          <: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.

heex
          <%!-- 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.

heex
          <%!-- 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.

heex
          <%!-- 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.

heex
          <: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.

heex
          <: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.

heex
          <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.

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

        
heex
          <:col :let={post} field={:user_email} label="Email" sortable filterable={[:ilike]}>
  {post.user.email}
</:col>

        

Attributes reference

elixir
          # <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 []