LineaUI.Components.Table (linea_ui v0.3.0)

Copy Markdown View Source

LineOne's tables: basic, bordered, hoverable, zebra and the grid look, with :col slots the way Phoenix's generated CoreComponents.table/1 takes them.

<.table id="users" rows={@users}>
  <:col :let={user} label="Name">{user.name}</:col>
  <:col :let={user} label="Job">{user.job}</:col>
</.table>

<.table id="users" rows={@streams.users} variant="hoverable" row_click={&JS.navigate(~p"/users/#{elem(&1, 1)}")}>
  <:col :let={{_id, user}} label="Name">{user.name}</:col>
  <:action :let={{_id, user}}><.button size="xs">Edit</.button></:action>
</.table>

variant="hoverable" and "zebra" draw the filled header; rounded picks its corners: row rounds both ends of the header row (a table inside a padded card), top only the top corners (a table that is the top of a card) and none keeps it square (full width).

Sortable headers: give a column sort_key and the table on_sort; the header becomes a button that sends on_sort with %{"column" => sort_key, "direction" => "asc" | "desc"}, the direction being the one to sort by next. sort_by and sort_dir mark the current order.

<.table id="t" rows={@rows} on_sort="sort" sort_by={@sort_by} sort_dir={@sort_dir}>
  <:col :let={row} label="Name" sort_key="name">{row.name}</:col>
</.table>

Row selection: on_select adds a checkbox column. Each checkbox sends on_select with %{"id" => key, "selected" => "true" | "false"} (the new state of that row); the header one sends on_select_all with %{"selected" => "true" | "false"}. selected is the list of keys checked. The key is row_key.(row), or the row's DOM id when row_key is not given; keys are compared as given, so pass strings or convert the "id" back.

Density and chrome: compact tightens the cell padding, borderless drops the row rules (and the cell borders of bordered), headless leaves out the <thead> altogether. A column's align (left, center, right) aligns its header and cells.

<.table id="stats" rows={@rows} compact borderless headless>
  <:col :let={row}>{row.label}</:col>
  <:col :let={row} align="right">{row.count}</:col>
</.table>

Every event attr takes an event name or a %JS{}; target sets phx-target.

Expandable rows: an :expand slot adds a last column with a chevron that shows a detail row under each row (expand_label names that column). The slot receives %{row: row, toggle: js}, toggle being the JS that opens and closes it. Expandable rows render one <tbody> per row, so they take a list, not a stream.

Per-row markup: row_class and row_attrs are functions of the row giving extra classes and attributes (phx-click, data-*) of its <tr>; :foot cells make a totals row in a <tfoot>; sticky_header pins the header inside a height-bound wrapper.

Hand-written tables

Without rows, the table renders its inner block and its cells take the look of variant, rounded, compact, borderless and sticky_header from it, so rows can carry any markup: per-row attributes, colspans, detail rows, a totals footer.

<.table id="orders" variant="hoverable" rounded="top" class="min-w-[760px]">
  <.table_head on_sort="sort" sort_by={@sort_by} sort_dir={@sort_dir}
    on_select_all="select_all" all_selected={@all?} some_selected={@some?}>
    <:col sort_key="number">Number</:col>
    <:col align="right">Total</:col>
  </.table_head>
  <.table_body id="orders-body" phx-update="stream">
    <.table_row :for={{id, o} <- @streams.orders} id={id} on_click="open" phx-value-id={o.id}
      selected={o.id in @selected} on_select="select" select_value={o.id}>
      <:cell>{o.number}</:cell>
      <:cell align="right">{o.total}</:cell>
    </.table_row>
    <.table_empty id="orders-empty">No orders.</.table_empty>
  </.table_body>
  <.table_foot><:cell>Total</:cell><:cell align="right">{@sum}</:cell></.table_foot>
</.table>

table_cell/1, table_checkbox/1, table_expand_toggle/1 and table_detail_row/1 cover free cells, custom checkboxes and expandable rows; table_skeleton/1 is the loading state. A class given to a cell always wins over the table's look (the look lives in the CSS components layer).

Summary

Functions

Renders a table of rows (a list or a stream), one column per :col slot.

The <tbody> of a hand-written table. Attributes pass through, so a stream is id plus phx-update="stream", each row taking its stream DOM id.

A cell of a hand-written table, for rows written with free markup: <td>, or <th> with header (a row header, scope="row").

The selection checkbox the table draws, for a header or row written by hand. indeterminate draws the dash of a partial selection (it needs an id, for its hook).

The detail row under the row with DOM id row_id: one cell spanning every column, hidden until table_expand_toggle/1 (or table_toggle_row/2) opens it.

The empty state of a hand-written table: a full-width row shown only when it is the only row of its <tbody>, so it works for lists and streams alike (put it last in the body; a stream needs its id).

The chevron button that opens and closes the detail row of the row with DOM id row_id, for hand-written expandable rows (pair it with table_detail_row/1).

The <tfoot> of a hand-written table, for totals: one row of :cells in bold over a rule, then any free rows.

The <thead> of a hand-written table: one header cell per :col, after an optional select-all checkbox. Takes the look (variant, compact, sticky_header) from the table/1 around it.

A row of a hand-written table: one <td> per :cell, then the inner content (free <td>s or table_cell/1). Any attribute (id, phx-click, phx-value-*, data-*) lands on the <tr>.

A table of placeholder rows shown while the real one loads, in the same look.

The JS that shows or hides the detail panel of the expandable row with DOM id row_id (the toggle the :expand slot receives is this, already bound to its row).

Functions

table(assigns)

Renders a table of rows (a list or a stream), one column per :col slot.

<.table id="people" rows={@streams.people} variant="zebra">
  <:col :let={{_id, person}} label="Name">{person.name}</:col>
  <:col :let={{_id, person}} label="Job">{person.job}</:col>
  <:action :let={{_id, person}}>
    <.button size="xs" phx-click="edit" phx-value-id={person.id}>Edit</.button>
  </:action>
</.table>

Events (each an event name or a %JS{}, sent to target when given):

  • on_sort — a sortable header was clicked: %{"column" => sort_key, "direction" => "asc" | "desc"} (the direction to sort by next).
  • on_select — a row checkbox: %{"id" => key, "selected" => "true" | "false"}.

  • on_select_all — the header checkbox: %{"selected" => "true" | "false"}.

With a stream, selected only reaches the rows the server re-renders: stream_insert the rows whose selection changed, and give all_selected for the header checkbox.

Attributes

  • id (:string) (required)
  • rows (:any) - A list or a @streams.x stream. Leave it out to write the table by hand with table_head/1, table_body/1 and table_row/1. Defaults to nil.
  • row_id (:any) - A function giving each row's DOM id. Streams default to the stream id. Defaults to nil.
  • row_click (:any) - A function of the row returning a JS or an event. Defaults to nil.
  • row_class (:any) - A function of the row returning extra classes of its <tr>. Defaults to nil.
  • row_attrs (:any) - A function of the row returning a map of extra attributes of its <tr> (phx-click, data-*...). Defaults to nil.
  • row_item (:any) - Maps each row before it is given to the slots. Defaults to &Function.identity/1.
  • row_key (:any) - A function giving the selection key of a row. Defaults to the row's DOM id. Defaults to nil.
  • variant (:string) - grid is the gridjs look of LineOne's Grid Table page. Defaults to "basic". Must be one of "basic", "bordered", "hoverable", "zebra", or "grid".
  • rounded (:string) - Corners of the filled header of hoverable and zebra. Defaults to "row". Must be one of "row", "top", or "none".
  • sort_by (:string) - The sort_key of the sorted column. Defaults to nil.
  • sort_dir (:string) - Direction of sort_by. Defaults to "asc". Must be one of "asc", or "desc".
  • on_sort (:any) - Event sent when a sortable header is clicked. Defaults to nil.
  • on_select (:any) - Event sent when a row checkbox is clicked. Defaults to nil.
  • on_select_all (:any) - Event sent by the header checkbox. Defaults to nil.
  • selected (:list) - The keys of the selected rows. Defaults to [].
  • all_selected (:boolean) - Whether the header checkbox is checked. Computed for lists; give it for streams. Defaults to nil.
  • hoverable (:boolean) - Highlights the hovered row. Defaults to the variant's own behavior. Defaults to nil.
  • compact (:boolean) - Tighter cells: px-3 py-2 instead of the variant's padding. Defaults to false.
  • headless (:boolean) - Renders no <thead> at all (not hidden columns), for list-like tables. Defaults to false.
  • sticky_header (:boolean) - Keeps the header row at the top while the body scrolls. The wrapper is the scroll container, so bound its height with wrapper_class (max-h-96). Defaults to false.
  • borderless (:boolean) - Removes the rules between rows, and the cell borders of the bordered variant. Defaults to false.
  • target (:any) - phx-target of the events. Defaults to nil.
  • expand_label (:string) - Header of the expand column. Defaults to nil.
  • wrapper_class (:any) - Classes of the scrolling wrapper. Defaults to nil.
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • col - A column; receives the row (after row_item). Accepts attributes:
    • label (:string) - Header text.
    • class (:any) - Extra classes of the column's cells (the header takes head_class).
    • head_class (:any) - Extra classes of the column's header.
    • align (:string) - Text alignment of the column's header and cells. Must be one of "left", "center", or "right".
    • sort_key (:string) - Makes the header sortable; sent as "column" in on_sort.
  • action - Cells appended to every row, after the columns.
  • expand - Detail shown under a row; receives %{row: row, toggle: js}.
  • empty - Shown in a full-width row when there are no rows (a stream shows it whenever its body is empty).
  • foot - A cell of the <tfoot> row, for totals; one entry per cell. Accepts attributes:
    • class (:any) - Extra classes of the cell.
    • align (:string) - Text alignment of the cell. Must be one of "left", "center", or "right".
    • colspan (:any) - Columns the cell spans.
  • inner_block - The hand-written table (table_head/1, table_body/1, table_foot/1) when rows is not given.

table_body(assigns)

The <tbody> of a hand-written table. Attributes pass through, so a stream is id plus phx-update="stream", each row taking its stream DOM id.

<.table_body id="orders-body" phx-update="stream">
  <.table_row :for={{dom_id, order} <- @streams.orders} id={dom_id}>
    <:cell>{order.number}</:cell>
  </.table_row>
  <.table_empty id="orders-empty">No orders yet.</.table_empty>
</.table_body>

Attributes

  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

table_cell(assigns)

A cell of a hand-written table, for rows written with free markup: <td>, or <th> with header (a row header, scope="row").

<.table_row id="total">
  <.table_cell colspan="3" align="right">Total</.table_cell>
  <.table_cell align="right">{@total}</.table_cell>
</.table_row>

Attributes

  • header (:boolean) - Renders a <th scope="row">. Defaults to false.
  • align (:string) - Defaults to nil.Must be one of nil, "left", "center", or "right".
  • colspan (:any) - Defaults to nil.
  • rowspan (:any) - Defaults to nil.
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block

table_checkbox(assigns)

The selection checkbox the table draws, for a header or row written by hand. indeterminate draws the dash of a partial selection (it needs an id, for its hook).

<.table_checkbox
  checked={row.id in @selected}
  on_click="select"
  values={%{"id" => row.id, "selected" => to_string(row.id not in @selected)}}
/>

on_click (an event name or a %JS{}) is sent with values as phx-value-*, to target when given.

Attributes

  • id (:string) - Defaults to nil.
  • checked (:boolean) - Defaults to false.
  • indeterminate (:boolean) - Defaults to false.
  • label (:string) (required) - The aria-label of the box.
  • on_click (:any) - Defaults to nil.
  • values (:map) - Sent as phx-value-* with on_click. Defaults to %{}.
  • target (:any) - Defaults to nil.
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

table_detail_row(assigns)

The detail row under the row with DOM id row_id: one cell spanning every column, hidden until table_expand_toggle/1 (or table_toggle_row/2) opens it.

<.table_detail_row row_id="order-1" class="bg-slate-50">Notes...</.table_detail_row>

Attributes

  • row_id (:string) (required)
  • open (:boolean) - Defaults to false.
  • class (:any) - Classes of the detail panel. Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

table_empty(assigns)

The empty state of a hand-written table: a full-width row shown only when it is the only row of its <tbody>, so it works for lists and streams alike (put it last in the body; a stream needs its id).

<.table_body id="orders-body" phx-update="stream">
  <.table_row :for={{id, order} <- @streams.orders} id={id}>...</.table_row>
  <.table_empty id="orders-empty">No orders in this filter.</.table_empty>
</.table_body>

Attributes

  • id (:string) (required)
  • class (:any) - Classes of the cell. Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

table_expand_toggle(assigns)

The chevron button that opens and closes the detail row of the row with DOM id row_id, for hand-written expandable rows (pair it with table_detail_row/1).

<.table_row id={"order-#{order.id}"}>
  <:cell>{order.number}</:cell>
  <:cell><.table_expand_toggle row_id={"order-#{order.id}"} /></:cell>
</.table_row>
<.table_detail_row row_id={"order-#{order.id}"}>{order.notes}</.table_detail_row>

The toggle runs on the client (table_toggle_row/2); open sets the starting state. Rows that come in pairs need a list, not a stream.

Attributes

  • row_id (:string) (required)
  • open (:boolean) - Defaults to false.
  • label (:string) - Defaults to "Toggle details".
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

table_foot(assigns)

The <tfoot> of a hand-written table, for totals: one row of :cells in bold over a rule, then any free rows.

<.table_foot>
  <:cell colspan="3">Total</:cell>
  <:cell align="right">{@total}</:cell>
</.table_foot>

Attributes

  • class (:any) - Classes of the :cell row. Defaults to nil.
  • Global attributes are accepted.

Slots

  • cell - A <td> of the totals row. Takes class, align (left center right) and colspan; any other attribute (id, data-*, phx-*, title, aria-*) lands on the <td>.
  • inner_block - Free <tr> rows after the :cell row.

table_head(assigns)

The <thead> of a hand-written table: one header cell per :col, after an optional select-all checkbox. Takes the look (variant, compact, sticky_header) from the table/1 around it.

<.table id="orders" variant="hoverable" rounded="top">
  <.table_head on_sort="sort" sort_by={@sort_by} sort_dir={@sort_dir}>
    <:col sort_key="number">Number</:col>
    <:col>Customer</:col>
    <:col align="right" class="w-32">Total</:col>
  </.table_head>
  <.table_body id="orders-body" phx-update="stream">
    ...
  </.table_body>
</.table>

Events (each an event name or a %JS{}, sent to target when given):

  • on_sort — a header with sort_key was clicked: %{"column" => sort_key, "direction" => "asc" | "desc"} (the direction to sort by next). The header cell carries aria-sort (ascending, descending or none).
  • on_select_all — the header checkbox: %{"selected" => "true" | "false"}, the state to move every row to. all_selected checks it and some_selected draws it indeterminate.

Attributes

  • sort_by (:string) - The sort_key of the sorted column. Defaults to nil.
  • sort_dir (:string) - Direction of sort_by. Defaults to "asc". Must be one of "asc", or "desc".
  • on_sort (:any) - Event sent when a sortable header is clicked. Defaults to nil.
  • on_select_all (:any) - Adds a select-all checkbox cell first; the event it sends. Defaults to nil.
  • all_selected (:boolean) - Whether every row is selected. Defaults to false.
  • some_selected (:boolean) - Whether some rows are selected; with all_selected false the box is indeterminate. Defaults to false.
  • select_all_id (:string) - Id of the select-all checkbox (its hook needs one). Generated when not given. Defaults to nil.
  • select_all_label (:string) - Defaults to "Select all rows".
  • target (:any) - phx-target of the events. Defaults to nil.
  • class (:any) - Classes of the <tr>. Defaults to nil.
  • Global attributes are accepted.

Slots

  • col - A header cell; its content is the label (or give label). Takes label (text when the slot has no content), class, align (left center right), colspan and sort_key (makes it sortable; sent as "column" in on_sort). Any other attribute (id, data-*, phx-*, title, aria-*) lands on the <th>.
  • inner_block - Extra <th> cells after the columns, or further header rows.

table_row(assigns)

A row of a hand-written table: one <td> per :cell, then the inner content (free <td>s or table_cell/1). Any attribute (id, phx-click, phx-value-*, data-*) lands on the <tr>.

<.table_row
  :for={{dom_id, order} <- @streams.orders}
  id={dom_id}
  selected={order.id in @selected}
  on_select="select"
  select_value={order.id}
  on_click={JS.patch(~p"/orders/#{order.id}")}
  class={order.canceled && "opacity-60"}
>
  <:cell class="font-medium">{order.number}</:cell>
  <:cell colspan="2">{order.customer}</:cell>
  <:cell align="right">{order.total}</:cell>
</.table_row>

Events (each an event name or a %JS{}, sent to target when given):

  • on_click — the row was clicked; phx-value-* attributes ride along.
  • on_select — adds a checkbox cell first; clicking it sends %{"id" => select_value, "selected" => "true" | "false"} (the row's new state).

selected checks the box and marks the row (data-selected, a tinted background with a primary bar on its first cell).

Attributes

  • selected (:boolean) - Whether the row is selected. nil leaves the row unmarked. Defaults to nil.
  • on_click (:any) - Event or %JS{} run when the row is clicked. Defaults to nil.
  • on_select (:any) - Adds a selection checkbox cell first; the event it sends. Defaults to nil.
  • select_value (:any) - Sent as "id" by the checkbox. Defaults to nil.
  • select_label (:string) - Defaults to "Select row".
  • target (:any) - phx-target of the events. Defaults to nil.
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • cell - A <td> of the row. Takes class, align (left center right), colspan and rowspan; any other attribute (id, data-*, phx-*, title, aria-*) lands on the <td>.
  • inner_block - Free cells after the :cell ones.

table_skeleton(assigns)

A table of placeholder rows shown while the real one loads, in the same look.

<.table_skeleton id="orders-loading" columns={["Number", "Customer", "Total"]} rows={8} variant="hoverable" />
<.table_skeleton id="loading" cols={4} compact />

columns gives the header labels; without them cols header cells are placeholders too.

Attributes

  • id (:string) (required)
  • rows (:integer) - How many placeholder rows. Defaults to 5.
  • columns (:list) - Header labels; one column each. Defaults to [].
  • cols (:integer) - How many columns when columns is empty. Defaults to 4.
  • variant (:string) - Defaults to "basic". Must be one of "basic", "bordered", "hoverable", "zebra", or "grid".
  • rounded (:string) - Defaults to "row". Must be one of "row", "top", or "none".
  • compact (:boolean) - Defaults to false.
  • headless (:boolean) - Defaults to false.
  • label (:string) - The table's aria-label. Defaults to "Loading".
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

table_toggle_row(js \\ %JS{}, row_id)

@spec table_toggle_row(Phoenix.LiveView.JS.t(), String.t()) :: Phoenix.LiveView.JS.t()

The JS that shows or hides the detail panel of the expandable row with DOM id row_id (the toggle the :expand slot receives is this, already bound to its row).

<.button phx-click={table_toggle_row("devs-row-0")}>Details</.button>