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
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.xstream. Leave it out to write the table by hand withtable_head/1,table_body/1andtable_row/1. Defaults tonil.row_id(:any) - A function giving each row's DOM id. Streams default to the stream id. Defaults tonil.row_click(:any) - A function of the row returning aJSor an event. Defaults tonil.row_class(:any) - A function of the row returning extra classes of its<tr>. Defaults tonil.row_attrs(:any) - A function of the row returning a map of extra attributes of its<tr>(phx-click,data-*...). Defaults tonil.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 tonil.variant(:string) -gridis 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 ofhoverableandzebra. Defaults to"row". Must be one of"row","top", or"none".sort_by(:string) - Thesort_keyof the sorted column. Defaults tonil.sort_dir(:string) - Direction ofsort_by. Defaults to"asc". Must be one of"asc", or"desc".on_sort(:any) - Event sent when a sortable header is clicked. Defaults tonil.on_select(:any) - Event sent when a row checkbox is clicked. Defaults tonil.on_select_all(:any) - Event sent by the header checkbox. Defaults tonil.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 tonil.hoverable(:boolean) - Highlights the hovered row. Defaults to the variant's own behavior. Defaults tonil.compact(:boolean) - Tighter cells:px-3 py-2instead of the variant's padding. Defaults tofalse.headless(:boolean) - Renders no<thead>at all (not hidden columns), for list-like tables. Defaults tofalse.sticky_header(:boolean) - Keeps the header row at the top while the body scrolls. The wrapper is the scroll container, so bound its height withwrapper_class(max-h-96). Defaults tofalse.borderless(:boolean) - Removes the rules between rows, and the cell borders of theborderedvariant. Defaults tofalse.target(:any) -phx-targetof the events. Defaults tonil.expand_label(:string) - Header of the expand column. Defaults tonil.wrapper_class(:any) - Classes of the scrolling wrapper. Defaults tonil.class(:any) - Defaults tonil.- Global attributes are accepted.
Slots
col- A column; receives the row (afterrow_item). Accepts attributes:label(:string) - Header text.class(:any) - Extra classes of the column's cells (the header takeshead_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"inon_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) whenrowsis not given.
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 tonil.- Global attributes are accepted.
Slots
inner_block(required)
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 tofalse.align(:string) - Defaults tonil.Must be one ofnil,"left","center", or"right".colspan(:any) - Defaults tonil.rowspan(:any) - Defaults tonil.class(:any) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block
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 tonil.checked(:boolean) - Defaults tofalse.indeterminate(:boolean) - Defaults tofalse.label(:string) (required) - Thearia-labelof the box.on_click(:any) - Defaults tonil.values(:map) - Sent asphx-value-*withon_click. Defaults to%{}.target(:any) - Defaults tonil.class(:any) - Defaults tonil.- Global attributes are accepted.
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 tofalse.class(:any) - Classes of the detail panel. Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
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 tonil.- Global attributes are accepted.
Slots
inner_block(required)
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 tofalse.label(:string) - Defaults to"Toggle details".class(:any) - Defaults tonil.- Global attributes are accepted.
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:cellrow. Defaults tonil.- Global attributes are accepted.
Slots
cell- A<td>of the totals row. Takesclass,align(left center right) andcolspan; any other attribute (id,data-*,phx-*,title,aria-*) lands on the<td>.inner_block- Free<tr>rows after the:cellrow.
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 withsort_keywas clicked:%{"column" => sort_key, "direction" => "asc" | "desc"}(the direction to sort by next). The header cell carriesaria-sort(ascending,descendingornone).on_select_all— the header checkbox:%{"selected" => "true" | "false"}, the state to move every row to.all_selectedchecks it andsome_selecteddraws it indeterminate.
Attributes
sort_by(:string) - Thesort_keyof the sorted column. Defaults tonil.sort_dir(:string) - Direction ofsort_by. Defaults to"asc". Must be one of"asc", or"desc".on_sort(:any) - Event sent when a sortable header is clicked. Defaults tonil.on_select_all(:any) - Adds a select-all checkbox cell first; the event it sends. Defaults tonil.all_selected(:boolean) - Whether every row is selected. Defaults tofalse.some_selected(:boolean) - Whether some rows are selected; withall_selectedfalse the box is indeterminate. Defaults tofalse.select_all_id(:string) - Id of the select-all checkbox (its hook needs one). Generated when not given. Defaults tonil.select_all_label(:string) - Defaults to"Select all rows".target(:any) -phx-targetof the events. Defaults tonil.class(:any) - Classes of the<tr>. Defaults tonil.- Global attributes are accepted.
Slots
col- A header cell; its content is the label (or givelabel). Takeslabel(text when the slot has no content),class,align(left center right),colspanandsort_key(makes it sortable; sent as"column"inon_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.
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.nilleaves the row unmarked. Defaults tonil.on_click(:any) - Event or%JS{}run when the row is clicked. Defaults tonil.on_select(:any) - Adds a selection checkbox cell first; the event it sends. Defaults tonil.select_value(:any) - Sent as"id"by the checkbox. Defaults tonil.select_label(:string) - Defaults to"Select row".target(:any) -phx-targetof the events. Defaults tonil.class(:any) - Defaults tonil.- Global attributes are accepted.
Slots
cell- A<td>of the row. Takesclass,align(left center right),colspanandrowspan; any other attribute (id,data-*,phx-*,title,aria-*) lands on the<td>.inner_block- Free cells after the:cellones.
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 to5.columns(:list) - Header labels; one column each. Defaults to[].cols(:integer) - How many columns whencolumnsis empty. Defaults to4.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 tofalse.headless(:boolean) - Defaults tofalse.label(:string) - The table'saria-label. Defaults to"Loading".class(:any) - Defaults tonil.- Global attributes are accepted.
@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>