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
56
Animi est qui soluta non aut ipsam blanditiis.
1000
52
Nihil sit rerum alias qui exercitationem maxime.
997
33
Veniam quia accusantium quibusdam tenetur qui.
980
98
Id nostrum vel sed maxime impedit libero.
979
65
Officia accusantium minus consequatur sint sint repudiandae.
958
84
Aut saepe impedit officia deleniti porro corrupti.
950
24
Est culpa omnis qui laborum ea voluptatibus.
942
57
Quia aut est saepe a nam voluptatem.
939
75
Dolore ratione veritatis itaque culpa consectetur sit ipsam!
934
35
Quo quibusdam aut ipsam fuga debitis!
926
96
Et recusandae quibusdam dicta aliquid aut alias omnis.
925
77
Ex fugiat enim asperiores inventore ducimus autem officia sequi repudiandae!
925
48
In animi sunt eveniet et rem aut?
915
3
Ex fugiat accusantium voluptate voluptatem reprehenderit.
914
40
Hic rem deleniti voluptatibus minus voluptas ipsam accusamus placeat tenetur!
914
55
Dicta dignissimos architecto at ipsam ipsam placeat!
895
100
Voluptas earum iusto et reiciendis cum consequatur quisquam at.
871
78
Labore nemo autem facere distinctio neque repellat.
862
5
Natus excepturi ipsum in tempore nesciunt!
833
6
Quidem est minima autem maxime nostrum.
826
47
Sequi magnam repellat dolorem eius ducimus ex delectus.
807
23
Ducimus molestiae soluta atque illum beatae aut dolor?
805
16
Quisquam dolorem suscipit dolorem in error laborum eaque occaecati asperiores.
798
20
Ea illo tempora tempora voluptate voluptatem.
791
51
Quam doloremque error at minus.
773
36
Nesciunt at ipsum sed aliquam hic ducimus est aut deserunt?
766
46
Deleniti ex eos iste distinctio.
762
92
Laudantium commodi perferendis ipsam iure vitae nobis eveniet quibusdam quam.
762
44
Aut veritatis qui mollitia quidem aspernatur?
760
72
Ex iste ipsum maxime beatae.
760
93
Libero omnis itaque ut voluptatem dolor.
755
82
Enim rerum sunt laboriosam hic rerum qui.
746
21
Aut similique eos et quidem quod voluptate!
744
8
Qui quis veritatis tenetur asperiores modi est error id aut.
742
94
Asperiores enim a similique velit molestias.
735
81
Voluptatum minima in dolores eos doloremque.
732
13
Molestias tempora excepturi commodi molestias suscipit qui quo.
726
63
Beatae ipsa rerum dolores fugit.
674
83
Voluptatibus tempora vero nostrum mollitia id vel qui praesentium.
662
32
Autem qui nihil harum rerum velit aliquid.
655
50
Sequi nemo non qui omnis nihil eum sapiente autem qui.
653
26
Incidunt dicta qui officiis quos cumque voluptatem reprehenderit.
635
53
Sit quibusdam hic id sed veniam.
617
97
Blanditiis fuga rerum totam quod repudiandae sint qui.
593
18
Cupiditate fuga quisquam minus aperiam adipisci quisquam est maiores molestias!
575
80
Sit quis ipsum ad vero eum est consectetur.
559
95
Sapiente accusamus necessitatibus fugit aut?
541
2
Sit dolor expedita beatae ut laboriosam odit ducimus.
530
1
Non commodi debitis provident sit.
524
69
Dicta corporis deleniti sit necessitatibus velit doloremque.
519
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 []