PetalComponents.DataTable (petal_components v4.13.0)

Copy Markdown View Source

A full data table composed around PetalComponents.Table: sortable columns, paging, loading and empty states - driven entirely by a PetalComponents.DataTable.State.

Two wiring modes, like pagination:

  • link mode (default, pass path): every sort and page change is a patch URL built via State.to_params/1. State is shareable, the back button works, and handle_params + State.from_params/2 is the whole backend:

    def handle_params(params, _uri, socket) do
      state = State.from_params(params, fields: [:name, :email])
      {rows, state} = Engine.List.run(all_rows(), state)
      {:noreply, assign(socket, rows: rows, table: state)}
    end
  • event mode (pass on_change): one event, one grammar. Sorts arrive as %{"op" => "sort", "field" => f}, page changes as %{"op" => "page", "page" => n}, quick-search as %{"op" => "search", "term" => t}, page-size changes as %{"op" => "page_size", "page_size" => n}, filter edits as %{"op" => "filter", "field" => f, ...editor inputs}, filter clears as %{"op" => "clear_filters"}. State.handle_op/3 speaks the whole grammar, so the handler is a one-liner.

In link mode the quick-search input and rows-per-page select are wired by the PetalDataTable hook (patch URLs built from templates the component renders); event mode needs no JS.

Selection (selectable) is UI state, not query state - it rides an event in BOTH wiring modes (on_ui, defaulting to on_change) and never touches URLs. Its ops sit outside State: select (id), select_all, clear_selection, toggle_column and move_column (a field + dir delta, applied to your current order with move_column/4 - one line in the handler, race-free under rapid clicks) - a MapSet plus a handful of clauses is the whole backend. Selection is keyed by row_id, which must uniquely identify a record across the whole dataset, not just the current page (primary keys qualify, display fields do not) - a selection retained while paging is only as sound as this key. Same-page duplicates raise; cross-page uniqueness is the caller's contract, exactly as with LiveView stream ids.

Rows can be any enumerable of maps/structs; pair with PetalComponents.DataTable.Engine.List for zero-setup in-memory data, or run the state against your own query layer.

Summary

Functions

Applies one move_column gesture (field + dir) to the CURRENT order. This is the whole contract: the event carries a delta, never a computed destination, because a destination is a snapshot of the DOM the user saw - two rapid moves would both start from it and the second would silently undo the first. Applying the delta to the server's own current order serializes gestures no matter how fast they arrive

Functions

data_table(assigns)

Attributes

  • id (:string) (required)

  • rows (:list) - Defaults to [].

  • state (PetalComponents.DataTable.State) (required)

  • path (:string) - link mode: the base path sort and page changes patch to, with the state encoded as query params. Required unless on_change is set.

    Defaults to nil.

  • on_change (:string) - event mode: the event every table interaction pushes, with an op-shaped payload (op of "sort" | "page" | "clear_filters").

    Defaults to nil.

  • target (:any) - event mode: the phx-target (e.g. @myself). Defaults to nil.

  • loading (:boolean) - render skeleton rows instead of data. Defaults to false.

  • density (:string) - Defaults to "comfortable". Must be one of "comfortable", or "compact".

  • striped (:boolean) - Defaults to false.

  • sticky_header (:boolean) - pin the header row. Inside a data table this needs max_height too: the scroll region is what the header sticks to, and a wrapper that scrolls only sideways cannot pin anything.

    Defaults to false.

  • max_height (:string) - caps the table body's height (any CSS length), making it scroll under a pinned header. Defaults to nil.

  • variant (:string) - Defaults to "basic". Must be one of "ghost", or "basic".

  • of_label (:string) - the range summary's connective, localizable. Defaults to "of".

  • page_label (:string) - cursor mode's page word, localizable. Defaults to "Page".

  • no_results_text (:string) - Defaults to "No results".

  • no_filtered_results_text (:string) - the empty message while filters are active. Defaults to "No results for these filters".

  • clear_filters_label (:string) - Defaults to "Clear filters".

  • results_label (:string) - the announced result-count noun, localizable. Defaults to "results".

  • actions_label (:string) - the actions column's header, announced but not shown. Defaults to "Actions".

  • searchable (:boolean) - render the quick-search input in the toolbar (drives state.search). Defaults to false.

  • search_placeholder (:string) - Defaults to "Search…".

  • search_debounce (:integer) - quick-search debounce in ms, both wiring modes. Defaults to 300.

  • page_size_options (:list) - when non-empty, render a rows-per-page select in the footer. Defaults to [].

  • per_page_label (:string) - the rows-per-page label, localizable. Defaults to "Per page".

  • reset_filters_label (:string) - Defaults to "Reset filters".

  • apply_label (:string) - the filter editors' submit label, localizable. Defaults to "Apply".

  • filter_options_placeholder (:string) - the select editor's option-filter placeholder (shown from 8 options up), localizable. Defaults to "Filter options…".

  • selectable (:boolean) - render a leading checkbox column. Defaults to false.

  • selected (:list) - the currently selected row ids (any terms; compared as strings). Defaults to [].

  • row_label (:any) - a 1-arity function returning a human name for a row, used as the selection checkbox's accessible name. Without it the checkbox announces its position ("Select row 3"), because a primary key - a UUID, say - is not something a screen reader user can act on.

    Defaults to nil.

  • row_id (:any) - row identity for selection: a field (atom) or a 1-arity function of the row. The key must uniquely identify a record across ALL pages, not just the visible one - a selection retained while paging is only as sound as this key. Same-page duplicates raise.

    Defaults to :id.

  • on_ui (:string) - the event UI-state ops (selection) push, both wiring modes. Defaults to on_change; required alongside selectable in link mode.

    Defaults to nil.

  • selected_label (:string) - the selection count's word, localizable. Defaults to "selected".

  • clear_selection_label (:string) - Defaults to "Clear selection".

  • select_row_label (:string) - the row checkbox's aria-label prefix, localizable. Defaults to "Select row".

  • select_all_label (:string) - the header checkbox's aria-label. Defaults to "Select all rows".

  • column_toggle (:boolean) - render a columns-visibility dropdown in the toolbar (rides the on_ui event). Defaults to false.

  • hidden_columns (:list) - fields currently hidden (atoms or strings) - presentation state, never in URLs. Defaults to [].

  • column_toggle_label (:string) - Defaults to "Columns".

  • reorderable (:boolean) - render move up/down controls in the Columns menu (requires column_toggle). Defaults to false.

  • column_order (:list) - fields in display order (atoms or strings) - presentation state on the on_ui event, like hidden_columns, never in URLs. Fields not listed keep their declared order after the listed ones. Empty means the declared :col order.

    Defaults to [].

  • move_up_label (:string) - reorder buttons, localizable. Defaults to "Move up".

  • move_down_label (:string) - Defaults to "Move down".

  • filter_op_labels (:map) - overrides for the operator display names, e.g. %{contains: "enthält"}. Defaults to %{}.

  • class (:any) - Defaults to nil.

Slots

  • col (required) - Accepts attributes:
    • field (:atom) (required)
    • label (:string)
    • sortable (:boolean)
    • filterable (:string) - render a typed filter button + popover editor for this column. Must be one of "text", "number", "select", or "date".
    • options (:list) - select filters: the pickable values, as strings or {label, value} tuples.
    • align (:string) - Must be one of "left", "center", or "right".
    • class (:any)
  • action - trailing actions column, :let receives the row.
  • bulk_action - toolbar content while rows are selected; :let receives the selected ids.
  • toolbar - custom toolbar content rendered above the table.
  • empty - custom empty state; a filters-aware default renders otherwise.

move_column(current_order, all_fields, field, dir)

Applies one move_column gesture (field + dir) to the CURRENT order. This is the whole contract: the event carries a delta, never a computed destination, because a destination is a snapshot of the DOM the user saw - two rapid moves would both start from it and the second would silently undo the first. Applying the delta to the server's own current order serializes gestures no matter how fast they arrive:

def handle_event("table", %{"op" => "move_column", "field" => f, "dir" => dir}, socket) do
  fields = [:name, :email, :status, :amount]
  {:noreply, update(socket, :order, &DataTable.move_column(&1, fields, f, dir))}
end

current_order may be [] (meaning the declared order, supplied as all_fields). Unknown fields and edge positions are no-ops, and stale fields in a saved order are dropped - a ghost entry would make a visible move button do nothing until the field "crossed" it.