Components/DataTable

DataTable

Lightweight data table with sorting and selection

VerifiedSince 1.0.0

DataTable

Basic Usage

Loading demo...

Row Selection

Loading demo...

Sorting Interaction

Sortable headers expose aria-sort and support pointer click, Enter, and Space to cycle through ascending, descending, and unsorted states. When sortOnClient=false, the table still emits sortChange without reordering local rows.

Dashboard Data Operations

In dashboard data regions, TxDataTable owns the primary list, TxPagination owns page navigation, and TxSkeleton / TxLayoutSkeleton keep loading previews from collapsing into blank space.

Data operations panel

A screenshot-verified table, pagination, and skeleton-loading composition on the local Nexus page.

Loading demo...

Records Composition

The wide CRM layout: a pinned header, a pinned footer, a frozen first column, a three-state select-all, and composed cell primitives. Every one of these is off by default and changes nothing until it is switched on.

What makes the layout work:

  • maxHeight turns the table into its own scroll container, which is what the header and footer stick against; scrollX handles the horizontal axis. Do not put it inside TxScroll in its default mode — that scrolls by transform, and a transformed ancestor kills every position: sticky inside it.
  • With stickyHeader / stickyFooter on, the table switches to border-collapse: separate inside .is-sticky-shell. This is not a style preference: collapsed borders are painted by the table rather than the cell, so a pinned <th> loses its rules the moment it detaches. The switch is scoped to that class, so bordered and striped look unchanged everywhere else.
  • The summary row comes from the footer or footer-<key> slots; with no footer slot, no <tfoot> is rendered at all.
  • The select-all box goes indeterminate on a partial selection and reports aria-checked="mixed".
  • Sort on timestamp fields, never with localeCompare over readable text like "9 days ago" — that places "over 1 year ago" between "3 weeks ago" and "9 days ago" and calls it a chronology.
  • sortCycle="bi" keeps the table from ever returning to unsorted, which suits record lists that always need a definite order.

Records table

Pinned header and footer, a frozen first column, selection highlighting, a three-state select-all, and the tag / dot / link cell primitives.

Loading demo...

The row hover and selection fills are exposed as CSS variables, so a paper-like table can go neutral grey without a prop or an !important:

.records-shell {
  --tx-data-table-row-hover-bg: var(--tx-bui-hover);
  --tx-data-table-row-selected-bg: color-mix(in srgb, var(--tx-bui-accent) 7%, var(--tx-bui-surface));
}

API

TxDataTable Props

NameTypeDefaultDescription
columnsDataTableColumn[][]Column config
dataany[][]Data source
rowKeykeyof T | (row: T, index: number) => string | numberindexUnique row key
loadingbooleanfalseLoading state
emptyTextstring'No data'Empty text
stripedbooleanfalseZebra rows
borderedbooleanfalseShow borders
hoverbooleantrueHover highlight
interactiveRowsbooleanfalseMakes rows focusable (tabindex="0") so Enter/Space can trigger rowClick; automatically enabled when a rowClick listener is attached
selectablebooleanfalseSelectable rows
selectedKeysArray<string | number>[]Selected keys
defaultSort{ key: string; order: 'asc' | 'desc' | null }nullInitial sort for the uncontrolled mode, after which the component owns it. Mutually exclusive with sort — pass one or the other
sort{ key: string; order: 'asc' | 'desc' | null } | null-Controlled sort. Supply it (including as null for "unsorted") and the component stops holding its own state: it reports the user's intent through update:sort and renders whatever the parent sends back. Leave it out entirely for the uncontrolled mode driven by defaultSort
sortOnClientbooleantrueClient-side sort
sortCycle'tri' | 'bi''tri'Header click cycle: tri is ascending → descending → unsorted, bi is ascending → descending → ascending and never unsorts
tableLayout'auto' | 'fixed''auto'Native table layout mode. Use fixed when column widths must stay stable.
nowrapbooleanfalsePrevent wrapping for all header and cell content.
maxHeightstring | number-Caps the height and makes the component its own vertical scroll container. stickyHeader / stickyFooter need this to have anything to stick to, unless an ancestor already scrolls
scrollXbooleanfalseLets the table scroll horizontally inside the component; needed by wide tables with fixed columns
stickyHeaderbooleanfalsePins the header row while the body scrolls
stickyFooterbooleanfalsePins the footer row while the body scrolls
rowClass(row, index) => string | string[] | Record<string, boolean>-Extra classes per row, e.g. to tint a row by its state
highlightSelectedbooleanfalseTints selected rows. Off by default, so existing tables keep expressing selection through the checkbox alone

DataTableColumn

FieldTypeDescription
keystringColumn key
titlestringHeader title
dataIndexstringData field
widthstring | numberColumn width
minWidthstring | numberMinimum column width.
maxWidthstring | numberMaximum column width.
autobooleanForce column width to auto.
fixedboolean | 'left' | 'right'Sticky column side; true equals 'left'. Without scrollX / maxHeight, a fixed column switches the root from overflow: hidden to overflow: visible, so horizontal scrolling must come from an outer container or the sticky offsets have nothing to stick against; with scrollX the component becomes that scroll container itself.
nowrapbooleanPrevent wrapping for this column.
align'left' | 'center' | 'right'Alignment
sortablebooleanSortable
sorter(a, b) => numberCustom sorter
format(value, row, index) => stringCell formatter
headerClassstringHeader class
cellClassstringCell class

Events

EventPayloadDescription
update:selectedKeys(keys)Selection update
selectionChange(keys)Selection change
sortChange(sort)Sort change
update:sort(sort)Sort change; emitted in both controlled and uncontrolled modes, and paired with the sort prop
rowClick({ row, index })Row click

Slots

NameDescription
header-<columnKey>Custom header; receives { column, sorted, order, toggle }. sorted says whether this column is the active sort, order is the direction (null when it is not), and toggle advances the column through the configured cycle.
cell-<columnKey>Custom cell; receives { row, column, value, index }.
footerThe whole summary row: you supply the <td>s yourself, so cells can span columns. A <tfoot> is rendered only when some footer slot is present.
footer-<columnKey>Fills one summary cell per column; receives { column, data }. When footer is also present, footer wins.
emptyEmpty slot rendered when there are no display rows and loading=false.

Best Practices

  • Pass a stable rowKey for business lists; when selection persists across pages, drive selectedKeys with business ids instead of default indexes.
  • Sortable columns support click, Enter, and Space. Use sortOnClient=false with sortChange for remote sorting.
  • tableLayout="fixed" plus explicit width / minWidth keeps dense operations tables from shifting during loading or sorting.
  • Fixed columns should use numeric px width or minWidth; sticky offsets are calculated from those values.
  • Pick one horizontal scroller: either turn on scrollX and let the component be its own, or leave the default and wrap the table in an outer overflow-x: auto container. Both give sticky columns something to stick against — just do not nest two scrollers.
  • A pinned header or footer needs maxHeight (or an ancestor that already scrolls), otherwise there is nothing to stick to; and that scroll container must not scroll by transform, as TxScroll does in its default mode, which disables sticky entirely.
  • Sort on comparable fields such as timestamps or ranks, never on formatted relative-time text.
  • Custom cells should preserve readable text or status-badge labels because sortable headers expose state through aria-sort.
  • The shell clips to a rounded corner, so the component drops the bottom separator on the last row of whichever section ends the table; a summary tfoot keeps the rule that divides it from the body. Reproduce that if you restyle cell borders, otherwise a stray line sits under the table.

Review Notes

  • Accessibility note: TxDataTable renders a native table and gives sortable header cells scope="col", aria-sort, keyboard focus, and Enter/Space handlers.
  • Types: rowKey is declared as keyof T or a callback that returns a string or number; sortChange may emit a DataTableSortState or null.
  • Verified coverage: The component test renders headers and rows, checks pointer sorting plus keyboard aria-sort transitions, selection emission, and layout/nowrap/auto/fixed-column styles. The last-row separator reset is asserted against the SFC source, because vitest never evaluates a <style> block.

Source

  • Component source: packages/tuffex/packages/components/src/data-table/src/TxDataTable.vue.
  • Type contracts: packages/tuffex/packages/components/src/data-table/src/types.ts exports DataTableProps, DataTableColumn, sort state, row-key, and emit types.
  • Coverage: packages/tuffex/packages/components/src/data-table/__tests__/data-table.test.ts verifies header and row rendering, pointer and keyboard sorting (including aria-sort), selection emission, layout/nowrap/auto/fixed-column styles, and the last-row separator reset.
查看源码
packages/tuffex/packages/components/src/data-table/index.ts