A table for lists that are worked on: sticky header, sortable columns (aria-sort, the server sorts and
the table only draws the arrows), optional row selection with a tri-state header checkbox and a bulk
bar that appears while something is selected, two densities, and the states of a list: loading, error and
empty.
rows is a list, or a stream (stream(socket, :rows, rows)) when the table is long: then the body is a
phx-update="stream" and the empty state shows on its own when the stream is empty.
The columns are :col slots (label, key, sortable, class), each given the row with :let.
Selection is the browser's: the checkboxes are plain inputs named name[], a hook keeps the header
checkbox and the bulk bar in step, and fires input on the table so a form around it hears the change.
selected is the list of row ids ticked when the table is first drawn; after that the browser owns the
ticks. A column with no title of its own (the actions of a row) takes hide_label: its label stays for
screen readers and is not drawn. The arrows of a sorted header are the sort_asc_icon, sort_desc_icon and
sort_none_icon slots (a typographic arrow when empty).
Texts (select_all_label, select_row_label, selected_label with __COUNT__, clear_label) come
from the caller, so the library carries no language of its own.
Around the rows: the toolbar slot sits above the table (a search, filter chips, the columns menu) and the
footer slot below it, for the range of the page ("1-6 of 33") and a DevactionUI.Pagination. With
density_label the toolbar ends with a toggle that switches between the two densities in the browser (density
is where it starts; the choice is kept across redraws), its icon the density_icon slot. max_height is a class
written in full, so the Tailwind of the app finds it through the @source of this package.
A :col with sticky={:start} (the first column) or sticky={:end} (the actions) stays in view while the table
scrolls sideways; in a selectable table the checkbox column is pinned along with the first one. The header is
always sticky, and a table with pinned columns gets a shadow on the pinned side only while there is content
behind it (the DevactionUI.ScrollArea hook).
loading draws loading_rows rows of skeletons in place of the rows (named once for assistive technology by
loading_label, which it needs) and error draws its message, with the error_action slot for a retry button,
as an alert; the empty state is the empty slot, or the text empty_text. In a stream the rows stay in the page
while one of these shows, hidden.
Variations, which add up:
variantis:default(a border and a grey head) or:minimal(no border, a transparent head in sentence case: for a table inside a card or a panel);density={:compact}andstripedthin and band the rows.- the
expandedslot is a detail row opened by a chevron at the start of the row (aria-expanded; the browser toggles it and a redraw keeps it open);expand_labelnames the button and is required with it. group_byis a function row -> key; consecutive rows with the same key sit under a group row drawn by thegroupslot, which is given{key, rows}.- the
tfootslot is the totals row (give it a<tr>), thefooterslot sits below the table. stackturns each row into a card under 640px: theprimarycolumn is the title of the card and the other cells carry their columnlabelas a caption.- the
actionslot is the last cell of a row, the⋯menu with no box of its own (action_labelnames the column and is required with it);sticky_actionspins it to the end while the table scrolls sideways. - a
:coltakesalign(:start,:center,:end) andmonofor figures. - Shift+click on a row checkbox ticks every row between it and the last one clicked.
expanded and group_by need rows to be a list: a stream holds one element per row.
Summary
Functions
Attributes
id(:string) (required)label(:string) (required) - what the table lists, for assistive technology.rows(:any) (required) - a list, or a LiveView stream for a long table.row_id(:any) - a function: row -> id. Defaults to&DevactionUI.DataTable.default_id/1.sort(:any) -{key, :asc | :desc}or nil. Defaults tonil.on_sort(:string) - the event sent withphx-value-keywhen a header is clicked. Defaults tonil.variant(:atom) -:minimalhas no border and a transparent head, for a table inside a card. Defaults to:default. Must be one of:default, or:minimal.striped(:boolean) - bands every other row. Defaults tofalse.stack(:boolean) - under 640px each row becomes a card; theprimarycolumn is its title. Defaults tofalse.group_by(:any) - a function: row -> key; consecutive rows with one key sit under a group row (needs a list). Defaults tonil.selectable(:boolean) - Defaults tofalse.name(:string) - the field name of the row checkboxes (name[]). Defaults to"selected".density(:atom) - the density the table starts in. Defaults to:comfortable. Must be one of:comfortable, or:compact.density_label(:string) - names the density toggle at the end of the toolbar; without it there is no toggle. Defaults tonil.loading(:boolean) - draws skeleton rows instead of the rows. Defaults tofalse.loading_rows(:integer) - how many skeleton rows. Defaults to5.loading_label(:string) - what is loading, read once by screen readers; required whenloading. Defaults tonil.error(:string) - a message that replaces the rows, drawn as an alert. Defaults tonil.empty_text(:string) - what the empty state says when it has no slot. Defaults tonil.selected(:list) - the ids of the rows ticked at the first draw. Defaults to[].max_height(:string) - a Tailwind class; it is only found if it is written in the app's own sources. Defaults to"max-h-[300px]".sticky_actions(:boolean) - pins the cell of theactionslot to the end while the table scrolls sideways. Defaults tofalse.action_label(:string) - names the column of theactionslot for assistive technology; required with it. Defaults tonil.expand_label(:string) - names the chevron that opens a detail row; required with theexpandedslot. Defaults tonil.select_all_label(:string) (required)select_row_label(:string) (required)selected_label(:string) (required) - with__COUNT__.clear_label(:string) (required)class(:any) - Defaults tonil.target(:any) - aphx-target(a@myself) when used inside a LiveComponent. Defaults tonil.- Global attributes are accepted.
Slots
col(required) - Accepts attributes:label(:string) (required)key(:string)sortable(:boolean)hide_label(:boolean) - keep the label for screen readers only.class(:string)primary(:boolean) - the title of the card when the table is astack.mono(:boolean) - figures in the mono face.align(:atom) - where the text of the column sits;:endfor numbers. Must be one of:start,:center, or:end.sticky(:atom) - pins the column to the start (the first column) or the end (the actions) while the table scrolls sideways. Must be one of:start, or:end.
sort_asc_icon- the arrow of the column sorted ascending.sort_desc_icon- the arrow of the column sorted descending.sort_none_icon- the arrow of a sortable column that is not sorted.expand_icon- the chevron of the button that opens a detail row (it turns when open).action- the last cell of a row, given the row: the⋯menu, with no box of its own.expanded- the detail row under a row, given the row; opened by a chevron.group- the content of a group row, given{key, rows}; required withgroup_by.tfoot- the totals: a<tr>in the foot of the table.bulk- the actions of the bulk bar.empty- shown when there are no rows.toolbar- above the table: a search, filter chips, a menu of columns.footer- below the table: the range of the page and a pagination.density_icon- the icon of the density toggle.error_action- under the error: a retry button.