Skip to content

Data table

<cae-data-table> shows rows of data in a real <table>: a header cell per column, and the first rowHeader column as the header of its row, so a screen reader names each cell by its column and its row. Register it with @contraagent/elements/data-table/define; the types and sortRows are in @contraagent/elements/data-table.

docs/examples/data-table.ts
// A data table: hand-written rows, one sortable column, a row's action. The
// host holds the rows and the order, and answers the events.
import '@contraagent/elements/data-table/define';
import { sortRows, type CaeDataTable, type DataColumn } from '@contraagent/elements/data-table';
import { pill } from '@contraagent/elements';
interface Job {
id: string;
name: string;
minutes: number;
state: 'done' | 'failed' | 'running';
}
const jobs: Job[] = [
{ id: 'j1', name: 'Build', minutes: 4, state: 'done' },
{ id: 'j2', name: 'Unit tests', minutes: 11, state: 'failed' },
{ id: 'j3', name: 'Deploy preview', minutes: 2, state: 'running' },
];
const columns: DataColumn<Job>[] = [
{ id: 'name', header: 'Job', rowHeader: true, cell: (j) => j.name },
{ id: 'minutes', header: 'Minutes', align: 'end', width: '96px', cell: (j) => j.minutes, compare: (a, b) => a.minutes - b.minutes },
{ id: 'state', header: 'State', width: '120px', cell: (j) => pill({ text: j.state, tone: j.state }) },
];
export function mount(host: HTMLElement): void {
const table = document.createElement('cae-data-table') as CaeDataTable<Job>;
table.caption = 'Jobs';
table.columns = columns;
table.rowKey = (j) => j.id;
table.rows = jobs;
table.addEventListener('cae-sort', (e) => {
table.sort = e.detail;
table.rows = sortRows(jobs, columns, e.detail);
});
const shown = document.createElement('p');
table.addEventListener('cae-activate', (e) => (shown.textContent = `Opened: ${e.detail.id}`));
host.replaceChildren(table, shown);
}

A column is a DataColumn: its header, a CSS grid track as its width, and a function that draws its cell for a row (a template, a string or a number). Every row is a grid of the columns’ tracks, so the rows line up with the header.

src/table/types.ts
/** A column: its header, its width, and what a cell shows for a row. */
export interface DataColumn<R> {
/** Unique among the columns: `data-col` on its cells, the `id` of `cae-sort`. */
id: string;
header: string;
/** The header's tooltip. */
tip?: string;
/** `end` for numbers: right-aligned, tabular figures. */
align?: 'start' | 'end';
/** A CSS grid track: '72px', 'minmax(0, 1fr)' (the default). */
width?: string;
/** The cell: a template, a string or a number. */
cell: (row: R, index: number) => unknown;
/** The cell as plain text: what `sortRows` orders by when the column has no `compare`. */
text?: (row: R) => string;
/** The order of two rows by this column, ascending. */
compare?: (a: R, b: R) => number;
/**
* The header is a button that sends `cae-sort` (true when the column has `compare`).
* Without `compare` and `text`, `sortRows` orders by the cell when it is a string or a number.
*/
sortable?: boolean;
/** The cell names its row (`<th scope="row">`): the first such column does. */
rowHeader?: boolean;
}

The rows are one tab stop. The arrow keys move between rows, and Home and End move to the first and the last row. Enter or a click on a row sends cae-activate with the row’s id (rowKey). With activatable off, they choose the row as the selection and send cae-select; the selected row has the part row-selected. A click on a button or a link inside a cell is that control’s.

A column with compare (or sortable) has a header button that sends cae-sort with the column and the direction. The table does not reorder the rows: the host sets sort and the rows, ordered by sortRows or its own way. sortRows orders by the column’s compare, else its text, else its cell when the cell is a string or a number: a column of plain values needs only sortable.

Above windowAt rows (200), the table draws only the rows on screen and a spacer for the rows above and below them, so the scroll height stays that of the whole list. The rows’ height is measured; rowHeight is the first estimate. The table finds the element that scrolls it, across shadow roots, or the window.

--cae-data-table-gap sets the space between the columns and --cae-data-table-row-padding a row’s padding. The parts are table, caption, head, header-cell, row, row-selected, cell and empty; rowTone gives a row a tone (data-tone) and rowAttrs more attributes. What the cells draw lives in the table’s shadow root: cellStyles adds styles there, after the table’s own (the rows are tr, the cells th and td, each with data-col set to its column’s id).

The words are strings (Strings): empty with no rows (or the empty slot), and the sort buttons’ tooltips.