Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,10 @@
{
"to": "framework/vanilla/examples/pagination",
"label": "Pagination"
},
{
"to": "framework/vanilla/examples/sorting",
"label": "Sorting"
}
]
}
Expand Down
5 changes: 5 additions & 0 deletions examples/vanilla/sorting/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
6 changes: 6 additions & 0 deletions examples/vanilla/sorting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install` or `yarn`
- `npm run start` or `yarn start`
15 changes: 15 additions & 0 deletions examples/vanilla/sorting/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
<script type="module" src="https://cdn.skypack.dev/twind/shim"></script>
</head>
<body>
<div id="root" class="p-2">
<div id="wrapper"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/vanilla/sorting/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "tanstack-table-example-vanilla-sorting",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"start": "vite"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@rollup/plugin-replace": "^5.0.7",
"typescript": "5.4.5",
"vite": "^5.3.2"
},
"dependencies": {
"@tanstack/table-core": "^8.20.5",
"nanostores": "^0.11.3"
}
}
30 changes: 30 additions & 0 deletions examples/vanilla/sorting/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
html {
font-family: sans-serif;
font-size: 14px;
}

table {
border: 1px solid lightgray;
}

tbody {
border-bottom: 1px solid lightgray;
}

th {
border-bottom: 1px solid lightgray;
border-right: 1px solid lightgray;
padding: 2px 4px;
}

tfoot {
color: gray;
}

tfoot th {
font-weight: normal;
}

button:disabled {
opacity: 0.5;
}
142 changes: 142 additions & 0 deletions examples/vanilla/sorting/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import './index.css'

import {
type SortingFn,
createColumnHelper,
getCoreRowModel,
getSortedRowModel,
} from '@tanstack/table-core'

import { makeData, Person } from './makeData'
import { flexRender, useTable } from './useTable'

const data = makeData(1000)

// 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)
}

const columnHelper = createColumnHelper<Person>()

const columns = [
columnHelper.accessor('firstName', {
cell: info => info.getValue(),
// This column will sort in ascending order by default since it is a string column
}),
columnHelper.accessor(row => row.lastName, {
id: 'lastName',
cell: info => `<i>${info.getValue()}</i>`,
header: () => '<span>Last Name</span>',
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)
}),
columnHelper.accessor('age', {
header: () => 'Age',
cell: info => info.renderValue(),
// This column will sort in descending order by default since it is a number column
}),
columnHelper.accessor('visits', {
header: () => '<span>Visits</span>',
sortUndefined: 'last', // Force undefined values to the end
}),
columnHelper.accessor('status', {
header: 'Status',
sortingFn: sortStatusFn, // Use our custom sorting function for this enum column
}),
columnHelper.accessor('progress', {
header: 'Profile Progress',
enableSorting: false, // Disable sorting for this column
}),
columnHelper.accessor('rank', {
header: 'Rank',
invertSorting: true, // Invert the sorting order (golf score-like where smaller is better)
}),
columnHelper.accessor('createdAt', {
header: 'Created At',
}),
]

const renderTable = () => {
// Create table elements
const tableElement = document.createElement('table')
const theadElement = document.createElement('thead')
const tbodyElement = document.createElement('tbody')

tableElement.classList.add('mb-2')

tableElement.appendChild(theadElement)
tableElement.appendChild(tbodyElement)

// Render table headers
table.getHeaderGroups().forEach(headerGroup => {
const trElement = document.createElement('tr')
headerGroup.headers.forEach(header => {
const thElement = document.createElement('th')
thElement.colSpan = header.colSpan
const divElement = document.createElement('div')
divElement.classList.add(
'w-36',
...(header.column.getCanSort() ? ['cursor-pointer', 'select-none'] : [])
)
;(divElement.onclick = e => header.column.getToggleSortingHandler()?.(e)),
(divElement.innerHTML = header.isPlaceholder
? ''
: flexRender(header.column.columnDef.header, header.getContext()))
divElement.innerHTML +=
{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? ''
thElement.appendChild(divElement)
trElement.appendChild(thElement)
})
theadElement.appendChild(trElement)
})

// Render table rows
table
.getRowModel()
.rows.slice(0, 10)
.forEach(row => {
const trElement = document.createElement('tr')
row.getVisibleCells().forEach(cell => {
const tdElement = document.createElement('td')
tdElement.innerHTML = flexRender(
cell.column.columnDef.cell,
cell.getContext()
)
trElement.appendChild(tdElement)
})
tbodyElement.appendChild(trElement)
})

// Render table state info
const stateInfoElement = document.createElement('pre')
stateInfoElement.textContent = JSON.stringify(
{
sorting: table.getState().sorting,
},
null,
2
)

// Clear previous content and append new content
const wrapperElement = document.getElementById('wrapper') as HTMLDivElement
wrapperElement.innerHTML = ''
wrapperElement.appendChild(tableElement)
wrapperElement.appendChild(stateInfoElement)
}

const table = useTable<Person>({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onStateChange: () => renderTable(),
})

renderTable()
52 changes: 52 additions & 0 deletions examples/vanilla/sorting/src/makeData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { faker } from '@faker-js/faker'

export type Person = {
firstName: string
lastName: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
rank: number
createdAt: Date
subRows?: Person[]
}

const range = (len: number) => {
const arr: number[] = []
for (let i = 0; i < len; i++) {
arr.push(i)
}
return arr
}

const newPerson = (): Person => {
return {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
rank: faker.number.int(100),
createdAt: faker.date.anytime(),
}
}

export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((d): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}

return makeDataLevel()
}
57 changes: 57 additions & 0 deletions examples/vanilla/sorting/src/useTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { atom } from 'nanostores'

import {
type RowData,
type TableOptions,
type TableOptionsResolved,
createTable,
} from '@tanstack/table-core'

export const flexRender = <TProps extends object>(comp: any, props: TProps) => {
if (typeof comp === 'function') {
return comp(props)
}
return comp
}

export const useTable = <TData extends RowData>(
options: TableOptions<TData>
) => {
// Compose in the generic options to the user options
const resolvedOptions: TableOptionsResolved<TData> = {
state: {}, // Dummy state
onStateChange: () => {}, // noop
renderFallbackValue: null,
...options,
}

// Create a new table
const table = createTable<TData>(resolvedOptions)

// By default, manage table state here using the table's initial state
const state = atom(table.initialState)

// Subscribe to state changes
state.subscribe(currentState => {
table.setOptions(prev => ({
...prev,
...options,
state: {
...currentState,
...options.state,
},
// Similarly, we'll maintain both our internal state and any user-provided state
onStateChange: updater => {
if (typeof updater === 'function') {
const newState = updater(currentState)
state.set(newState)
} else {
state.set(updater)
}
options.onStateChange?.(updater)
},
}))
})

return table
}
26 changes: 26 additions & 0 deletions examples/vanilla/sorting/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"emitDecoratorMetadata": true,
"noEmit": true,
"jsx": "react-jsx",
"experimentalDecorators": true,
"useDefineForClassFields": false,

/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
15 changes: 15 additions & 0 deletions examples/vanilla/sorting/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import rollupReplace from '@rollup/plugin-replace'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
rollupReplace({
preventAssignment: true,
values: {
__DEV__: JSON.stringify(true),
'process.env.NODE_ENV': JSON.stringify('development'),
},
}),
],
})
Loading
Loading