Corex.DataTable (Corex v0.1.0-rc.1)

View Source

Phoenix table component for tabular data with column slots, optional row actions, sorting, and row selection.

Supports in-memory lists, LiveView stream rows (phx-update="stream"), and Ecto-backed pages paired with Corex.Pagination. See data_table/1 for anatomy and patterns (basic, actions, streaming, sortable, selectable, with database).

Helpers: Corex.DataTable.Sort, Corex.DataTable.Selection.

Summary

Components

Renders a table with data.

Components

data_table(assigns)

Renders a table with data.

Anatomy

Basic

<.data_table id="basic-table" class="data-table" rows={@list_rows}>
  <:col :let={row} label="ID">{row.id}</:col>
  <:col :let={row} label="Name">{row.name}</:col>
  <:col :let={row} label="Role">{row.role}</:col>
  <:col :let={row} label="Email">{row.email}</:col>
</.data_table>

Actions

Use the :action slot to add actions for each row, like Edit and Delete buttons.

<.data_table id="basic-table" class="data-table" rows={@list_rows}>
  <:col :let={row} label="ID">{row.id}</:col>
  <:col :let={row} label="Name">{row.name}</:col>
  <:col :let={row} label="Role">{row.role}</:col>
  <:col :let={row} label="Email">{row.email}</:col>
  <:action :let={row}>
    <.action phx-click="edit" phx-value-id={row.id}>Edit</.action>
    <.action phx-click="delete" phx-value-id={row.id}>Delete</.action>
  </:action>
</.data_table>

Streaming

Pass the stream to rows. Column slot receives {id, item}. Items need an :id field (or use stream_configure/3 with :dom_id). Add rows with stream_insert/3.

# mount
socket |> stream(:items, []) |> assign(:next_id, 1)
<.data_table id="my-table" class="data-table" rows={@streams.items}>
  <:col :let={{_id, item}} label="Name">{item.name}</:col>
</.data_table>

Add a row: stream_insert(socket, :items, %{id: id, name: "New"}) from handle_event or handle_info.

With the :empty slot, the empty row stays in the DOM and is hidden by the data-table stylesheet whenever the tbody has data rows (same idea as stream empty state siblings; avoids counting stream items on the server).

Row click

Pass row_click to handle clicks on data cells (not the action column). Use JS.push/2 to update LiveView state without navigating.

<p :if={@row_clicked}>Row clicked: {@row_clicked}</p>
<.data_table
  id="users-table"
  class="data-table"
  rows={@users}
  row_click={fn user -> JS.push("row_click", value: %{id: user.id, name: user.name}) end}
>
  <:col :let={user} label="Name">{user.name}</:col>
  <:action :let={user}>
    <.action class="button button--sm">Edit</.action>
  </:action>
</.data_table>
def handle_event("row_click", %{"id" => id, "name" => name}, socket) do
  {:noreply, assign(socket, :row_clicked, "#{name} (##{id})")}
end

Sortable

Set sort_by, sort_order, on_sort; give each sortable column a name. Delegate sorting to Corex.DataTable.Sort. LiveView minimum:

# mount
socket
|> assign(:users, users)
|> Corex.DataTable.Sort.assign_for_sort(:users, default_sort_by: :id, default_sort_order: :asc)

# handle_event("sort", params, socket)
{:noreply, Corex.DataTable.Sort.handle_sort(socket, params, :users)}
<.data_table id="users-sortable" class="data-table" rows={@users} sort_by={@sort_by} sort_order={@sort_order} on_sort="sort">
  <:col :let={user} label="ID" name={:id}>{user.id}</:col>
  <:col :let={user} label="Name" name={:name}>{user.name}</:col>
  <:sort_icon :let={%{direction: direction}}>
    <.heroicon name={%{asc: "hero-chevron-up", desc: "hero-chevron-down", none: "hero-chevron-up-down"}[direction]} />
  </:sort_icon>
</.data_table>

Selectable

Set selectable, selected, on_select, on_select_all, and row_id. Delegate selection to Corex.DataTable.Selection. LiveView minimum:

# mount
socket
|> assign(:users, users)
|> Corex.DataTable.Selection.assign_for_selection(:users, table_id: "users-table", row_id: &"user-#{&1.id}")

def handle_event("select", params, socket) do
  {:noreply, Corex.DataTable.Selection.handle_select(socket, params, :users)}
end

def handle_event("select_all", params, socket) do
  {:noreply, Corex.DataTable.Selection.handle_select_all(socket, params, :users)}
end
<.data_table
  id="users-table"
  class="data-table"
  rows={@users}
  row_id={&"user-#{&1.id}"}
  selectable={true}
  selected={@selected}
  on_select="select"
  on_select_all="select_all"
  checkbox_class="checkbox"
>
  <:checkbox_indicator>
    <.heroicon name="hero-check" />
  </:checkbox_indicator>
  <:col :let={user} label="ID" name={:id}>{user.id}</:col>
  <:col :let={user} label="Name" name={:name}>{user.name}</:col>
  <:col :let={user} label="Email" name={:email}>{user.email}</:col>
</.data_table>

With database

Sort and paginate in your context (order_by, limit, offset), then pass each page to <.data_table> and <.pagination>. Re-fetch on on_sort and on_page_change.

# mount
{rows, total} = MyApp.list_cities(page: 1, page_size: 10, order_by: :name, order_dir: :asc)

{:ok,
 socket
 |> assign(:cities, rows)
 |> assign(:page, 1)
 |> assign(:page_size, 10)
 |> assign(:sort_by, :name)
 |> assign(:sort_order, :asc)
 |> assign(:total, total)}
<.data_table
  id="cities-table"
  class="data-table"
  rows={@cities}
  sort_by={@sort_by}
  sort_order={@sort_order}
  on_sort="sort"
>
  <:col :let={city} label="Name" name={:name}>{city.name}</:col>
</.data_table>
<.pagination
  id="cities-pagination"
  class="pagination"
  count={@total}
  page={@page}
  page_size={@page_size}
  controlled
  on_page_change="page"
/>
def handle_event("sort", %{"sort_by" => sort_by}, socket) do
  sort_by = String.to_existing_atom(sort_by)
  order =
    if socket.assigns.sort_by == sort_by do
      if socket.assigns.sort_order == :asc, do: :desc, else: :asc
    else
      :asc
    end

  {rows, total} =
    MyApp.list_cities(
      page: 1,
      page_size: socket.assigns.page_size,
      order_by: sort_by,
      order_dir: order
    )

  {:noreply,
   socket
   |> assign(:cities, rows)
   |> assign(:page, 1)
   |> assign(:sort_by, sort_by)
   |> assign(:sort_order, order)
   |> assign(:total, total)}
end

def handle_event("page", %{"page" => page}, socket) do
  page = String.to_integer(page)

  {rows, total} =
    MyApp.list_cities(
      page: page,
      page_size: socket.assigns.page_size,
      order_by: socket.assigns.sort_by,
      order_dir: socket.assigns.sort_order
    )

  {:noreply,
   socket
   |> assign(:cities, rows)
   |> assign(:page, page)
   |> assign(:total, total)}
end

Style

Use data attributes to target elements:

[data-scope="data-table"][data-part="root"] {}
[data-scope="data-table"][data-part="thead"] {}
[data-scope="data-table"][data-part="tbody"] {}
[data-scope="data-table"][data-part="row"] {}
[data-scope="data-table"][data-part="cell"] {}
[data-scope="data-table"][data-part="grow-cell"] {}
[data-scope="data-table"][data-part="col-grow"] {}
[data-scope="data-table"][data-part="sort-header"] {}
[data-scope="data-table"][data-part="sort-text"] {}
[data-scope="data-table"][data-part="sort-icon-container"] {}
[data-scope="data-table"][data-part="sort-trigger"] {}
[data-scope="data-table"][data-part="selection-header"] {}
[data-scope="data-table"][data-part="selection-cell"] {}
[data-scope="data-table"][data-part="action-header"] {}
[data-scope="data-table"][data-part="action-cell"] {}
[data-scope="data-table"][data-part="actions"] {}
[data-scope="data-table"][data-part="empty-row"] {}
[data-scope="data-table"][data-part="empty-cell"] {}
[data-scope="data-table"][data-part="empty"] {}

With the data-table class, the stylesheet hides [data-part="empty-row"] when it is not the only row in the tbody so list and stream tables can use <:empty> without server-side row counts.

If you wish to use the default Corex styling, use the class data-table on the component.

Modifier classes on the root:

  • data-table--sm|md|lg|xl — font size on header and body cells; cell padding
  • data-table--accent|brand|alert|success|info — header ink (--color-ink-*) on column titles only

Default host caps use max-width and max-height at --container-md. Override on the host with the same container scale as width, e.g. max-w-none, max-h-none, max-h-2xs, min-h-md, or h-full in a sized parent.

Optional dir="ltr" or dir="rtl" on the component root for text direction. This requires to install Mix.Tasks.Corex.Design first and import the component css file.

@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/data-table.css";

Attributes

  • id (:string) (required) - The id of the table, used for LiveStream updates.
  • rows (:list) (required) - The list of row data to render.
  • row_id (:any) - the function for generating the row id. Defaults to nil.
  • row_click (:any) - the function for handling phx-click on each row. Defaults to nil.
  • row_item (:any) - the function for mapping each row before calling the :col and :action slots. Defaults to &Function.identity/1.
  • translation (Corex.DataTable.Translation) - Override translatable strings. Defaults to nil.
  • sort_by (:atom) - The currently sorted column name. Defaults to nil.
  • sort_order (:atom) - The current sort direction. Defaults to :asc. Must be one of :asc, or :desc.
  • on_sort (:any) - The event to trigger when a sortable header is clicked. Defaults to nil.
  • selectable (:boolean) - Whether the rows are selectable. Defaults to false.
  • selected (:list) - The list of currently selected row IDs. Defaults to [].
  • on_select (:any) - The event to trigger when a single row is selected. Defaults to nil.
  • on_select_all (:any) - The event to trigger when the select all checkbox is toggled. Defaults to nil.
  • checkbox_class (:string) - The class applied to the internal checkboxes. Defaults to nil.
  • dir (:string) - Text direction. Defaults to nil. Must be one of nil, "ltr", or "rtl".
  • Global attributes are accepted.

Slots

  • col (required) - Accepts attributes:
    • label (:string)
    • class (:string)
    • name (:atom) - The field name used for sorting.
  • sort_icon - the slot for showing the sort icon. Accepts attributes:
    • direction (:atom) - the current sort direction (:asc or :desc).
  • action - the slot for showing user actions in the last table column. Accepts attributes:
    • class (:string)
  • checkbox_indicator - the slot for showing the checkbox indicator icon.
  • empty - Optional slot shown when the table has no rows.