Skip to content

Data table ​

hx-data-table is a table with a column model on a native <table>: cell templates, caption, empty and loading state, striped rows, gridlines, sizes and a scrolling body with a sticky header. It draws with the CSS of table[hx-table]. Sorting, paging (client side or lazy), selection, filters and row expansion are built in.

html
<hx-data-table [value]="invoices" [columns]="columns" dataKey="id" caption="Invoices" stripedRows>
  <ng-template hxCell="amount" let-value="value">{{ value | currency: 'EUR' }}</ng-template>
</hx-data-table>
ts
columns: HxColumn[] = [
  { field: 'no', header: 'No.', width: '8rem' },
  { field: 'customer.name', header: 'Customer' },
  { field: 'amount', header: 'Amount', align: 'right' },
];

A column is { field, header, width?, align?, sortable?, filter? }; field can be a path (customer.name), align is left, center or right.

Inputs ​

NameTypeDefault
valueunknown[][]The rows.
columnsHxColumn[][]The column model.
dataKeystringnoneThe property that identifies a row: the DOM of a row survives a new array.
size'small' | 'medium' | 'large''medium'Cell padding.
stripedRows, showGridlinesbooleanfalseAlternate row background; borders around every cell.
hoverRowsbooleantrueRows change colour under the pointer.
emptyMessagestring'No records found'Shown in one cell across the table when there are no rows.
rowClass(row) => string | string[] | Record<string, boolean>noneClasses for the <tr> of a row, as for [class].
emptyValuestring''Text for a cell whose value is null or undefined (not 0, false or ''); cell templates still get the raw value.
loadingbooleanfalseA spinner covers the table; aria-busy="true".
captionstringnoneVisible caption above the table.
ariaLabelstringnoneAccessible name; without caption it is a visually hidden caption.
scrollHeightstringnoneThe body scrolls inside this height, the header stays in view.
scrollablebooleanfalseA wide table scrolls horizontally.

Sorting ​

A sortable column has a button in its header: a click sorts ascending, the next descending, the next removes the sort. The state is in models, so it can be bound and restored:

html
<hx-data-table [value]="rows" [columns]="columns" [(sortField)]="field" [(sortOrder)]="order" />
NameTypeDefault
sortMode'single' | 'multiple''single'In multiple mode a click adds the column to the sort order (ascending, descending, removed); a small number shows the position.
sortField (model), sortOrder (model)string | null, 1 | -1null, 1The sorted column in single mode.
multiSortMeta (model){ field, order }[][]The sorted columns in multiple mode, most important first.
sortFirstOrder'asc' | 'desc''asc'The direction of the first click on a column (descending for numbers, say); HxColumn.sortFirstOrder overrides it per column. The next click gives the opposite order, then the sort is removed.
sortFunction(rows, meta) => rowsnoneReplaces the client-side sort.

The default order puts empty values last, compares numbers and dates by value and text by locale with numeric parts (Item 2 before Item 10); compareValues is exported.

Paging ​

html
<hx-data-table [value]="rows" [columns]="columns" paginator [(first)]="first" [(rows)]="rows" [rowsPerPageOptions]="[10, 25, 50]" />

paginator adds an hx-paginator below the table; rows, first, rowsPerPageOptions and showCurrentPageReport (default on) are its settings. rowsPerPageOptions accepts 'all' ([10, 25, 'all']): the select gets an "All" entry (label: allLabel, default 'All') and the table shows every row. rows is left as it was; "All" ends when the user picks a number, when rows is set from outside or when 'all' leaves rowsPerPageOptions. It stays active when filters or the data change, so the page then shows every matching row. With lazy, lazyLoad carries totalRecords as rows (the current rows while the total is unknown); the table does not load again when the total grows later.

Selection ​

html
<hx-data-table [value]="rows" [columns]="columns" dataKey="id" selectionMode="multiple" [(selection)]="selected" />
NameTypeDefault
selectionMode'single' | 'multiple'nonesingle: a click (or Enter/Space on the focused row) selects the row, a second click unselects it. multiple: a checkbox column with a select-all box in the header.
selection (model)row, null or rowsnullThe selected row (single) or rows (multiple). Rows are compared by dataKey when set, so a new array of the same records keeps the selection.
metaKeySelectionbooleanfalsemultiple without the checkbox column: a click selects only that row, Ctrl/Cmd+click (or Enter/Space) adds or removes it.
selectAllLabel, selectRowLabelstring'Select all rows', 'Select row {row}'The accessible names of the checkboxes.

The select-all box selects all filtered rows of every page and is indeterminate when only some are selected.

Filtering ​

  • Global filter: globalFilterFields lists the properties to look in; call filterGlobal(text) (for example from an hx-input next to the table through a template reference). It is a case-insensitive "contains".
  • Column filters: filterDisplay="row" adds a row of hx-inputs under the headers of columns with filter: true; the texts are in the filters model ({ field: text }). The inputs are named Filter {header} (filterLabel).
  • Header menu: filterDisplay="menu" puts a filter icon button in the header of each column with filter: true instead of the filter row. It opens a popover with a text input (live filtering, same filters model) and a "Clear" button; the button shows an active state while the column is filtered. Keyboard: Tab to the button, Enter or Space opens, the focus moves to the input, Enter in the input or Escape closes and returns the focus to the button.
InputTypeDefaultDescription
filterDisplay'row' | 'menu'noneWhere the column filters are: a filter row, or a popover from the header.
filterLabel, clearLabelstring'Filter {header}', 'Clear'The accessible name of the filter inputs and buttons, and the text of the menu's clear button.

A change of a filter goes back to the first page. In lazy mode nothing is filtered by the table: the texts are in lazyLoad.

Row expansion ​

html
<hx-data-table [value]="rows" [columns]="columns" dataKey="id" [(expandedRowKeys)]="open">
  <ng-template hxRowExpansion let-row>Orders of {{ row.name }}</ng-template>
</hx-data-table>

A hxRowExpansion template adds an expander column: a button with aria-expanded (named by expandLabel / collapseLabel) opens a row below the row, across all columns. expandedRowKeys holds the open rows by dataKey (the row index without one).

Lazy loading ​

With lazy the table neither sorts nor pages: it shows value as given and emits lazyLoad when the sort, the page or the rows per page change (and once on init unless lazyLoadOnInit is off). Set totalRecords to the number of all records so the paginator knows how many pages there are.

html
<hx-data-table [value]="page" [columns]="columns" lazy paginator [totalRecords]="total" [rows]="20" (lazyLoad)="load($event)" />

HxLazyLoadEvent is { first, rows, sortField, sortOrder, multiSortMeta, filters, globalFilter }.

Header hint ​

title on a column is set as the title attribute of the header cell, which browsers show as a tooltip and assistive technology may use as its description. hxColumnHeader="field" adds content (an info icon, a link) next to the label; the sort button, aria-sort and keyboard use stay as they are. Neither has an effect when hxTableHeader replaces the headers. Two hxColumnHeader templates for the same field log a warning in development; the last one wins.

html
<hx-data-table [value]="rows" [columns]="[{ field: 'price', header: 'Price', title: 'Gross, in EUR', sortable: true }]">
  <ng-template hxColumnHeader="price"><a href="/pricing">Pricing</a></ng-template>
</hx-data-table>

Templates ​

Templates are ng-templates inside the table:

DirectiveReplacesContext
hxCell="field"the cell content of that column$implicit row, value, index
hxColumnHeader="field"adds content beside the label of that header, outside the sort button (links, icons)$implicit column
hxTableHeaderthe header cells (inside the <tr>; write <th scope="col">)$implicit columns
hxTableBodythe cells of every row (write <td>)$implicit row, index, columns
hxTableFooteradds a footer row (write <td>)$implicit columns
hxTableCaptionthe caption textnone
hxRowExpansionadds the content of an opened row$implicit row, index, columns
hxTableEmptythe content of the empty row (instead of emptyMessage)none

Accessibility ​

A native <table> with a <caption> (visually hidden when only ariaLabel is set) and <th scope="col"> header cells. A sortable header contains a <button> and its th has aria-sort (ascending, descending, none); a polite live region announces "Sorted by Name, ascending" or that the sort was removed. The paginator is a labelled nav. Selectable rows have aria-selected and, in single or meta-key mode, tabindex="0" with Enter/Space; a filter button (filterDisplay="menu") is named Filter {header}, has aria-haspopup="dialog" and aria-expanded, and opens a non-modal role="dialog" popover with the same name; the checkboxes are labelled "Select row 3" and "Select all rows"; an expander is a button with aria-expanded. While loading the frame has aria-busy="true" and the spinner is a role="status". A table that scrolls (scrollHeight or scrollable) is a focusable region named by ariaLabel or caption, so the keyboard can scroll it.

Tokens ​

The look comes from the design tokens --h-datatable-* (see Theming); override them in your theme, never the component CSS. The filter button of filterDisplay="menu" uses the sort icon tokens (--h-datatable-sort-icon-size, -color, -hover-color) and the header focus ring and selected tokens for its active state.

Part of @gravionlabs/helix-ui; all components are listed in the component reference.

Released under the MIT licence.