Sorting
Multi-column sorting with priority badges, per-type comparators and configurable null ordering. Sorting is a pipeline stage, so it runs after filtering and before windowing.
Basic Usage
Mark a column sortable and its header becomes a button. Clicking walks it through ascending, descending and back to
unsorted.
// sortable turns on click-to-sort for one column
const columns: ColumnDef<Person>[] = [
{ id: 'name', header: 'Name', sortable: true, flex: 1 },
{ id: 'team', header: 'Team', sortable: true, width: 130 },
{ id: 'salary', header: 'Salary', sortable: true, align: 'right', width: 140,
type: 'currency' },
{ id: 'email', header: 'Email', flex: 1.4 } // not sortable
];
// The shorthand form registers sorting for you
<DataGrid data={people} {columns} {getRowId} />Multi-Sort
Shift-click a second header to add it to the sort rather than replace it. A numbered badge appears once more than one column is involved, so the order is visible rather than implied.
Sorting reads the value behind a cell rather than the one on screen, which matters for one
thing only: a cellValue gate does not reach it, so a masked column can still be ordered by what it hides. Take sortable off a column you mask.
The sort is stable, so rows every column in it compares as equal keep the order they arrived in. Two people on the same team with the same salary stay in the order the data listed them, and they stay there each time the same sort is applied.
import { createDataGrid, sorting } from '@sv5ui/datagrid';
const grid = createDataGrid<Person>({
data: people,
columns,
getRowId,
features: [
sorting({
// Where the grid starts
initial: [
{ columnId: 'team', direction: 'asc' },
{ columnId: 'salary', direction: 'desc' }
]
})
]
});
// Shift-click a header, or press Shift+Enter on a focused header cell, to
// add a column to the sort instead of replacing it. A numbered badge shows
// the priority once more than one column is sorted.Sort Cycle
The Salary column below starts descending and never returns to unsorted, which suits amounts and dates where an arbitrary order is rarely what anyone wants.
// What a header click walks through. null clears the column.
sorting({ cycle: ['asc', 'desc', null] }) // the default
// Never leave the column unsorted
sorting({ cycle: ['asc', 'desc'] })
// Start with the largest first, which suits amounts and dates
sorting({ cycle: ['desc', 'asc', null] })
// A cycle with no direction at all falls back to the default.Null Ordering
Both grids are sorted by Country ascending. The only difference is where the blanks go.
nulls: 'first'
nulls: 'last'
// Where blank values land, regardless of direction
sorting({ nulls: 'first' }) // the default
sorting({ nulls: 'last' })
// Blank is null, undefined or the empty string: the same set the blank
// filter operator matches and the renderers show as empty. A column that
// means something by '' and wants it ordered as a value needs a sortFn.
// Under rowModel: 'server' the choice travels, so the same grid does not
// reorder itself when the rows start coming from a database:
toSortRequest(getSorting(grid)!.sort, grid.columns.defs, getSorting(grid)!.nulls);
// [{ field: 'country', direction: 'desc', nulls: 'last' }]
//
// Written as the side the blanks actually land on rather than the side the
// option names: on the client a blank sorts as the smallest value, so
// nulls: 'first' arrives as 'last' once the direction is descending, while
// SQL's NULLS FIRST means first either way. Passing it straight into
// ORDER BY ... NULLS LAST is what keeps the two orderings identical.Dates Sort as Dates
A column typed date or datetime orders by the date, not by the text of whatever the row happens to hold. The five rows below
carry three different forms of the same idea, and the column still reads chronologically.
Without the type there is nothing to say these are dates, so a Date object next to an ISO string would be compared as text and put June before January.
// A date or datetime column orders by the date behind the cell, so the
// form a row happens to hold does not decide the order.
const columns: ColumnDef<Shipment>[] = [
{ id: 'ref', header: 'Reference', width: 130 },
{ id: 'shippedAt', header: 'Shipped', sortable: true, width: 150,
type: 'date', typeOptions: { locale: 'en-US' } }
];
const shipments = [
{ id: 1, ref: 'A-1001', shippedAt: '2024-03-14' }, // date-only string
{ id: 2, ref: 'A-1002', shippedAt: new Date(2024, 0, 5) }, // Date object
{ id: 3, ref: 'A-1003', shippedAt: new Date(2024, 6, 22).getTime() }, // epoch number
{ id: 4, ref: 'A-1004', shippedAt: '2024-05-02' },
{ id: 5, ref: 'A-1005', shippedAt: new Date(2023, 11, 31) }
];
// Without type: 'date' these are compared as text, which puts a Date
// object's "Fri Jul 22 2024" before an ISO string's "2024-01-05".
//
// A date-only string names a calendar day rather than UTC midnight, so it
// keeps its day west of Greenwich as well as east of it.Custom Comparators
sortFn replaces the default value comparison. Compare ascending and let the grid apply the direction,
otherwise descending inverts your intent twice.
It receives whole rows, which is also what it costs: a plain column reads its value once per
row and compares the keys, while a sortFn is called on every comparison, on the order of 1.7 million times for 100k rows. Worth it for an
order that has meaning; not worth it for one a sortField or a type already describes. It is also client-only: under a server row model the request carries the
column and the direction, and your backend decides what they mean.
// A custom comparator. The direction factor is applied on top of the
// result, so always compare ascending here.
const rank = { suspended: 0, invited: 1, active: 2 };
const columns: ColumnDef<Person>[] = [
{
id: 'status',
header: 'Status',
sortable: true,
width: 140,
type: 'badge',
sortFn: (a, b) => rank[a.status] - rank[b.status]
}
];Sorting by Another Field
A name column showing the full name but ordering by the surname. Sort the first column and watch the second: it is what the order follows.
// A column that shows one thing and orders by another: the cell reads
// "Ada Lovelace" while the sort follows the surname.
{
id: 'name',
header: 'Name',
sortable: true,
sortField: 'lastName'
}
// On the client sortField names a row property. Under rowModel: 'server'
// it is what toSortRequest puts on the wire, so a column id that is a UI
// concern never has to be one your database recognises.
// sortFn wins over sortField when both are set.Driving the Sort
getSorting(grid) returns the feature state, or undefined when sorting is not registered.
Nothing here touches the headers. The grid below starts unsorted and every button is a call you could make from anywhere: a toolbar, a saved view, a keyboard shortcut of your own.
import { getSorting } from '@sv5ui/datagrid';
const sorting = getSorting(grid);
sorting?.sort; // SortState[], reactive
sorting?.directionOf('salary'); // 'asc' | 'desc' | undefined
sorting?.priorityOf('salary'); // 1-based, or null below two columns
// Replace the sort outright
sorting?.setSort([{ columnId: 'name', direction: 'asc' }]);
// Walk one column through the cycle; append keeps the rest of the sort
sorting?.toggleSort('team');
sorting?.toggleSort('salary', { append: true });
// The same two, merged into the grid api
grid.api.setSort?.([{ columnId: 'name', direction: 'asc' }]);
grid.api.toggleSort?.('team');
// Every change emits
grid.events.on('sortChanged', ({ sort }) => console.log(sort));sorting() Options
| Option | Default |
|---|---|
initial | [] |
nulls | 'first' |
cycle | ['asc', 'desc', null] |
Column Fields
| Property | Default |
|---|---|
sortable | false |
sortFn | - |
sortField | - |
Sorting State
Everything getSorting(grid) exposes. setSort and toggleSort are also merged into grid.api.
| Member | Description |
|---|---|
sort | The active sort, in priority order |
nulls | Where blanks land, as the feature was registered. Read it back to send it with a server request |
cycle | What a header click walks through, as the feature was registered |
setSort(sort) | Replaces the sort outright and emits sortChanged |
toggleSort(id, opts) | Walks one column through the cycle; append keeps the rest |
directionOf(id) | The direction in force for one column |
priorityOf(id) | 1-based position, or null when fewer than two columns are sorted |
Keyboard
| Keys | Action |
|---|---|
Enter or Space | Sorts by the focused header column, replacing the sort |
Shift + Enter | Adds the focused column to the sort instead |
Click | Walks the column through the configured cycle |
Shift + Click | Adds the column to the sort, keeping the rest |