Data table
@xui/data-tableA virtualized data grid — only the visible rows are in the DOM, so it stays smooth at 100k+ rows. Columns resize (drag the header divider), auto-fit (double-click the divider), reorder (drag the header) and sort (click the header). Cells select as regions (click, shift-click for a range, ⌘/Ctrl-click for disjoint blocks, ⌘/Ctrl+A for all), the focused cell moves with the arrow keys (hold Shift to extend), and ⌘/Ctrl+C copies the selection as TSV. Built on the pure @xui/core/grid region/size/locator model.
Install
pnpm add @xui/data-table Add the barrel to a standalone component's imports:
import { XuiDataTableImports } from '@xui/data-table';
@Component({
imports: [XuiDataTableImports],
// …
})Examples
Default
<xui-data-table class="w-[760px]" [data]="rows" [columns]="columns" [height]="360" />
<p class="text-foreground-muted mt-2 text-sm">1,000 rows — only the visible window is rendered.</p>Hundred thousand rows
<xui-data-table class="w-[760px]" [data]="rows" [columns]="columns" [height]="400" />
<p class="text-foreground-muted mt-2 text-sm">100,000 rows.</p>Selection and copy
<xui-data-table
class="w-[760px]"
[data]="rows"
[columns]="columns"
[height]="360"
(copied)="copied = $event"
/>
<p class="text-foreground-muted mt-2 text-sm">
Select cells, then press <kbd>⌘/Ctrl</kbd>+<kbd>C</kbd>. Copied text:
</p>
<pre class="bg-surface-inset text-foreground mt-1 max-w-[760px] overflow-auto rounded-md p-2 text-xs">{{ copied || '—' }}</pre>Custom cells
Source only — this one needs a component of its own to run, so there is nothing to render here.
<xui-data-table class="w-[760px]" [data]="rows" [columns]="columns" [height]="360">
<ng-template xuiDataCell let-value let-column="column">
@if (column.id === 'age') {
<span [class]="value >= 50 ? 'text-warning font-medium' : ''">{{ value }}</span>
} @else {
<span class="truncate">{{ value }}</span>
}
</ng-template>
</xui-data-table>Frozen columns
<xui-data-table
class="w-[720px]"
[data]="rows"
[columns]="columns"
[height]="360"
[frozenColumns]="2"
[frozenRows]="1"
/>
<p class="text-foreground-muted mt-2 text-sm">32 columns · 2 frozen cols · 1 frozen row · scroll any direction</p>Row headers
<xui-data-table class="w-[760px]" [data]="rows" [columns]="columns" [height]="360" [rowHeader]="true" />Cell formatters
Source only — this one needs a component of its own to run, so there is nothing to render here.
<xui-data-table class="w-[760px]" [data]="rows" [columns]="columns" [height]="360" [rowHeight]="72">
<ng-template xuiDataCell let-value let-column="column">
@switch (column.id) {
@case ('body') { <xui-truncated-format [value]="value" [length]="70" /> }
@case ('meta') { <xui-json-format [value]="value" [pretty]="true" [length]="120" /> }
@default { <span class="truncate">{{ value }}</span> }
}
</ng-template>
</xui-data-table>Editable cells
Source only — this one needs a component of its own to run, so there is nothing to render here.
<xui-data-table class="w-[600px]" [data]="rows" [columns]="columns" [height]="360" [selectable]="false">
<ng-template xuiDataCell let-value let-row="row" let-column="column">
@if (column.id === 'id') {
<span class="tabular-nums">{{ value }}</span>
} @else {
<xui-editable-cell [value]="value" (edited)="save(row, column, $event)" />
}
</ng-template>
</xui-data-table>API
Marks an <ng-template> as the cell renderer for a {@link XuiDataTable}.
A virtualized data grid: only the visible rows are in the DOM, so it stays smooth at 100k+ rows. Columns are resizable (drag the header divider), reorderable (drag the header), auto-fit (double-click the divider) and sortable; cells support region selection (shift/ctrl-click, arrow keys) and copy-to-clipboard. Cells render with an optional [xuiDataCell] template. Built on the pure @xui/core/grid region/size/locator model.
Inputs
classClassValueExtra classes, merged into the component's own rather than replacing them.''datareadonly T[]The rows. Rendered through a virtual viewport, so a large array costs no more DOM than a small one.[]columnsreadonly XuiDataColumn<T>[]The column definitions, in their initial order. Reordering or resizing by the user is held separately, and resets when this changes.[]rowHeightnumberHeight of a body row in pixels. Fixed rather than measured — it is what the virtual scroller maps a scroll offset onto a row with.36headerHeightnumberHeight of the header row in pixels. Subtracted from height to give the scrolling viewport.36defaultColumnWidthnumberWidth in pixels for a column that declares none of its own.150minColumnWidthnumberFloor for a column's width in pixels, both while dragging a resize handle and when auto-fitting to content.48heightnumberViewport height in pixels.400overscannumberExtra rows and columns rendered beyond the viewport, so a fast scroll does not expose blank space. Higher values trade DOM for smoothness.4selectablebooleanAllow selecting cell regions (shift-range, ctrl/cmd-toggle, arrow keys).truemultiplebooleanAllow more than one disjoint region (ctrl/cmd-click, select-all).truereorderablebooleanAllow reordering columns by dragging their headers.truerowHeaderbooleanShow a leading row-header gutter (row numbers; click to select the whole row).falserowHeaderWidthnumberWidth of the row-header gutter in pixels.48frozenColumnsnumberNumber of leading columns to freeze (pinned left while the rest scroll).0frozenRowsnumberNumber of leading rows to freeze (pinned below the header while the rest scroll).0focusedCellCellCoord | nullThe focused cell. Two-way bindable with [(focusedCell)].nullselectionXRegion[]The selected regions. Two-way bindable with [(selection)].[]Outputs
cellClicked{ row: T; rowIndex: number; column: XuiDataColumn<T> }Emits the clicked cell, with its row, the row's index in data, and the column.sortChangeXuiSortState | nullEmits the new sort, or null when sorting is cleared. Handle it for server-side sorting; local sorting is applied regardless.copiedstringEmitted with the tab-separated text when the selection is copied.columnReorder{ from: number; to: number }Emitted when a column is dragged to a new position (indices into the displayed order).Methods
buildClipboardText(): stringThe selected region(s) as tab-separated rows (unselected cells within the bounding box are blank).reorderColumn(from: number, to: number): voidMove a column from one displayed position to another, carrying its width.scrollToRow(rowIndex: number): voidMove the scroll position so a given row is at the top (used by tests/consumers).An inline-editable cell value. Double-click (or press Enter while focused) to edit; commit with Enter or blur, cancel with Escape. value is two-way bindable and edited fires with the committed text only when it changed.
Inputs
classClassValueExtra classes, merged into the component's own rather than replacing them.''valuestringThe cell value. Two-way bindable with [(value)].''editablebooleanWhether editing is allowed.trueplaceholderstringPlaceholder shown in the editor when the value is empty.''Outputs
editedstringEmitted with the committed value when it changed.cancelledvoidEmitted when editing is cancelled (Escape).Methods
edit(): voidEnter edit mode (no-op when not editable).commit(): voidCommit the draft, emitting edited if it changed.cancel(): voidDiscard the draft and leave edit mode.Renders a cell value as JSON in a monospace <code>, truncated past length characters (via {@link XuiTruncatedFormat}). omitQuotes prints a plain string value without the surrounding quotes; pretty indents the output.
Inputs
classClassValueExtra classes, merged into the component's own rather than replacing them.''valueunknownThe value to serialize as JSON.nullomitQuotesbooleanPrint a plain-string value without surrounding quotes.falseprettybooleanIndent the JSON with two spaces.falselengthnumberMaximum characters before truncating.200showExpandbooleanWhether the truncated value shows an inline "more"/"less" toggle.trueTruncates a long cell value to length characters, appending an ellipsis and a title tooltip with the full text. When showExpand is set (the default), a "more"/"less" toggle reveals or re-collapses the full value inline.
Inputs
classClassValueExtra classes, merged into the component's own rather than replacing them.''valueunknownThe value to render (coerced to a string).''lengthnumberMaximum characters before truncating.80showExpandbooleanWhether to show an inline "more"/"less" toggle when truncated.true