Skip to content

Commit

Permalink
feat: new sortUndefined last and first options (#5486)
Browse files Browse the repository at this point in the history
fixes #5191
fixes #4113
  • Loading branch information
KevinVandy committed Apr 13, 2024
1 parent 5a907f3 commit d2ae539
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 31 deletions.
8 changes: 7 additions & 1 deletion docs/api/features/sorting.md
Expand Up @@ -123,16 +123,22 @@ Inverts the order of the sorting for this column. This is useful for values that
### `sortUndefined`
```tsx
sortUndefined?: false | -1 | 1 // defaults to 1
sortUndefined?: 'first' | 'last' | false | -1 | 1 // defaults to 1
```
- `'first'`
- Undefined values will be pushed to the beginning of the list
- `'last'`
- Undefined values will be pushed to the end of the list
- `false`
- Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies)
- `-1`
- Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list)
- `1`
- Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list)
> NOTE: `'first'` and `'last'` options are new in v8.16.0
## Column API
### `getAutoSortingFn`
Expand Down
6 changes: 5 additions & 1 deletion docs/guide/sorting.md
Expand Up @@ -284,16 +284,20 @@ Any undefined or nullish values will be sorted to the beginning or end of the li

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.

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

> NOTE: `'first'` and `'last'` options are new in v8.16.0
```jsx
const columns = [
{
header: () => 'Rank',
accessorKey: 'rank',
sortUndefined: -1, // 1 | -1 | false
sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false
},
]
```
Expand Down
9 changes: 5 additions & 4 deletions examples/react/pagination/src/main.tsx
Expand Up @@ -251,7 +251,7 @@ function Filter({
const columnFilterValue = column.getFilterValue()

return typeof firstValue === 'number' ? (
<div className="flex space-x-2">
<div className="flex space-x-2" onClick={e => e.stopPropagation()}>
<input
type="number"
value={(columnFilterValue as [number, number])?.[0] ?? ''}
Expand Down Expand Up @@ -279,11 +279,12 @@ function Filter({
</div>
) : (
<input
type="text"
value={(columnFilterValue ?? '') as string}
className="w-36 border shadow rounded"
onChange={e => column.setFilterValue(e.target.value)}
onClick={e => e.stopPropagation()}
placeholder={`Search...`}
className="w-36 border shadow rounded"
type="text"
value={(columnFilterValue ?? '') as string}
/>
)
}
Expand Down
57 changes: 42 additions & 15 deletions examples/react/sorting/src/main.tsx
Expand Up @@ -8,11 +8,20 @@ import {
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingFn,
SortingState,
useReactTable,
} from '@tanstack/react-table'
import { makeData, Person } from './makeData'

//custom sorting logic for one of our enum columns
const sortStatusFn: SortingFn<Person> = (rowA, rowB, _columnId) => {
const statusA = rowA.original.status
const statusB = rowB.original.status
const statusOrder = ['single', 'complicated', 'relationship']
return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB)
}

function App() {
const rerender = React.useReducer(() => ({}), {})[1]

Expand All @@ -23,60 +32,78 @@ function App() {
{
accessorKey: 'firstName',
cell: info => info.getValue(),
footer: props => props.column.id,
//this column will sort in ascending order by default since it is a string column
},
{
accessorFn: row => row.lastName,
id: 'lastName',
cell: info => info.getValue(),
header: () => <span>Last Name</span>,
footer: props => props.column.id,
sortUndefined: 'last', //force undefined values to the end
sortDescFirst: false, //first sort order will be ascending (nullable values can mess up auto detection of sort order)
},
{
accessorKey: 'age',
header: () => 'Age',
footer: props => props.column.id,
//this column will sort in descending order by default since it is a number column
},
{
accessorKey: 'visits',
header: () => <span>Visits</span>,
footer: props => props.column.id,
sortUndefined: 'last', //force undefined values to the end
},
{
accessorKey: 'status',
header: 'Status',
footer: props => props.column.id,
sortingFn: sortStatusFn, //use our custom sorting function for this enum column
},
{
accessorKey: 'progress',
header: 'Profile Progress',
footer: props => props.column.id,
sortDescFirst: true, // This column will sort in descending order first (default for number columns anyway)
// enableSorting: false, //disable sorting for this column
},
{
accessorKey: 'rank',
header: 'Rank',
invertSorting: true, //invert the sorting order (golf score-like where smaller is better)
},
{
accessorKey: 'createdAt',
header: 'Created At',
// sortingFn: 'datetime' (inferred from the data)
// sortingFn: 'datetime' //make sure table knows this is a datetime column (usually can detect if no null values)
},
],
[]
)

const [data, setData] = React.useState(() => makeData(10_000))
const refreshData = () => setData(() => makeData(10_000))
const [data, setData] = React.useState(() => makeData(1_000))
const refreshData = () => setData(() => makeData(100_000)) //stress test with 100k rows

const table = useReactTable({
data,
columns,
data,
debugTable: true,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), //client-side sorting
onSortingChange: setSorting, //optionally control sorting state in your own scope for easy access
// sortingFns: {
// sortStatusFn, //or provide our custom sorting function globally for all columns to be able to use
// },
//no need to pass pageCount or rowCount with client-side pagination as it is calculated automatically
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
debugTable: true,
// autoResetPageIndex: false, // turn off page index reset when sorting or filtering - default on/true
// enableMultiSort: false, //Don't allow shift key to sort multiple columns - default on/true
// enableSorting: false, // - default on/true
// enableSortingRemoval: false, //Don't allow - default on/true
// isMultiSortEvent: (e) => true, //Make all clicks multi-sort - default requires `shift` key
// maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once - default is Infinity
})

//access sorting state from the table instance
console.log(table.getState().sorting)

return (
<div className="p-2">
<div className="h-2" />
Expand Down
12 changes: 7 additions & 5 deletions examples/react/sorting/src/makeData.ts
Expand Up @@ -2,11 +2,12 @@ import { faker } from '@faker-js/faker'

export type Person = {
firstName: string
lastName: string
lastName: string | undefined
age: number
visits: number
visits: number | undefined
progress: number
status: 'relationship' | 'complicated' | 'single'
rank: number
createdAt: Date
subRows?: Person[]
}
Expand All @@ -22,23 +23,24 @@ const range = (len: number) => {
const newPerson = (): Person => {
return {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
lastName: Math.random() < 0.1 ? undefined : faker.person.lastName(),
age: faker.number.int(40),
visits: faker.number.int(1000),
visits: Math.random() < 0.1 ? undefined : faker.number.int(1000),
progress: faker.number.int(100),
createdAt: faker.date.anytime(),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
rank: faker.number.int(100),
}
}

export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((d): Person => {
return range(len).map((_d): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
Expand Down
2 changes: 1 addition & 1 deletion packages/table-core/src/features/RowSorting.ts
Expand Up @@ -90,7 +90,7 @@ export interface SortingColumnDef<TData extends RowData> {
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/sorting#sortundefined)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/sorting)
*/
sortUndefined?: false | -1 | 1
sortUndefined?: false | -1 | 1 | 'first' | 'last'
}

export interface SortingColumn<TData extends RowData> {
Expand Down
11 changes: 7 additions & 4 deletions packages/table-core/src/utils/getSortedRowModel.ts
Expand Up @@ -25,7 +25,7 @@ export function getSortedRowModel<TData extends RowData>(): (
const columnInfoById: Record<
string,
{
sortUndefined?: false | -1 | 1
sortUndefined?: false | -1 | 1 | 'first' | 'last'
invertSorting?: boolean
sortingFn: SortingFn<TData>
}
Expand All @@ -51,25 +51,28 @@ export function getSortedRowModel<TData extends RowData>(): (
for (let i = 0; i < availableSorting.length; i += 1) {
const sortEntry = availableSorting[i]!
const columnInfo = columnInfoById[sortEntry.id]!
const sortUndefined = columnInfo.sortUndefined
const isDesc = sortEntry?.desc ?? false

let sortInt = 0

// All sorting ints should always return in ascending order
if (columnInfo.sortUndefined) {
if (sortUndefined) {
const aValue = rowA.getValue(sortEntry.id)
const bValue = rowB.getValue(sortEntry.id)

const aUndefined = aValue === undefined
const bUndefined = bValue === undefined

if (aUndefined || bUndefined) {
if (sortUndefined === 'first') return aUndefined ? -1 : 1
if (sortUndefined === 'last') return aUndefined ? 1 : -1
sortInt =
aUndefined && bUndefined
? 0
: aUndefined
? columnInfo.sortUndefined
: -columnInfo.sortUndefined
? sortUndefined
: -sortUndefined
}
}

Expand Down

0 comments on commit d2ae539

Please sign in to comment.