From 707a5b4f67e93ada09168f39798015e45da6bf38 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Tue, 19 Mar 2024 13:32:14 -0500 Subject: [PATCH] feat: allow custom features (#5415) * feat: initial refactor for allowing custom features * add example and docs for custom features * regen lock file * update link * proofread feature docs * add declaration merging caveat --- docs/api/core/table.md | 8 + docs/config.json | 8 + docs/guide/custom-features.md | 269 ++++++++++++ examples/react/basic/src/main.tsx | 2 +- examples/react/custom-features/.gitignore | 5 + examples/react/custom-features/README.md | 6 + examples/react/custom-features/index.html | 13 + examples/react/custom-features/package.json | 24 ++ examples/react/custom-features/src/index.css | 34 ++ examples/react/custom-features/src/main.tsx | 397 ++++++++++++++++++ .../react/custom-features/src/makeData.ts | 48 +++ examples/react/custom-features/tsconfig.json | 24 ++ examples/react/custom-features/vite.config.js | 17 + examples/react/filters/src/main.tsx | 18 +- examples/react/pagination/src/main.tsx | 15 +- packages/table-core/src/core/column.ts | 4 +- packages/table-core/src/core/headers.ts | 12 +- packages/table-core/src/core/row.ts | 2 +- packages/table-core/src/core/table.ts | 34 +- .../table-core/src/features/ColumnFaceting.ts | 3 +- .../src/features/ColumnFiltering.ts | 10 +- .../table-core/src/features/ColumnGrouping.ts | 10 +- .../table-core/src/features/ColumnOrdering.ts | 10 +- .../table-core/src/features/ColumnPinning.ts | 2 +- .../table-core/src/features/ColumnSizing.ts | 11 +- .../src/features/ColumnVisibility.ts | 2 +- .../src/features/GlobalFiltering.ts | 10 +- .../table-core/src/features/RowExpanding.ts | 10 +- .../table-core/src/features/RowPagination.ts | 10 +- .../table-core/src/features/RowPinning.ts | 10 +- .../table-core/src/features/RowSelection.ts | 11 +- .../table-core/src/features/RowSorting.ts | 2 +- packages/table-core/src/types.ts | 23 +- pnpm-lock.yaml | 37 +- 34 files changed, 1028 insertions(+), 73 deletions(-) create mode 100644 docs/guide/custom-features.md create mode 100644 examples/react/custom-features/.gitignore create mode 100644 examples/react/custom-features/README.md create mode 100644 examples/react/custom-features/index.html create mode 100644 examples/react/custom-features/package.json create mode 100644 examples/react/custom-features/src/index.css create mode 100644 examples/react/custom-features/src/main.tsx create mode 100644 examples/react/custom-features/src/makeData.ts create mode 100644 examples/react/custom-features/tsconfig.json create mode 100644 examples/react/custom-features/vite.config.js diff --git a/docs/api/core/table.md b/docs/api/core/table.md index 902465e665..400aed9047 100644 --- a/docs/api/core/table.md +++ b/docs/api/core/table.md @@ -168,6 +168,14 @@ debugRows?: boolean Set this option to true to output row debugging information to the console. +### `_features` + +```tsx +_features?: TableFeature[] +``` + +An array of extra features that you can add to the table instance. + ### `render` > ⚠️ This option is only necessary if you are implementing a table adapter. diff --git a/docs/config.json b/docs/config.json index 8b5e0bfd0d..1058f81d4b 100644 --- a/docs/config.json +++ b/docs/config.json @@ -175,6 +175,10 @@ { "label": "Virtualization", "to": "guide/virtualization" + }, + { + "label": "Custom Features", + "to": "guide/custom-features" } ] }, @@ -391,6 +395,10 @@ { "to": "framework/react/examples/full-width-resizable-table", "label": "React Full Width Resizable" + }, + { + "to": "framework/react/examples/custom-features", + "label": "Custom Features" } ] }, diff --git a/docs/guide/custom-features.md b/docs/guide/custom-features.md new file mode 100644 index 0000000000..04052af27a --- /dev/null +++ b/docs/guide/custom-features.md @@ -0,0 +1,269 @@ +--- +title: Custom Features Guide +--- + +## Examples + +Want to skip to the implementation? Check out these examples: + +- [custom-features](../framework/react/examples/custom-features) + +## Custom Features Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v8 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers where TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`useReactTable`, `useVueTable`, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrapper hooks around the `useReactTable` hook to extend the table instance with custom functionality. + +However, starting in version 8.14.0, TanStack Table has exposed a new `_features` table option that allows you to more tightly and cleanly integrate custom code into the table instance in exactly the same way that the built-in table features are already integrated. + +> TanStack Table v8.14.0 introduced a new `_features` option that allows you to add custom features to the table instance. + +With this new tighter integration, you can easily add more complex custom features to your tables, and possibly even package them up and share them with the community. We'll see how this evolves over time. In a future v9 release, we may even lower the bundle size of TanStack Table by making all features opt-in, but that is still being explored. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + createCell?: ( + cell: Cell, + column: Column, + row: Row, + table: Table + ) => void + createColumn?: (column: Column, table: Table) => void + createHeader?: (header: Header, table: Table) => void + createRow?: (row: Row, table: Table) => void + createTable?: (table: Table) => void + getDefaultColumnDef?: () => Partial> + getDefaultOptions?: ( + table: Table + ) => Partial> + getInitialState?: (initialState?: InitialTableState) => Partial +} +``` + +This might be a bit confusing, so let's break down what each of these methods do: + +#### Default Options and Initial State + +##### `getDefaultOptions` + +The `getDefaultOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/ColumnSizing.ts) feature, the `getDefaultOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +##### `getDefaultColumnDef` + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/RowSorting.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +##### `getInitialState` + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/RowPagination.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +##### `createTable` + +The `createTable` method in a table feature is responsible for adding methods to the `table` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/RowSelection.ts) feature, the `createTable` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `RowSelection` feature. + +##### `createHeader` + +The `createHeader` method in a table feature is responsible for adding methods to the `header` instance. For example, in the [Column Sizing](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/ColumnSizing.ts) feature, the `createHeader` method adds many header instance API methods such as `getStart`, and many others. So then, when you call `header.getStart()`, you are calling a method that was added to the header instance by the `ColumnSizing` feature. + +##### `createColumn` + +The `createColumn` method in a table feature is responsible for adding methods to the `column` instance. For example, in the [Sorting](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/RowSorting.ts) feature, the `createColumn` method adds many column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added to the column instance by the `RowSorting` feature. + +##### `createRow` + +The `createRow` method in a table feature is responsible for adding methods to the `row` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/RowSelection.ts) feature, the `createRow` method adds many row instance API methods such as `toggleSelected`, `getIsSelected`, etc. So then, when you call `row.toggleSelected()`, you are calling a method that was added to the row instance by the `RowSelection` feature. + +##### `createCell` + +The `createCell` method in a table feature is responsible for adding methods to the `cell` instance. For example, in the [Column Grouping](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/ColumnGrouping.ts) feature, the `createCell` method adds many cell instance API methods such as `getIsGrouped`, `getIsAggregated`, etc. So then, when you call `cell.getIsGrouped()`, you are calling a method that was added to the cell instance by the `ColumnGrouping` feature. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full [custom-features](../framework/react/examples/expanding) example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface DensityTableState { + density: DensityState +} + +// define types for our new feature's table options +export interface DensityOptions { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface DensityInstance { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Use Declaration Merging to Add New Types to TanStack Table + +We can tell TypeScript to modify the exported types from TanStack Table to include our new feature's types. This is called "declaration merging" and it's a powerful feature of TypeScript. This way, we should not have to use any TypeScript hacks such as `as unknown as CustomTable` or `// @ts-ignore` in our new feature's code or in our application code. + +```ts +// Use declaration merging to add our new feature APIs and state types to TanStack Table's existing types. +declare module '@tanstack/react-table' { // or whatever framework adapter you are using + //merge our new feature's state with the existing table state + interface TableState extends DensityTableState {} + //merge our new feature's options with the existing table options + interface TableOptionsResolved + extends DensityOptions {} + //merge our new feature's instance APIs with the existing table instance APIs + interface Table extends DensityInstance {} + // if you need to add cell instance APIs... + // interface Cell extends DensityCell + // if you need to add row instance APIs... + // interface Row extends DensityRow + // if you need to add column instance APIs... + // interface Column extends DensityColumn + // if you need to add header instance APIs... + // interface Header extends DensityHeader + + // Note: declaration merging on `ColumnDef` is not possible because it is a complex type, not an interface. + // But you can still use declaration merging on `ColumnDef.meta` +} +``` + +Once we do this correctly, we should have no TypeScript errors when we try to both create our new feature's code and use it in our application. + +##### Caveats of Using Declaration Merging + +One caveat of using declaration merging is that it will affect the TanStack Table types for every table across your codebase. This is not a problem if you plan on loading the same feature set for every table in your application, but it could be a problem if some of your tables load extra features and some do not. Alternatively, you can just make a bunch of custom types that extend off of the TanStack Table types with your new features added. This is what [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/types.ts) does in order to avoid affecting the types of vanilla TanStack Table tables, but it's a bit more tedious, and requires a lot of type casting at certain points. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const DensityFeature: TableFeature = { //Use the TableFeature type!! + // define the new feature's initial state + getInitialState: (state): DensityTableState => { + return { + density: 'md', + ...state, + } + }, + + // define the new feature's default options + getDefaultOptions: ( + table: Table + ): DensityOptions => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } as DensityOptions + }, + // if you need to add a default column definition... + // getDefaultColumnDef: (): Partial> => { + // return { meta: {} } //use meta instead of directly adding to the columnDef to avoid typescript stuff that's hard to workaround + // }, + + // define the new feature's table instance methods + createTable: (table: Table): void => { + table.setDensity = updater => { + const safeUpdater: Updater = old => { + let newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + } + table.toggleDensity = value => { + table.setDensity(old => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' //cycle through the 3 options + }) + } + }, + + // if you need to add row instance APIs... + // createRow: (row, table): void => {}, + // if you need to add cell instance APIs... + // createCell: (cell, column, row, table): void => {}, + // if you need to add column instance APIs... + // createColumn: (column, table): void => {}, + // if you need to add header instance APIs... + // createHeader: (header, table): void => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by passing it to the `_features` option when we create the table instance. + +```ts +const table = useReactTable({ + _features: [DensityFeature], //pass the new feature to merge with all of the built-in features under the hood + columns, + data, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs options, and state in your application. + +```tsx +const table = useReactTable({ + _features: [DensityFeature], //pass our custom feature to the table to be instantiated upon creation + columns, + data, + //... + state: { + density, //passing the density state to the table, TS is still happy :) + }, + onDensityChange: setDensity, //using the new onDensityChange option, TS is still happy :) +}) +//... +const { density } = table.getState() +return( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + +) +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `React.useState`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. \ No newline at end of file diff --git a/examples/react/basic/src/main.tsx b/examples/react/basic/src/main.tsx index 5f89f13846..c1f615525e 100644 --- a/examples/react/basic/src/main.tsx +++ b/examples/react/basic/src/main.tsx @@ -79,7 +79,7 @@ const columns = [ ] function App() { - const [data, setData] = React.useState(() => [...defaultData]) + const [data, _setData] = React.useState(() => [...defaultData]) const rerender = React.useReducer(() => ({}), {})[1] const table = useReactTable({ diff --git a/examples/react/custom-features/.gitignore b/examples/react/custom-features/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/examples/react/custom-features/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/react/custom-features/README.md b/examples/react/custom-features/README.md new file mode 100644 index 0000000000..b168d3c4b1 --- /dev/null +++ b/examples/react/custom-features/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm run start` or `yarn start` diff --git a/examples/react/custom-features/index.html b/examples/react/custom-features/index.html new file mode 100644 index 0000000000..3fc40c9367 --- /dev/null +++ b/examples/react/custom-features/index.html @@ -0,0 +1,13 @@ + + + + + + Vite App + + + +
+ + + diff --git a/examples/react/custom-features/package.json b/examples/react/custom-features/package.json new file mode 100644 index 0000000000..8190075540 --- /dev/null +++ b/examples/react/custom-features/package.json @@ -0,0 +1,24 @@ +{ + "name": "tanstack-table-example-custom-features", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@faker-js/faker": "^8.4.1", + "@tanstack/react-table": "^8.13.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@rollup/plugin-replace": "^5.0.5", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.11" + } +} diff --git a/examples/react/custom-features/src/index.css b/examples/react/custom-features/src/index.css new file mode 100644 index 0000000000..5747ffc905 --- /dev/null +++ b/examples/react/custom-features/src/index.css @@ -0,0 +1,34 @@ +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; +} + +tr { + border-bottom: 1px solid lightgray; +} + +button:disabled { + opacity: 0.5; +} diff --git a/examples/react/custom-features/src/main.tsx b/examples/react/custom-features/src/main.tsx new file mode 100644 index 0000000000..5bad334d38 --- /dev/null +++ b/examples/react/custom-features/src/main.tsx @@ -0,0 +1,397 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' + +import './index.css' + +import { + useReactTable, + makeStateUpdater, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + getCoreRowModel, + flexRender, + TableFeature, + Table, + RowData, + OnChangeFn, + ColumnDef, + Column, + Updater, + functionalUpdate, +} from '@tanstack/react-table' + +import { makeData, Person } from './makeData' + +// TypeScript setup for our new feature with all of the same type-safety as stock TanStack Table features + +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface DensityTableState { + density: DensityState +} + +// define types for our new feature's table options +export interface DensityOptions { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface DensityInstance { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} + +// Use declaration merging to add our new feature APIs and state types to TanStack Table's existing types. +declare module '@tanstack/react-table' { + //merge our new feature's state with the existing table state + interface TableState extends DensityTableState {} + //merge our new feature's options with the existing table options + interface TableOptionsResolved + extends DensityOptions {} + //merge our new feature's instance APIs with the existing table instance APIs + interface Table extends DensityInstance {} + // if you need to add cell instance APIs... + // interface Cell extends DensityCell + // if you need to add row instance APIs... + // interface Row extends DensityRow + // if you need to add column instance APIs... + // interface Column extends DensityColumn + // if you need to add header instance APIs... + // interface Header extends DensityHeader + + // Note: declaration merging on `ColumnDef` is not possible because it is a type, not an interface. + // But you can still use declaration merging on `ColumnDef.meta` +} + +// end of TS setup! + +// Here is all of the actual javascript code for our new feature +export const DensityFeature: TableFeature = { + // define the new feature's initial state + getInitialState: (state): DensityTableState => { + return { + density: 'md', + ...state, + } + }, + + // define the new feature's default options + getDefaultOptions: ( + table: Table + ): DensityOptions => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } as DensityOptions + }, + // if you need to add a default column definition... + // getDefaultColumnDef: (): Partial> => { + // return { meta: {} } //use meta instead of directly adding to the columnDef to avoid typescript stuff that's hard to workaround + // }, + + // define the new feature's table instance methods + createTable: (table: Table): void => { + table.setDensity = updater => { + const safeUpdater: Updater = old => { + let newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + } + table.toggleDensity = value => { + table.setDensity(old => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' //cycle through the 3 options + }) + } + }, + + // if you need to add row instance APIs... + // createRow: (row, table): void => {}, + // if you need to add cell instance APIs... + // createCell: (cell, column, row, table): void => {}, + // if you need to add column instance APIs... + // createColumn: (column, table): void => {}, + // if you need to add header instance APIs... + // createHeader: (header, table): void => {}, +} +//end of custom feature code + +//app code +function App() { + const columns = React.useMemo[]>( + () => [ + { + accessorKey: 'firstName', + cell: info => info.getValue(), + footer: props => props.column.id, + }, + { + accessorFn: row => row.lastName, + id: 'lastName', + cell: info => info.getValue(), + header: () => Last Name, + footer: props => props.column.id, + }, + { + accessorKey: 'age', + header: () => 'Age', + footer: props => props.column.id, + }, + { + accessorKey: 'visits', + header: () => Visits, + footer: props => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: props => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: props => props.column.id, + }, + ], + [] + ) + + const [data, _setData] = React.useState(() => makeData(1000)) + const [density, setDensity] = React.useState('md') + + const table = useReactTable({ + _features: [DensityFeature], //pass our custom feature to the table to be instantiated upon creation + columns, + data, + debugTable: true, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + state: { + density, //passing the density state to the table, TS is still happy :) + }, + onDensityChange: setDensity, //using the new onDensityChange option, TS is still happy :) + }) + + return ( +
+
+ + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => { + return ( + + ) + })} + + ) + })} + +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} +
+ {header.column.getCanFilter() ? ( +
+ +
+ ) : null} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+
+ + + + + +
Page
+ + {table.getState().pagination.pageIndex + 1} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = e.target.value ? Number(e.target.value) - 1 : 0 + table.setPageIndex(page) + }} + className="border p-1 rounded w-16" + /> + + +
+
+ Showing {table.getRowModel().rows.length.toLocaleString()} of{' '} + {table.getRowCount().toLocaleString()} Rows +
+
{JSON.stringify(table.getState().pagination, null, 2)}
+
+ ) +} + +function Filter({ + column, + table, +}: { + column: Column + table: Table +}) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + + const columnFilterValue = column.getFilterValue() + + return typeof firstValue === 'number' ? ( +
+ + column.setFilterValue((old: [number, number]) => [ + e.target.value, + old?.[1], + ]) + } + placeholder={`Min`} + className="w-24 border shadow rounded" + /> + + column.setFilterValue((old: [number, number]) => [ + old?.[0], + e.target.value, + ]) + } + placeholder={`Max`} + className="w-24 border shadow rounded" + /> +
+ ) : ( + column.setFilterValue(e.target.value)} + placeholder={`Search...`} + className="w-36 border shadow rounded" + /> + ) +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +ReactDOM.createRoot(rootElement).render( + + + +) diff --git a/examples/react/custom-features/src/makeData.ts b/examples/react/custom-features/src/makeData.ts new file mode 100644 index 0000000000..331dd1eb19 --- /dev/null +++ b/examples/react/custom-features/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + 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([ + 'relationship', + 'complicated', + 'single', + ])[0]!, + } +} + +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() +} diff --git a/examples/react/custom-features/tsconfig.json b/examples/react/custom-features/tsconfig.json new file mode 100644 index 0000000000..6d545f543f --- /dev/null +++ b/examples/react/custom-features/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/custom-features/vite.config.js b/examples/react/custom-features/vite.config.js new file mode 100644 index 0000000000..2e1361723a --- /dev/null +++ b/examples/react/custom-features/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +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'), + }, + }), + react(), + ], +}) diff --git a/examples/react/filters/src/main.tsx b/examples/react/filters/src/main.tsx index 8e79248919..8af956ff62 100644 --- a/examples/react/filters/src/main.tsx +++ b/examples/react/filters/src/main.tsx @@ -5,21 +5,21 @@ import './index.css' import { Column, - Table, - useReactTable, + ColumnDef, ColumnFiltersState, + FilterFn, + SortingFn, + Table, + flexRender, getCoreRowModel, - getFilteredRowModel, + getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, - getFacetedMinMaxValues, + getFilteredRowModel, getPaginationRowModel, - sortingFns, getSortedRowModel, - FilterFn, - SortingFn, - ColumnDef, - flexRender, + sortingFns, + useReactTable, } from '@tanstack/react-table' import { diff --git a/examples/react/pagination/src/main.tsx b/examples/react/pagination/src/main.tsx index 39e4163413..700672575b 100644 --- a/examples/react/pagination/src/main.tsx +++ b/examples/react/pagination/src/main.tsx @@ -5,15 +5,15 @@ import './index.css' import { Column, - Table as ReactTable, + ColumnDef, PaginationState, - useReactTable, + Table, + flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, - ColumnDef, - flexRender, getSortedRowModel, + useReactTable, } from '@tanstack/react-table' import { makeData, Person } from './makeData' @@ -64,7 +64,7 @@ function App() { return ( <> - ) } + function Filter({ column, table, }: { column: Column - table: ReactTable + table: Table }) { const firstValue = table .getPreFilteredRowModel() diff --git a/packages/table-core/src/core/column.ts b/packages/table-core/src/core/column.ts index 1d6d57e9ff..b725db57db 100644 --- a/packages/table-core/src/core/column.ts +++ b/packages/table-core/src/core/column.ts @@ -157,9 +157,9 @@ export function createColumn( } for (const feature of table._features) { - feature.createColumn?.(column, table) + feature.createColumn?.(column as Column, table) } - // Yes, we have to convert table to uknown, because we know more than the compiler here. + // Yes, we have to convert table to unknown, because we know more than the compiler here. return column as Column } diff --git a/packages/table-core/src/core/headers.ts b/packages/table-core/src/core/headers.ts index 448ffc1e59..263da8d29d 100644 --- a/packages/table-core/src/core/headers.ts +++ b/packages/table-core/src/core/headers.ts @@ -1,6 +1,12 @@ -import { RowData, Column, Header, HeaderGroup, Table } from '../types' +import { + RowData, + Column, + Header, + HeaderGroup, + Table, + TableFeature, +} from '../types' import { getMemoOptions, memo } from '../utils' -import { TableFeature } from './table' const debug = 'debugHeaders' @@ -250,7 +256,7 @@ function createHeader( } table._features.forEach(feature => { - feature.createHeader?.(header, table) + feature.createHeader?.(header as Header, table) }) return header as Header diff --git a/packages/table-core/src/core/row.ts b/packages/table-core/src/core/row.ts index 3a752897fd..0af28aa94c 100644 --- a/packages/table-core/src/core/row.ts +++ b/packages/table-core/src/core/row.ts @@ -194,7 +194,7 @@ export const createRow = ( for (let i = 0; i < table._features.length; i++) { const feature = table._features[i] - feature?.createRow?.(row, table) + feature?.createRow?.(row as Row, table) } return row as Row diff --git a/packages/table-core/src/core/table.ts b/packages/table-core/src/core/table.ts index dcc58eb6f2..4a6ef3d954 100644 --- a/packages/table-core/src/core/table.ts +++ b/packages/table-core/src/core/table.ts @@ -15,6 +15,7 @@ import { TableMeta, ColumnDefResolved, GroupColumnDef, + TableFeature, } from '../types' // @@ -36,18 +37,7 @@ import { RowPinning } from '../features/RowPinning' import { RowSelection } from '../features/RowSelection' import { RowSorting } from '../features/RowSorting' -export interface TableFeature { - createCell?: (cell: any, column: any, row: any, table: any) => any - createColumn?: (column: any, table: any) => any - createHeader?: (column: any, table: any) => any - createRow?: (row: any, table: any) => any - createTable?: (table: any) => any - getDefaultColumnDef?: () => any - getDefaultOptions?: (table: any) => any - getInitialState?: (initialState?: InitialTableState) => any -} - -const features = [ +const builtInFeatures = [ Headers, ColumnVisibility, ColumnOrdering, @@ -69,6 +59,12 @@ const features = [ export interface CoreTableState {} export interface CoreOptions { + /** + * An array of extra features that you can add to the table instance. + * @link [API Docs](https://tanstack.com/table/v8/docs/api/core/table#_features) + * @link [Guide](https://tanstack.com/table/v8/docs/guide/tables) + */ + _features?: TableFeature[] /** * Set this option to override any of the `autoReset...` feature options. * @link [API Docs](https://tanstack.com/table/v8/docs/api/core/table#autoresetall) @@ -285,11 +281,16 @@ export interface CoreInstance { export function createTable( options: TableOptionsResolved ): Table { - if (options.debugAll || options.debugTable) { + if ( + process.env.NODE_ENV !== 'production' && + (options.debugAll || options.debugTable) + ) { console.info('Creating Table Instance...') } - let table = { _features: features } as unknown as Table + const _features = [...builtInFeatures, ...(options._features ?? [])] + + let table = { _features } as unknown as Table const defaultOptions = table._features.reduce((obj, feature) => { return Object.assign(obj, feature.getDefaultOptions?.(table)) @@ -314,14 +315,15 @@ export function createTable( } as TableState table._features.forEach(feature => { - initialState = feature.getInitialState?.(initialState) ?? initialState + initialState = (feature.getInitialState?.(initialState) ?? + initialState) as TableState }) const queued: (() => void)[] = [] let queuedTimeout = false const coreInstance: CoreInstance = { - _features: features, + _features, options: { ...defaultOptions, ...options, diff --git a/packages/table-core/src/features/ColumnFaceting.ts b/packages/table-core/src/features/ColumnFaceting.ts index 86af46b731..e5fe235982 100644 --- a/packages/table-core/src/features/ColumnFaceting.ts +++ b/packages/table-core/src/features/ColumnFaceting.ts @@ -1,6 +1,5 @@ import { RowModel } from '..' -import { TableFeature } from '../core/table' -import { Column, Table, RowData } from '../types' +import { Column, RowData, Table, TableFeature } from '../types' export interface FacetedColumn { _getFacetedMinMaxValues?: () => undefined | [number, number] diff --git a/packages/table-core/src/features/ColumnFiltering.ts b/packages/table-core/src/features/ColumnFiltering.ts index eb140dabe3..0616124733 100644 --- a/packages/table-core/src/features/ColumnFiltering.ts +++ b/packages/table-core/src/features/ColumnFiltering.ts @@ -1,15 +1,15 @@ import { RowModel } from '..' -import { TableFeature } from '../core/table' import { BuiltInFilterFn, filterFns } from '../filterFns' import { Column, + FilterFns, + FilterMeta, OnChangeFn, - Table, Row, - Updater, RowData, - FilterMeta, - FilterFns, + Table, + TableFeature, + Updater, } from '../types' import { functionalUpdate, isFunction, makeStateUpdater } from '../utils' diff --git a/packages/table-core/src/features/ColumnGrouping.ts b/packages/table-core/src/features/ColumnGrouping.ts index 0d34bb6373..973ee8b355 100644 --- a/packages/table-core/src/features/ColumnGrouping.ts +++ b/packages/table-core/src/features/ColumnGrouping.ts @@ -1,16 +1,16 @@ import { RowModel } from '..' import { BuiltInAggregationFn, aggregationFns } from '../aggregationFns' -import { TableFeature } from '../core/table' import { + AggregationFns, Cell, Column, + ColumnDefTemplate, OnChangeFn, - Table, Row, - Updater, - ColumnDefTemplate, RowData, - AggregationFns, + Table, + TableFeature, + Updater, } from '../types' import { isFunction, makeStateUpdater } from '../utils' diff --git a/packages/table-core/src/features/ColumnOrdering.ts b/packages/table-core/src/features/ColumnOrdering.ts index 555ae23e18..c64370ef15 100644 --- a/packages/table-core/src/features/ColumnOrdering.ts +++ b/packages/table-core/src/features/ColumnOrdering.ts @@ -1,9 +1,15 @@ import { getMemoOptions, makeStateUpdater, memo } from '../utils' -import { Table, OnChangeFn, Updater, Column, RowData } from '../types' +import { + Column, + OnChangeFn, + RowData, + Table, + TableFeature, + Updater, +} from '../types' import { orderColumns } from './ColumnGrouping' -import { TableFeature } from '../core/table' import { ColumnPinningPosition, _getVisibleLeafColumns } from '..' export interface ColumnOrderTableState { diff --git a/packages/table-core/src/features/ColumnPinning.ts b/packages/table-core/src/features/ColumnPinning.ts index 2141807fdf..1c0ef70799 100644 --- a/packages/table-core/src/features/ColumnPinning.ts +++ b/packages/table-core/src/features/ColumnPinning.ts @@ -1,4 +1,3 @@ -import { TableFeature } from '../core/table' import { OnChangeFn, Updater, @@ -7,6 +6,7 @@ import { Row, Cell, RowData, + TableFeature, } from '../types' import { getMemoOptions, makeStateUpdater, memo } from '../utils' diff --git a/packages/table-core/src/features/ColumnSizing.ts b/packages/table-core/src/features/ColumnSizing.ts index 468c039d78..3cce2ddcb2 100644 --- a/packages/table-core/src/features/ColumnSizing.ts +++ b/packages/table-core/src/features/ColumnSizing.ts @@ -1,6 +1,13 @@ import { _getVisibleLeafColumns } from '..' -import { TableFeature } from '../core/table' -import { RowData, Column, Header, OnChangeFn, Table, Updater } from '../types' +import { + RowData, + Column, + Header, + OnChangeFn, + Table, + Updater, + TableFeature, +} from '../types' import { getMemoOptions, makeStateUpdater, memo } from '../utils' import { ColumnPinningPosition } from './ColumnPinning' diff --git a/packages/table-core/src/features/ColumnVisibility.ts b/packages/table-core/src/features/ColumnVisibility.ts index 5fbd542891..f37e57be8e 100644 --- a/packages/table-core/src/features/ColumnVisibility.ts +++ b/packages/table-core/src/features/ColumnVisibility.ts @@ -1,5 +1,4 @@ import { ColumnPinningPosition } from '..' -import { TableFeature } from '../core/table' import { Cell, Column, @@ -8,6 +7,7 @@ import { Updater, Row, RowData, + TableFeature, } from '../types' import { getMemoOptions, makeStateUpdater, memo } from '../utils' diff --git a/packages/table-core/src/features/GlobalFiltering.ts b/packages/table-core/src/features/GlobalFiltering.ts index 236555e21c..c5ef77a4e1 100644 --- a/packages/table-core/src/features/GlobalFiltering.ts +++ b/packages/table-core/src/features/GlobalFiltering.ts @@ -1,7 +1,13 @@ import { FilterFn, FilterFnOption, RowModel } from '..' -import { TableFeature } from '../core/table' import { BuiltInFilterFn, filterFns } from '../filterFns' -import { Column, OnChangeFn, Table, Updater, RowData } from '../types' +import { + Column, + OnChangeFn, + Table, + Updater, + RowData, + TableFeature, +} from '../types' import { isFunction, makeStateUpdater } from '../utils' export interface GlobalFilterTableState { diff --git a/packages/table-core/src/features/RowExpanding.ts b/packages/table-core/src/features/RowExpanding.ts index 478c4db022..15da45e0ea 100644 --- a/packages/table-core/src/features/RowExpanding.ts +++ b/packages/table-core/src/features/RowExpanding.ts @@ -1,6 +1,12 @@ import { RowModel } from '..' -import { TableFeature } from '../core/table' -import { OnChangeFn, Table, Row, Updater, RowData } from '../types' +import { + OnChangeFn, + Table, + Row, + Updater, + RowData, + TableFeature, +} from '../types' import { makeStateUpdater } from '../utils' export type ExpandedStateList = Record diff --git a/packages/table-core/src/features/RowPagination.ts b/packages/table-core/src/features/RowPagination.ts index 3779661519..836470673b 100644 --- a/packages/table-core/src/features/RowPagination.ts +++ b/packages/table-core/src/features/RowPagination.ts @@ -1,5 +1,11 @@ -import { TableFeature } from '../core/table' -import { OnChangeFn, Table, RowModel, Updater, RowData } from '../types' +import { + OnChangeFn, + Table, + RowModel, + Updater, + RowData, + TableFeature, +} from '../types' import { functionalUpdate, getMemoOptions, diff --git a/packages/table-core/src/features/RowPinning.ts b/packages/table-core/src/features/RowPinning.ts index 8df517819b..3525111de0 100644 --- a/packages/table-core/src/features/RowPinning.ts +++ b/packages/table-core/src/features/RowPinning.ts @@ -1,5 +1,11 @@ -import { TableFeature } from '../core/table' -import { OnChangeFn, Updater, Table, Row, RowData } from '../types' +import { + OnChangeFn, + Updater, + Table, + Row, + RowData, + TableFeature, +} from '../types' import { getMemoOptions, makeStateUpdater, memo } from '../utils' export type RowPinningPosition = false | 'top' | 'bottom' diff --git a/packages/table-core/src/features/RowSelection.ts b/packages/table-core/src/features/RowSelection.ts index 4503081c2a..90166823aa 100644 --- a/packages/table-core/src/features/RowSelection.ts +++ b/packages/table-core/src/features/RowSelection.ts @@ -1,5 +1,12 @@ -import { TableFeature } from '../core/table' -import { OnChangeFn, Table, Row, RowModel, Updater, RowData } from '../types' +import { + OnChangeFn, + Table, + Row, + RowModel, + Updater, + RowData, + TableFeature, +} from '../types' import { getMemoOptions, makeStateUpdater, memo } from '../utils' export type RowSelectionState = Record diff --git a/packages/table-core/src/features/RowSorting.ts b/packages/table-core/src/features/RowSorting.ts index 0f0c0648d7..3d127b9a10 100644 --- a/packages/table-core/src/features/RowSorting.ts +++ b/packages/table-core/src/features/RowSorting.ts @@ -1,5 +1,4 @@ import { RowModel } from '..' -import { TableFeature } from '../core/table' import { BuiltInSortingFn, reSplitAlphaNumeric, @@ -14,6 +13,7 @@ import { Updater, RowData, SortingFns, + TableFeature, } from '../types' import { isFunction, makeStateUpdater } from '../utils' diff --git a/packages/table-core/src/types.ts b/packages/table-core/src/types.ts index 9b20fc423b..a1cf9c9886 100644 --- a/packages/table-core/src/types.ts +++ b/packages/table-core/src/types.ts @@ -96,6 +96,24 @@ import { PartialKeys, UnionToIntersection } from './utils' import { CellContext, CoreCell } from './core/cell' import { CoreColumn } from './core/column' +export interface TableFeature { + createCell?: ( + cell: Cell, + column: Column, + row: Row, + table: Table + ) => void + createColumn?: (column: Column, table: Table) => void + createHeader?: (header: Header, table: Table) => void + createRow?: (row: Row, table: Table) => void + createTable?: (table: Table) => void + getDefaultColumnDef?: () => Partial> + getDefaultOptions?: ( + table: Table + ) => Partial> + getInitialState?: (initialState?: InitialTableState) => Partial +} + export interface TableMeta {} export interface ColumnMeta {} @@ -146,8 +164,9 @@ interface FeatureOptions PaginationOptions, RowSelectionOptions {} -export type TableOptionsResolved = CoreOptions & - FeatureOptions +export interface TableOptionsResolved + extends CoreOptions, + FeatureOptions {} export interface TableOptions extends PartialKeys< diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2185d645d9..d175d05e29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -436,6 +436,37 @@ importers: specifier: ^5.0.11 version: 5.0.11(@types/node@18.19.7) + examples/react/custom-features: + dependencies: + '@faker-js/faker': + specifier: ^8.4.1 + version: 8.4.1 + '@tanstack/react-table': + specifier: ^8.13.2 + version: link:../../../packages/react-table + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@rollup/plugin-replace': + specifier: ^5.0.5 + version: 5.0.5(rollup@4.9.5) + '@types/react': + specifier: ^18.2.48 + version: 18.2.48 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.2.18 + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.2.1(vite@5.0.11) + vite: + specifier: ^5.0.11 + version: 5.0.11(@types/node@18.19.7) + examples/react/editable-data: dependencies: '@faker-js/faker': @@ -10121,9 +10152,9 @@ packages: optional: true dependencies: '@types/node': 20.11.2 - esbuild: 0.19.10 - postcss: 8.4.32 - rollup: 4.9.2 + esbuild: 0.19.12 + postcss: 8.4.33 + rollup: 4.9.5 optionalDependencies: fsevents: 2.3.3 dev: true