tuix-table
A data table with slotted cells, a card layout on mobile, and sorting and pagination the application controls.
tuix-table draws the rows you give it. It never sorts, filters or slices them
by itself.
Sorting and pagination are controlled. The table reports what the user clicked and your application decides what happens next:
The table fires an event. sort, next-page, previous-page or page-size-change. The table itself
changes nothing.
Your code answers. It sorts or fetches the rows, then writes data and the matching state back on the
element.
Nothing moves in the table until that last step runs. Every example below shows it in Angular, Vue and React.
A first table
Two properties are enough: headers describes the columns, data holds the
rows.
id. It keys the rendering and it names the cell slots.Headers
One object per column. Only dataKey and width are required.
| Field | Type | What it does |
|---|---|---|
dataKey | keyof TRowData | Which field of the row to print. Required |
width | number | The relative width of the column. Required |
name | string | The title in the header row |
sortKey | keyof TRowData | Makes the column sortable. See Sorting |
isSlot | boolean | Print a slot instead of the value |
slotName | string | The base name of that slot. Required with isSlot |
cellColor | (row) => string | undefined | A TextVariant for the text of the cell, row by row |
cellBackgroundColor | (row) => string | undefined | A CSS colour for the cell, row by row |
Column widths
width is a weight, not a pixel value. The widths of all columns are added up
and each column takes its share, so 10 / 30 / 20 and 1 / 3 / 2 give the same
layout. When every width is 0, the columns share the space evenly.
Slotted cells
With isSlot: true the cell prints <slot name="{slotName}-{row.id}"> instead
of a value. The row id is part of the slot name, so every cell has a slot of
its own:
A slot takes as many elements as the row needs. The example puts an archive icon
and a notify icon in each cell. The framework tabs show that same pair as an
@for block, a v-for and a map.
Sorting
The table draws the sorting controls. Your application does the sorting.
A column with a sortKey gets the chevron pair, becomes focusable and reacts to
a click, to Enter and to Space. Every column below is sortable:
The three steps
Mark the column. sortKey names the field to order by. It does not have to be the dataKey, so a
formatted column or a slotted one can sort by any field of the row.
Listen for sort. The detail carries the column and the direction the table is asking for: asc for a
new column, and the opposite of the current one when sortBy already names it.
Sort, then write the state back. data takes the new order, and sortBy and sortDirection tell the
header where it stands.
Reorder data alone and the rows move while the header keeps the previous column highlighted. The chevron and
aria-sort follow sortBy and sortDirection, not the click.
Sorting the rows
Array.prototype.sort sorts in place, so sort a copy. The new array is also
what React, Vue and Angular need in order to redraw:
A few things to keep in mind:
- Compare by type. The default comparison is by text, which puts
10before2. Subtract numbers, send text throughlocaleCompare, and compare dates by their timestamps. - Decide where empty values go. A field that can be
nullneeds a rule of its own, so those rows always land on the same side. - One column at a time. The detail never carries a second key. Ordering by more than one column is your code's job, behind the same event.
- Paginated tables sort the whole set, not the page on screen. Sort on the
server, ask for the first page of the new order and set
currentPageback to1. The table does not reset it for you. - Leave
sortByunset until a column is clicked. No chevron is highlighted then, and the rows print in the order you gave them.
Pagination
Same contract as sorting: the footer asks, your code answers. The demo pages through 23 rows.
Turn the footer on. paginated shows it, totalItems is the count across all pages, and
pageSizeOptions fills the page-size select.
Listen for the three events. next-page, previous-page and page-size-change all carry
{ page, pageSize }, the page the table is asking for.
data takes the rows of that page, currentPage and currentSize the state.The chevrons stop at the first and at the last page, which the table works out
from totalItems and the page size. Choosing another page size always asks for
page 1.
Loading
isLoadingData replaces the rows with a loader. The header row and the
pagination footer stay, and the page-size select is disabled:
loader shapes it, and every field is optional:
| Field | Type | Default | What it does |
|---|---|---|---|
size | string | '14px' | The diameter of a dot |
color | string | Palette.MAIN | The colour of the dots |
visible | boolean | true | false hides the loader completely |
ariaLabel | string | 'Loading' | What a screen reader announces |
wrapperStyle | Record<string, string | number> | None | Overrides the centring wrapper |
Responsive behaviour
| Viewport | Layout |
|---|---|
| Above 1280px | Table |
| 640px to 1280px | Table, or cards with showMobileTableInTablet |
| Below 640px | Cards: one block per row, each field on its own line |
disableMobileTable keeps the table layout at every width. The layout is fixed:
a cell that overflows scrolls on its own instead of wrapping, and the table
scrolls sideways when the columns no longer fit its 650px minimum.
API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
data | data | Record<string, any>[] | [] | The rows of the current page. Property, or JSON in the attribute |
headers | headers | IDataTableHeader[] | [] | The columns. Property, or JSON in the attribute |
paginated | paginated | boolean | false | Show the pagination footer |
totalItems | totalitems | number | data.length | Total across all pages, for the counter |
currentPage | currentpage | number | 1 | The page being shown, 1-based |
currentSize | currentsize | number | first of pageSizeOptions | Rows per page |
pageSizeOptions | pagesizeoptions | number[] | [5, 10, 20] | Choices in the page-size select. Property, or JSON |
sortBy | sortby | string | None | The sortKey the rows are currently sorted by |
sortDirection | sortdirection | 'asc' | 'desc' | 'asc' | The direction of the current sort |
isLoadingData | isloadingdata | boolean | false | Replace the rows with the loader |
loader | loader | ThreeDotsLoaderConfig | None | Loader appearance. Property, or JSON in the attribute |
disableMobileTable | disablemobiletable | boolean | false | Keep the table layout on small screens |
showMobileTableInTablet | showmobiletableintablet | boolean | false | Use the card layout up to the tablet breakpoint too |
dark | dark | boolean | false | Render the dark palette |
data, headers, pageSizeOptions and loader are array and object
properties, so their attributes read JSON (data='[{"id":1}]'). Only the
property can carry the functions a header may hold, such as cellColor.
dark switches the whole element, including the tuix-text and tuix-icon elements it draws. Content you slot
into a cell is yours, so pass dark down to it as well.
Events
All four bubble and cross the shadow boundary, and all four carry the state the table is asking for:
| Event | Detail | When |
|---|---|---|
sort | { sortKey: string, sortDirection: SortDirection } | A sortable column header is clicked, or activated with Enter or Space |
next-page | { page: number, pageSize: number } | The next-page chevron is clicked |
previous-page | { page: number, pageSize: number } | The previous-page chevron is clicked |
page-size-change | { page: 1, pageSize: number } | Another page size is chosen |
TypeScript
TuixTableElement<TRowData> types a reference to the element with all its
properties, which is what a useRef or a querySelector needs: