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
101
Aut sapiente in rem vitae tempora voluptas ducimus?
982
100
In laborum consequatur sint incidunt sit et voluptatem voluptas et.
683
99
Delectus praesentium molestias voluptates in non magni ut commodi!
973
98
Quos voluptate voluptas iure beatae fugit?
342
97
Qui ut molestiae voluptates itaque commodi repellat dolores!
929
96
Voluptate magnam fugit illum illum quasi cupiditate.
768
95
Saepe quas distinctio et rerum sed est.
895
94
Repellendus neque non optio autem libero et quidem a molestias.
353
93
Aliquam et velit adipisci iusto.
286
92
Autem praesentium quo rem unde!
224
91
Quos sequi nostrum est quisquam numquam magnam.
390
90
Id itaque voluptas eveniet beatae est nemo!
750
89
Asperiores magnam quae iure inventore in rerum.
601
88
Consequatur laboriosam cumque dolores adipisci ut quo enim ut nostrum.
74
87
Quae alias minima excepturi provident.
184
86
Dolorum et molestiae qui officiis!
214
85
Recusandae error ut sit ipsum ea ipsum!
668
84
Et eum eos enim aut nostrum?
271
83
Accusamus odit perspiciatis rerum ut praesentium.
853
82
Et vel eligendi officia et deserunt?
863
81
Blanditiis nulla enim illum hic dicta necessitatibus nobis ut?
408
80
Cumque ullam ex natus reiciendis!
365
79
Animi nostrum cum vitae totam consequatur nisi mollitia!
696
78
Inventore fuga sequi modi neque.
413
77
Corrupti et repudiandae totam tempora non at culpa.
285
76
Ab repellendus ducimus repudiandae ipsa molestiae temporibus voluptates dignissimos?
236
75
Aut ut fuga et esse?
588
74
Hic possimus est ut hic id.
250
73
Earum iste nesciunt velit et!
911
72
Qui ad magni quia voluptas ratione voluptatem facere expedita.
156
71
Accusantium saepe et reiciendis voluptatem.
259
70
Ut quos a eos placeat.
769
69
Quia voluptatem maiores laudantium minima nihil aut illo id.
783
68
Neque et dolorum ut sapiente voluptas?
927
67
Dolore est amet consectetur inventore perspiciatis aut hic.
33
66
Culpa autem et consequatur ratione provident vel facilis?
883
65
Incidunt ratione reprehenderit est labore rerum beatae molestiae ab?
86
64
Quae eveniet voluptatem et et!
758
63
Eum eveniet quisquam autem dolor aliquam ut.
46
62
Aliquid rerum hic necessitatibus reprehenderit.
73
61
Aut omnis eum ut optio!
295
60
Qui rerum temporibus quaerat neque nostrum.
264
59
Qui praesentium debitis eum illo optio.
267
58
Eos eveniet corrupti sapiente qui eum.
652
57
Rerum et debitis cumque nisi tempore!
361
56
Non aut ad ipsam earum et minima porro.
800
55
Aut accusamus vel voluptas sint.
214
54
Ducimus dolorum dolor aut consequatur aut nisi!
191
53
Sit aut labore necessitatibus est.
728
52
Repellat assumenda et excepturi itaque magnam veritatis laudantium et animi.
964
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 []