Skip to content

Commit d2ae539

Browse files
authoredApr 13, 2024··
feat: new sortUndefined last and first options (#5486)
fixes #5191 fixes #4113
1 parent 5a907f3 commit d2ae539

File tree

7 files changed

+74
-31
lines changed

7 files changed

+74
-31
lines changed
 

‎docs/api/features/sorting.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,22 @@ Inverts the order of the sorting for this column. This is useful for values that
123123
### `sortUndefined`
124124
125125
```tsx
126-
sortUndefined?: false | -1 | 1 // defaults to 1
126+
sortUndefined?: 'first' | 'last' | false | -1 | 1 // defaults to 1
127127
```
128128
129+
- `'first'`
130+
- Undefined values will be pushed to the beginning of the list
131+
- `'last'`
132+
- Undefined values will be pushed to the end of the list
129133
- `false`
130134
- Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies)
131135
- `-1`
132136
- Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list)
133137
- `1`
134138
- Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list)
135139
140+
> NOTE: `'first'` and `'last'` options are new in v8.16.0
141+
136142
## Column API
137143
138144
### `getAutoSortingFn`

‎docs/guide/sorting.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -284,16 +284,20 @@ Any undefined or nullish values will be sorted to the beginning or end of the li
284284

285285
In not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list.
286286

287+
- `'first'` - Undefined values will be pushed to the beginning of the list
288+
- `'last'` - Undefined values will be pushed to the end of the list
287289
- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies)
288290
- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list)
289291
- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list)
290292

293+
> NOTE: `'first'` and `'last'` options are new in v8.16.0
294+
291295
```jsx
292296
const columns = [
293297
{
294298
header: () => 'Rank',
295299
accessorKey: 'rank',
296-
sortUndefined: -1, // 1 | -1 | false
300+
sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false
297301
},
298302
]
299303
```

‎examples/react/pagination/src/main.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ function Filter({
251251
const columnFilterValue = column.getFilterValue()
252252

253253
return typeof firstValue === 'number' ? (
254-
<div className="flex space-x-2">
254+
<div className="flex space-x-2" onClick={e => e.stopPropagation()}>
255255
<input
256256
type="number"
257257
value={(columnFilterValue as [number, number])?.[0] ?? ''}
@@ -279,11 +279,12 @@ function Filter({
279279
</div>
280280
) : (
281281
<input
282-
type="text"
283-
value={(columnFilterValue ?? '') as string}
282+
className="w-36 border shadow rounded"
284283
onChange={e => column.setFilterValue(e.target.value)}
284+
onClick={e => e.stopPropagation()}
285285
placeholder={`Search...`}
286-
className="w-36 border shadow rounded"
286+
type="text"
287+
value={(columnFilterValue ?? '') as string}
287288
/>
288289
)
289290
}

‎examples/react/sorting/src/main.tsx

+42-15
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@ import {
88
flexRender,
99
getCoreRowModel,
1010
getSortedRowModel,
11+
SortingFn,
1112
SortingState,
1213
useReactTable,
1314
} from '@tanstack/react-table'
1415
import { makeData, Person } from './makeData'
1516

17+
//custom sorting logic for one of our enum columns
18+
const sortStatusFn: SortingFn<Person> = (rowA, rowB, _columnId) => {
19+
const statusA = rowA.original.status
20+
const statusB = rowB.original.status
21+
const statusOrder = ['single', 'complicated', 'relationship']
22+
return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB)
23+
}
24+
1625
function App() {
1726
const rerender = React.useReducer(() => ({}), {})[1]
1827

@@ -23,60 +32,78 @@ function App() {
2332
{
2433
accessorKey: 'firstName',
2534
cell: info => info.getValue(),
26-
footer: props => props.column.id,
35+
//this column will sort in ascending order by default since it is a string column
2736
},
2837
{
2938
accessorFn: row => row.lastName,
3039
id: 'lastName',
3140
cell: info => info.getValue(),
3241
header: () => <span>Last Name</span>,
33-
footer: props => props.column.id,
42+
sortUndefined: 'last', //force undefined values to the end
43+
sortDescFirst: false, //first sort order will be ascending (nullable values can mess up auto detection of sort order)
3444
},
3545
{
3646
accessorKey: 'age',
3747
header: () => 'Age',
38-
footer: props => props.column.id,
48+
//this column will sort in descending order by default since it is a number column
3949
},
4050
{
4151
accessorKey: 'visits',
4252
header: () => <span>Visits</span>,
43-
footer: props => props.column.id,
53+
sortUndefined: 'last', //force undefined values to the end
4454
},
4555
{
4656
accessorKey: 'status',
4757
header: 'Status',
48-
footer: props => props.column.id,
58+
sortingFn: sortStatusFn, //use our custom sorting function for this enum column
4959
},
5060
{
5161
accessorKey: 'progress',
5262
header: 'Profile Progress',
53-
footer: props => props.column.id,
54-
sortDescFirst: true, // This column will sort in descending order first (default for number columns anyway)
63+
// enableSorting: false, //disable sorting for this column
64+
},
65+
{
66+
accessorKey: 'rank',
67+
header: 'Rank',
68+
invertSorting: true, //invert the sorting order (golf score-like where smaller is better)
5569
},
5670
{
5771
accessorKey: 'createdAt',
5872
header: 'Created At',
59-
// sortingFn: 'datetime' (inferred from the data)
73+
// sortingFn: 'datetime' //make sure table knows this is a datetime column (usually can detect if no null values)
6074
},
6175
],
6276
[]
6377
)
6478

65-
const [data, setData] = React.useState(() => makeData(10_000))
66-
const refreshData = () => setData(() => makeData(10_000))
79+
const [data, setData] = React.useState(() => makeData(1_000))
80+
const refreshData = () => setData(() => makeData(100_000)) //stress test with 100k rows
6781

6882
const table = useReactTable({
69-
data,
7083
columns,
84+
data,
85+
debugTable: true,
86+
getCoreRowModel: getCoreRowModel(),
87+
getSortedRowModel: getSortedRowModel(), //client-side sorting
88+
onSortingChange: setSorting, //optionally control sorting state in your own scope for easy access
89+
// sortingFns: {
90+
// sortStatusFn, //or provide our custom sorting function globally for all columns to be able to use
91+
// },
92+
//no need to pass pageCount or rowCount with client-side pagination as it is calculated automatically
7193
state: {
7294
sorting,
7395
},
74-
onSortingChange: setSorting,
75-
getCoreRowModel: getCoreRowModel(),
76-
getSortedRowModel: getSortedRowModel(),
77-
debugTable: true,
96+
// autoResetPageIndex: false, // turn off page index reset when sorting or filtering - default on/true
97+
// enableMultiSort: false, //Don't allow shift key to sort multiple columns - default on/true
98+
// enableSorting: false, // - default on/true
99+
// enableSortingRemoval: false, //Don't allow - default on/true
100+
// isMultiSortEvent: (e) => true, //Make all clicks multi-sort - default requires `shift` key
101+
// maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once - default is Infinity
78102
})
79103

104+
//access sorting state from the table instance
105+
console.log(table.getState().sorting)
106+
80107
return (
81108
<div className="p-2">
82109
<div className="h-2" />

‎examples/react/sorting/src/makeData.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { faker } from '@faker-js/faker'
22

33
export type Person = {
44
firstName: string
5-
lastName: string
5+
lastName: string | undefined
66
age: number
7-
visits: number
7+
visits: number | undefined
88
progress: number
99
status: 'relationship' | 'complicated' | 'single'
10+
rank: number
1011
createdAt: Date
1112
subRows?: Person[]
1213
}
@@ -22,23 +23,24 @@ const range = (len: number) => {
2223
const newPerson = (): Person => {
2324
return {
2425
firstName: faker.person.firstName(),
25-
lastName: faker.person.lastName(),
26+
lastName: Math.random() < 0.1 ? undefined : faker.person.lastName(),
2627
age: faker.number.int(40),
27-
visits: faker.number.int(1000),
28+
visits: Math.random() < 0.1 ? undefined : faker.number.int(1000),
2829
progress: faker.number.int(100),
2930
createdAt: faker.date.anytime(),
3031
status: faker.helpers.shuffle<Person['status']>([
3132
'relationship',
3233
'complicated',
3334
'single',
3435
])[0]!,
36+
rank: faker.number.int(100),
3537
}
3638
}
3739

3840
export function makeData(...lens: number[]) {
3941
const makeDataLevel = (depth = 0): Person[] => {
4042
const len = lens[depth]!
41-
return range(len).map((d): Person => {
43+
return range(len).map((_d): Person => {
4244
return {
4345
...newPerson(),
4446
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,

‎packages/table-core/src/features/RowSorting.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export interface SortingColumnDef<TData extends RowData> {
9090
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/sorting#sortundefined)
9191
* @link [Guide](https://tanstack.com/table/v8/docs/guide/sorting)
9292
*/
93-
sortUndefined?: false | -1 | 1
93+
sortUndefined?: false | -1 | 1 | 'first' | 'last'
9494
}
9595

9696
export interface SortingColumn<TData extends RowData> {

‎packages/table-core/src/utils/getSortedRowModel.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function getSortedRowModel<TData extends RowData>(): (
2525
const columnInfoById: Record<
2626
string,
2727
{
28-
sortUndefined?: false | -1 | 1
28+
sortUndefined?: false | -1 | 1 | 'first' | 'last'
2929
invertSorting?: boolean
3030
sortingFn: SortingFn<TData>
3131
}
@@ -51,25 +51,28 @@ export function getSortedRowModel<TData extends RowData>(): (
5151
for (let i = 0; i < availableSorting.length; i += 1) {
5252
const sortEntry = availableSorting[i]!
5353
const columnInfo = columnInfoById[sortEntry.id]!
54+
const sortUndefined = columnInfo.sortUndefined
5455
const isDesc = sortEntry?.desc ?? false
5556

5657
let sortInt = 0
5758

5859
// All sorting ints should always return in ascending order
59-
if (columnInfo.sortUndefined) {
60+
if (sortUndefined) {
6061
const aValue = rowA.getValue(sortEntry.id)
6162
const bValue = rowB.getValue(sortEntry.id)
6263

6364
const aUndefined = aValue === undefined
6465
const bUndefined = bValue === undefined
6566

6667
if (aUndefined || bUndefined) {
68+
if (sortUndefined === 'first') return aUndefined ? -1 : 1
69+
if (sortUndefined === 'last') return aUndefined ? 1 : -1
6770
sortInt =
6871
aUndefined && bUndefined
6972
? 0
7073
: aUndefined
71-
? columnInfo.sortUndefined
72-
: -columnInfo.sortUndefined
74+
? sortUndefined
75+
: -sortUndefined
7376
}
7477
}
7578

0 commit comments

Comments
 (0)
Please sign in to comment.