Sorting
Single & multi-column sort, custom comparators
Sorting is on by default. Click a column header to sort ascending, click again for descending, click a third time to clear. This chapter shows how to configure it.
Jargon check — "ascending" vs "descending": Ascending (
asc) goes small → large: A→Z for text, 0→9 for numbers, oldest → newest for dates. Descending (desc) is the reverse. The little arrow in the header shows which way the column is currently sorted.
Does sorting change my data? No. EliteGrid sorts a view of your rows for display; the original
dataarray you passed in is never reordered or mutated. This is true of filtering and pagination too — they all transform a copy.
Grid-level sorting options
These go in the sorting group of createGrid:
| Property | Type | Default | Meaning |
|---|---|---|---|
enabled |
boolean |
true |
Turn sorting on/off for the whole grid |
multiSort |
boolean |
true |
Allow sorting by more than one column |
multiSortKey |
'shift' | 'ctrl' |
'shift' |
Key to hold for multi-sort |
defaultDirection |
'asc' | 'desc' |
'asc' |
First click direction |
nullsFirst |
boolean |
false |
Put empty values first or last |
What does
nullsFirstmean? "Null" here is shorthand for empty values —null,undefined, or a missing field. When you sort a column, where should the blank rows go: bunched at the top or pushed to the bottom?nullsFirst: trueputs them at the top; the default (false) sends them to the bottom so the real data is what you see first.
const grid = createGrid<User>({
columns,
data,
sorting: {
enabled: true,
multiSort: true,
multiSortKey: 'shift',
defaultDirection: 'asc',
},
})
Turn sorting off entirely
sorting: { enabled: false }
Multi-column sorting
When multiSort is true, the user holds Shift (the default) and clicks additional headers to add secondary sorts. Example: sort by Department, then by Salary within each department.
The order of clicks is the sort priority. To use Ctrl instead of Shift:
sorting: { multiSort: true, multiSortKey: 'ctrl' }
To allow only one sort column at a time:
sorting: { multiSort: false }
Per-column sort settings — the sort group
Each column can override the grid defaults inside its sort group:
| Property | Type | Default | Meaning |
|---|---|---|---|
enabled |
boolean |
inherits grid | Allow sorting this column |
defaultDirection |
'asc' | 'desc' |
inherits grid | First click direction |
comparator |
(a, b) => number |
— | Custom sort logic |
nullsFirst |
boolean |
false |
Empty values first/last |
Disable sorting for one column
{ field: 'avatar', header: '', sort: { enabled: false } }
Custom sort with a comparator
A comparator is a small function that takes two values and decides their order. It works exactly like the callback you pass to JavaScript's built-in Array.prototype.sort: it receives two items, a and b, and returns a number:
| Return value | Meaning |
|---|---|
negative (e.g. -1) |
a should come before b |
positive (e.g. 1) |
a should come after b |
0 |
treat them as equal (keep their relative order) |
A handy trick: for numbers, a - b already gives exactly this shape — negative when a is smaller, positive when larger, zero when equal. That is why the examples below subtract.
You only need a comparator when the default ordering is wrong for your data — for example when text values have a meaningful order that is not alphabetical ("high/medium/low"), or when you want case-insensitive sorting.
Example — sort a priority field by a custom order (high > medium > low) instead of alphabetically:
const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 }
{
field: 'priority',
header: 'Priority',
sort: {
comparator: (a, b) =>
PRIORITY_ORDER[a as keyof typeof PRIORITY_ORDER] -
PRIORITY_ORDER[b as keyof typeof PRIORITY_ORDER],
},
}
Another example — case-insensitive string sort:
{
field: 'name',
header: 'Name',
sort: {
comparator: (a, b) =>
String(a).toLowerCase().localeCompare(String(b).toLowerCase()),
},
}
Controlling sort from code (the API)
The current sort is represented by a sort model — an array of { columnId, direction } objects. The array order is the priority: the first entry is the primary sort, the second breaks ties within the first, and so on. This is the same structure whether one or several columns are sorted.
// Apply a sort programmatically
api.setSortModel([
{ columnId: 'department', direction: 'asc' }, // primary: group by department
{ columnId: 'salary', direction: 'desc' }, // tie-breaker: highest paid first
])
// Read the current sort
const current = api.getSortModel()
// → [{ columnId: 'department', direction: 'asc' }, ...]
// Clear all sorting (returns rows to their original order)
api.clearSort()
columnIdis the column'sfield(see Chapter 02).
Reacting to sort changes
Pass an onSortChange callback in events to run code whenever the sort changes (e.g. to save it to the URL):
const grid = createGrid<User>({
columns,
data,
events: {
onSortChange: (model) => {
console.log('Sorted by:', model)
// model = [{ columnId: 'name', direction: 'asc' }]
},
},
})
Sorting a computed value
When a column reads its value through value.getter (see Chapter 08) instead of a plain field, sorting still works the same way — the comparator (or the default sort) receives whatever the getter returns, not the raw row. That means you can sort by a value that doesn't exist anywhere in your data:
{
field: 'fullName', // no such key on the row — it's computed
header: 'Name',
value: {
getter: (row) => `${row.firstName} ${row.lastName}`,
},
sort: {
comparator: (a, b) => String(a).localeCompare(String(b)),
},
}
Without a comparator here the default sort would still work (getter output is compared with </>), but adding one lets you control things like locale-aware ordering.
Persisting sort across page reloads
A sort the user picked usually shouldn't reset every time they refresh the page. Save the model in onSortChange, then restore it once the grid is ready:
const SORT_KEY = 'usersGrid.sort'
const grid = createGrid<User>({
columns,
data,
events: {
onSortChange: (model) => {
localStorage.setItem(SORT_KEY, JSON.stringify(model))
},
onReady: (api) => {
const saved = localStorage.getItem(SORT_KEY)
if (saved) api.setSortModel(JSON.parse(saved))
},
},
})
The same pattern works for the URL instead of
localStorage— write the model to a query param inonSortChange, and read it back inonReady. That makes a sorted view shareable: copying the link preserves what the other person sees.
Live example
Multi-column sort (hold Shift and click a second header) plus the custom priority comparator from earlier in this chapter, so high → medium → low sorts correctly instead of alphabetically. The button demonstrates the Grid API's clearSort(). Written as defineComponent + h() — the sandbox running this page can't compile a .vue SFC — but every grid concept is identical to <script setup>.
import { defineComponent, h } from 'vue'
import { createGrid, Grid, type GridAPI } from '@elitegrid/vue'
interface Task {
id: number
title: string
priority: 'high' | 'medium' | 'low'
assignee: string
dueInDays: number
}
const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 }
const tasks: Task[] = [
{ id: 1, title: 'Fix login bug', priority: 'high', assignee: 'Ada', dueInDays: 1 },
{ id: 2, title: 'Write release notes', priority: 'low', assignee: 'Alan', dueInDays: 5 },
{ id: 3, title: 'Review PR #482', priority: 'medium', assignee: 'Grace', dueInDays: 2 },
{ id: 4, title: 'Update dependencies', priority: 'low', assignee: 'Ada', dueInDays: 7 },
{ id: 5, title: 'Investigate slow query', priority: 'high', assignee: 'Grace', dueInDays: 1 },
{ id: 6, title: 'Design onboarding flow', priority: 'medium', assignee: 'Alan', dueInDays: 4 },
{ id: 7, title: 'Add dark mode toggle', priority: 'low', assignee: 'Grace', dueInDays: 10 },
{ id: 8, title: 'Patch security vulnerability', priority: 'high', assignee: 'Alan', dueInDays: 1 },
{ id: 9, title: 'Refactor auth module', priority: 'medium', assignee: 'Ada', dueInDays: 6 },
{ id: 10, title: 'Write onboarding docs', priority: 'low', assignee: 'Grace', dueInDays: 8 },
]
let api: GridAPI<Task> | null = null
const grid = createGrid<Task>({
columns: [
{ field: 'title', header: 'Task', size: { flex: 2 } },
{
field: 'priority',
header: 'Priority',
sort: {
comparator: (a, b) =>
PRIORITY_ORDER[a as keyof typeof PRIORITY_ORDER] -
PRIORITY_ORDER[b as keyof typeof PRIORITY_ORDER],
},
},
{ field: 'assignee', header: 'Assignee' },
{ field: 'dueInDays', header: 'Due (days)' },
],
data: tasks,
sorting: { multiSort: true },
events: { onReady: (a) => { api = a } },
})
export default defineComponent({
setup() {
return () =>
h('div', { style: 'height:440px;display:flex;flex-direction:column;gap:8px;padding:8px;box-sizing:border-box' }, [
h('div', { style: 'display:flex;gap:8px;flex-shrink:0' }, [
h('button', {
onClick: () => api?.clearSort(),
style: 'padding:7px 16px;border-radius:7px;border:none;cursor:pointer;font-size:0.8rem;font-weight:600;font-family:system-ui;background:#7c3aed;color:#ffffff',
}, 'Reset sort'),
]),
h('div', { style: 'flex:1;min-height:0' }, [h(Grid, { grid })]),
])
},
})
Common sorting mistakes
| Symptom | Cause | Fix |
|---|---|---|
Numbers sort like text (10 before 2) |
Field is stored as a string, not a number |
Store it as a number, or add a comparator: (a, b) => Number(a) - Number(b) |
| Shift-click adds a sort instead of replacing it | That's multi-sort working as intended | Set sorting: { multiSort: false } if you only ever want one active sort |
Custom comparator "works" but throws on some rows |
Not handling null/undefined values defensively |
Guard the comparator, or rely on nullsFirst instead of writing your own null-handling |
| Sort seems to reset when columns are rebuilt | columns was recreated as a brand-new array (e.g. inside a computed that reruns on unrelated state) — a fresh comparator function each time counts as "changed" |
Define columns once outside reactive tracking, or make sure the computed only reruns when the columns genuinely need to change |
Next: 04 · Filtering