# Infinite Table Documentation > Developer Documentation for Infinite Table, your go-to React DataGrid component to handle huge amounts of data Canonical page: https://infinite-table.com/docs/ ## What is Infinite Table? Infinite Table is a React DataGrid component for displaying virtualized tabular data. It helps you display huge datasets and get the most out of your data by providing you the right tools to enjoy these features: - [ sorting](https://infinite-table.com/docs/learn/sorting/overview.md) - [ row grouping](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md) - both server-side and client-side - [ pivoting](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md) - both server-side and client-side - [ aggregations](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md#aggregations) - [ live pagination](https://infinite-table.com/docs/learn/working-with-data/live-pagination.md) - [ lazy loading](https://infinite-table.com/docs/learn/working-with-data/lazy-loading.md) - [ keyboard navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells.md) - [ fixed and flexible columns](https://infinite-table.com/docs/learn/columns/fixed-and-flexible-size.md) - [ column grouping](https://infinite-table.com/docs/learn/columns/column-grouping.md) - [ filtering](https://infinite-table.com/docs/learn/filtering/index.md) - [ theming](https://infinite-table.com/docs/learn/theming/index.md) ## Installation Installation could not be more straightforward - just one npm command: npm i @infinite-table/infinite-react ## ❤️ TypeScript Infinite Table is fully typed and offers you a great developer experience, to help you get up and running quickly. > The TypeScript typings file is included in the npm package - you don't have to download an additional **@types** package Read more about how to use our TypeScript types ## 📄 Extensive Documentation We're aware good documentation is a must and are updating our documentation as we add new features. Head to [our getting started](https://infinite-table.com/docs/learn/getting-started/index.md) guide to get up and running quickly. ## 🏢 Enterprise-Ready Infinite Table is ready to power your enterprise apps, as it supports advanced [data fetching](https://infinite-table.com/docs/learn/working-with-data/index.md#data-loading-strategies), [filtering](https://infinite-table.com/docs/learn/filtering/index.md), [sorting](https://infinite-table.com/docs/learn/sorting/overview.md), [grouping](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md), [pivoting](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md), [aggregations](https://infinite-table.com/docs/learn/grouping-and-pivoting/group-aggregations.md), [live pagination](https://infinite-table.com/docs/learn/working-with-data/live-pagination.md), [lazy loading](https://infinite-table.com/docs/learn/working-with-data/lazy-loading.md) - all of those with support for both client-side and server-side implementations. You can choose to leverage our built-in implementations in the browser, or you can process your data on the server with full support from our-side. ### 🔒 Secure by Default We take security seriously and only have a total of 3 dependencies in our full dependency graph - and this number will only go down. ### 📦 Small Bundle Size Our bundle size is under `300kB` and we're dedicated to [keeping it small](https://bundlephobia.com/package/@infinite-table/infinite-react). See our bundle size in BundlePhobia ### 🧪 Automated End-to-End Tests Our releases are automated and, we have full end-to-end tests that ensure we're delivering to our standards. Real-browser tests help us move with confidence and continue to ship great features. Check out our end-to-end tests in GitHub ## 🎨 Themable `Infinite Table` is fully customizable, via CSS variables. It ships with both a **light** and a **dark** theme - all you have to do is import the CSS file from the package. ```ts import '@infinite-table/infinite-react/index.css'; // This file includes both the light and the dark themes. ``` Read how to use themes and **CSS variables** to customize every aspect of Infinite Table --- # Infinite Table DevTools > Guide on using the Chrome DevTools Extension for the Infinite Table React DataGrid Canonical page: https://infinite-table.com/docs/devtools We're happy to announce that [Infinite Table DevTools extension](https://chromewebstore.google.com/detail/infinite-table-devtools-e/jpipjljbfffijmgiecljadbogfegejfa) is now live - [install it here!](https://chromewebstore.google.com/detail/infinite-table-devtools-e/jpipjljbfffijmgiecljadbogfegejfa) To see the extension on a live demo, head to the [chrome webstore](https://chromewebstore.google.com/detail/infinite-table-devtools-e/jpipjljbfffijmgiecljadbogfegejfa) to download the extension. Then visit [our live demo page](https://infinite-table.com/full-demo) and open your browser devtools - you should see the "Infinite Table" devtool tab. Click it and enjoy interacting with the DataGrid! To see an Infinite Table instance in the devtools, specify the [`debugId`](https://infinite-table.com/docs/reference/infinite-table-props.md#debugId) prop. ```tsx {2} ``` Infinite Table is the first DataGrid with a Chrome DevTools extension. Starting with version `7.0.0` of Infinite, you can specify the `debugId` property on the `` instance and it will be picked up by the devtools. ```tsx {16} const columns = { name: { field: 'firstName', }, lastName: { field: 'lastName', }, age: { field: 'age', }, } const App = () => { return } ``` If you have multiple instances, each with a unique `debugId` property, they will all show up Infinite Table DevTools Extension --- # Working with Columns > Define columns to configure your Infinite Table React DataGrid - fixed and flexible columns, resize, column groups and more Canonical page: https://infinite-table.com/docs/learn/columns/ Columns are a central feature in `InfiniteTable`. You define columns as a an object, with keys being column ids while values are the column definitions. You then use them in the `columns` prop in your `InfiniteTable` component. The [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) prop is typed either as - `Record>` - or `InfiniteTablePropColumns`, which is an alias for the type above In `InfiniteTable`, columns are identified by their key in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) object. **We'll refer to this as the column id**. The column ids are used in many places - like defining the [column order](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder), column pinning, column visibility, etc. ```ts export type Employee = { id: number; companyName: string; firstName: string; lastName: string; country: string; city: string; department: string; team: string; salary: number; }; // InfiniteTableColumn is a generic type, you have to bind it to a specific data-type import { InfiniteTableColumn } from '@infinite-table/infinite-react'; // we're binding it here to the `Employee` type // which means the `column.field` has to be `keyof Employee` export const columns: Record> = { 'firstName': { field: 'firstName', header: 'First Name', }, 'country': { field: 'country', }, 'city': { field: 'city' }, 'salary': { field: 'salary', type: 'number' }, } ``` It's very important to remember you should not pass a different reference of a prop on each render. `` is a optimized to only re-render when props change - so if you change the props on every re-render you will get a performance penalty. You should use `React.useCallback` / `React.useMemo` / `React.useState` to make sure you only update the props you pass down to `InfiniteTable` when you have to. **Example: Basic Column Configuration** ```ts import { InfiniteTable, DataSource, type InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, }; export default function App() { return ( data={dataSource} primaryKey="id"> debugId="basic-columns-example" columns={columns} columnDefaultWidth={200} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; ``` Find out how to render custom content inside columns or even take full control of column cells and header. ## Column Types Column types allow you to customize column behavior and appearance for multiple columns at once. Most of the properties available for columns are also available for column types - for a full list, see [columnTypes](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) reference. There are two special [column types](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) for now, but more are coming soon: - `default` - all columns have this type, if not otherwise specified. The type does not contain any configuration, but allows you to define it and apply common configuration to all columns. - `number` - if specified on a column (in combination with local uncontrolled sorting), the column will be sorted numerically. Find out how to use column types to customize the appearance and behaviour of your columns. ## Column Order The implicit column order is the order in which columns have been defined in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) object. You can however control that explicitly by using the `columnOrder: string[]` prop. ```tsx const columnOrder = ['firstName','id','curency'] const App = () => { return primaryKey={"id"} dataSource={...}> columnOrder={columnOrder} onColumnOrderChange={(columnOrder: string[]) => {}} /> } ``` The [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) prop is an array of strings, representing the column ids. A column id is the key of the column in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) object. The [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) array can contain identifiers that are not yet defined in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) Map, or can contain duplicate ids. This is a feature, not a bug. We want to allow you to use the [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) in a flexible way so it can define the order of current and future columns. [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) is a controlled prop. For the uncontrolled version, see [`defaultColumnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnOrder) When using controlled [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder), make sure you also update the order by using the [`onColumnOrderChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnOrderChange) callback prop. **Example: Column Order demo, with firstName col displayed twice** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, }; export default function App() { const [columnOrder, setColumnOrder] = useState([ 'firstName', 'country', 'team', 'company', 'department', 'companySize', ]); return ( <>

Current column order:{' '}

{columnOrder.join(', ')}.

Drag column headers to reorder.

data={dataSource} primaryKey="id"> debugId="columnOrder-example" columns={columns} columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} columnDefaultWidth={200} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; ``` By keeping the column order simple, namely an array of strings, ordering becomes much easier. The alternative would be to make `columns` an array, which most DataGrids do - and whenever they are reordered, a new `columns` array would be needed. --- # Column Styling > Styling columns in the InfiniteTable React DataGrid via both style and className properties. Canonical page: https://infinite-table.com/docs/learn/columns/cell-and-column-styling ## Using the column `style` The most straightforward way to style the cells in a column is to use the [column.style](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style) property as an object. ```ts title="Styling a column in the DataGrid" const column = { firstName: { style: { color: 'red', fontWeight: 'bold', }, }, }; ``` ```tsx import * as React from 'react'; import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, }, name: { field: 'firstName', header: 'Name', style: { color: 'red', fontWeight: 'bold', }, }, }; type Developer = { id: number; firstName: string; lastName: string; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const domProps = { style: { minHeight: 300, }, }; export default function App() { return ( primaryKey="id" data={dataSource}> debugId="column-style-object-example" domProps={domProps} columns={columns} /> ); } ``` The [column.style](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style) property can either be an object (of type `React.CSSProperties`) or a function that returns an object (of the same type). Using functions for the [column.style](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style) property allows you to style the cells based on the cell's value or other properties. ```ts {6} title="Styling a column using a style function" const columns = { salary: { field: 'salary', type: 'number', style: ({ value, data, column, rowInfo }) => { return { color: value && value > 100_000 ? 'red' : 'tomato', }; }, }, }; ``` **Example: Using column.style as a function** ```tsx import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, }, firstName: { field: 'firstName' }, salary: { field: 'salary', type: 'number', style: ({ value }) => { return { color: value && value > 100_000 ? 'red' : 'tomato', }; }, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { return ( <> primaryKey="id" data={dataSource}> debugId="column-style-fn-example" columns={columns} columnDefaultWidth={200} /> ); } ``` If defined as a function, the [column.style](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style) accepts an object as a parameter, which has the following properties: - `column` - the current column where the style is being applied - `data` - the data object for the current row. The type of this object is `DATA_TYPE | Partial | null`. For regular rows, it will be of type `DATA_TYPE`, while for group rows it will be `Partial`. For rows not yet loaded (because of batching being used), it will be `null`. - `rowInfo` - the information about the current row - see [Using RowInfo](https://infinite-table.com/docs/learn/rows/using-row-info.md) for more details. - `value` - the underlying value of the current cell - will generally be `data[column.field]`, if the column is bound to a `field` property ## Using the column `className` Mirroring the behavior already described for the [column.style](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style) property, the [column.className](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.className) property can be used to apply a CSS class to the cells in a column. It can be used as a string or a function that returns a string. ```ts title="Styling a column using column.className" const columns = { firstName: { className: 'first-name-column', }, }; ``` **Example: Using column.className as an string** ```tsx files=["column-className-string-example.page.tsx","coloring.module.css"] ``` Using functions for the [column.className](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.className) property allows you to style the cells based on the cell's data/value/rowInfo etc. ```ts {6} title="Styling a column using a className function" const columns = { salary: { field: 'salary', type: 'number', className: ({ value, data, column, rowInfo }) => { return value && value > 100_000 ? 'red-color' : 'tomato-color', }, }, } ``` **Example: Using column.className as a function** ```tsx files=["column-className-fn-example.page.tsx","coloring.module.css"] ``` --- # Column Groups > Columns can be grouped with multiple levels of nesting thus making Infinite Table DataGrid a powerful tool for data analysts Canonical page: https://infinite-table.com/docs/learn/columns/column-grouping Specify column groups via the controlled [`columnGroups`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnGroups) (or uncontrolled [`defaultColumnGroups`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnGroups)) prop. The value is an object, with keys being the group id and value being the group description. ```tsx title="defining-column-groups" const columnGroups: Record = { 'contact info': { header: 'Contact info' }, // `street` column group belongs to the `address` columnGroup street: { header: 'street', columnGroup: 'address' }, location: { header: 'location', columnGroup: 'address' }, // this is a top-level group address, { header: 'Address' } } ``` A column group can have a parent column group, specified by the [`columnGroups.columnGroup`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnGroups.columnGroup) property. The same goes for a column - columns can have [columnGroup](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.columnGroup) as well. ```tsx title="defining-columns-with-groups" const columns: Record> = { id: { field: 'id' }, // `streetNo` column belongs to the `street` columnGroup streetNo: { field: 'streetNo', columnGroup: 'street' }, city: { field: 'city', columnGroup: 'location' }, streetName: { field: 'streetName', columnGroup: 'street' }, firstName: { field: 'firstName' }, country: { field: 'country', columnGroup: 'location' }, region: { field: 'region', columnGroup: 'location' }, email: { field: 'email', columnGroup: 'contact info' }, phone: { field: 'phone', columnGroup: 'contact info' }, }; ``` ## Column groups in action **Example** ```tsx files=["column-groups-example.page.tsx","column-groups-data.ts"] ``` --- # Column Headers > Configure column headers with custom column header, custom sort icon, menu icon and more. Canonical page: https://infinite-table.com/docs/learn/columns/column-headers Column headers have the same level of customization as column cells - you can fully control what is being rendered and when. Here's a summary of the things you can do in the column header: - customize the header label of a column - specify custom sort icon - configure and customize the menu icon - configure the column selection chechbox (for columns configured to display a selection checkbox) - customize the order of all of the above, and select which ones should be included ## Column Header Label By default, the label displayed for the column header is the [field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) the column is bound to. If you want to customize this, use the [header](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) property. ```tsx type Developer = { id: string; firstName: string; lastName: string; age: number; }; const columns: InfiniteTablePropColumns = { id: { field: 'id', // will be used as default label in column header defaultWidth: 100, }, name: { header: 'First and Last Name', // custom column header label valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`, }, }; ``` **Example: Simple table with both default and custom column headers** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { id: { field: 'id', // will be used as default label in column header defaultWidth: 100, }, name: { header: 'First and Last Name', // custom column header label valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`, }, }; type Developer = { id: number; firstName: string; lastName: string; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const domProps = { style: { minHeight: 300, }, }; export default function App() { return ( primaryKey="id" data={dataSource}> debugId="column-header-example" domProps={domProps} columns={columns} /> ); } ``` Having the [header](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) property be a strin value is useful but when you want more flexibility, you can use a function instead. When [the column header](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) is a function, it is called with an object that contains the following properties: - `column` - the current column object. NOTE: it's not the same as the column object you passed to the [columns](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) prop - but rather an enhanced version of that, which contains additional properties and computed values. It is called a "computed" column - typed as `InfiniteTableComputedColumn`. - `columnsMap` - a map of all computed columns available in the table, keyed by the column id. This is useful if at runtime you need access to other columns in the table. NOTE: this map does not contain only the visible columns, but rather ALL the columns. - `columnSortInfo` - the sorting information for the current column, or `null` if the column is not sorted. - `api` - a reference to the table [API](https://infinite-table.com/docs/reference/api/index.md) object. - `columnApi` - a reference to the table [Column API](https://infinite-table.com/docs/reference/column-api/index.md) object for bound to the current column. - `allRowsSelected: boolean` - `someRowsSelected: boolean` - `renderBag` - more on that below - used to reference changes between the different render functions of the column header (those functions are the column header rendering pipeline described in the next section). All the render props exposed for the rendering pipeline of the column header are called with the same object as the first argument. Having the [column header](https://infinite-table.com/docs/reference/infinite-table-props.md#column.header) as a function and having access to the state of the column and of the table allows you to create very dynamic column headers that accurately reflect column state. ## Column Header Rendering Pipeline The rendering pipeline of the column header is similar to the one of the column cells. It's a series of functions defined on the column that are called while rendering elements found in the column header (the header label, the sort and menu icons, the filtering icon, the selection checkbox). All of the functions that are part of the column header rendering pipeline are called with the same object as the first argument - the shape of this object is described in the previous section. - [renderSortIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSortIcon) - [renderFilterIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderFilterIcon) - [renderMenuIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderMenuIcon) - [renderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) - [renderHeaderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderHeaderSelectionCheckBox) - [header](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) If you want to customize any of the above, use the corresponding function. For even more control, the last function in the pipeline that gets called is the [column.renderHeader](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderHeader) function. This function is called with the same object as the first argument, but it also has a `renderBag` property that contains the result of all the previous functions in the pipeline (eg: `renderBag.sortIcon` - the result of the [renderSortIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSortIcon) call, `renderBag.filterIcon` - the result of the [renderFilterIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderFilterIcon) call, etc). So if you specify a custom [renderHeader](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderHeader) function, it's up to you to use the results of the previous functions in the pipeline, in order to fully take control of the column header. #### Available properties on the renderBag The `renderBag` object contains the following properties available to the render functions of the column header: - `header` - the label of the column header. - `sortIcon` - the default sort icon - `filterIcon` - the filter icon - displayed when the current column is used in filtering - `filterEditor` - the current filter editor - `menuIcon` - the menu icon that can be clicked to open the column menu - `selectionCheckBox` - the selection check box - displays the current selection status and controls the selection for all rows. - `all` - all of the above combined together in a `React.Fragment`. ### Customizing the Sort Icon For customizing the sort icon, use the [column.renderSortIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSortIcon) function. Inside that function you can either use the object passed as a parameter to get information about the sort state of the column ```tsx {1} title="Customizing_the_column_sort_icon" renderSortIcon({ columnSortInfo }) { if (!columnSortInfo) { return ' 🤷‍♂️'; } return columnSortInfo.dir === 1 ? '▲' : '▼'; } ``` or you can use the [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) hook to get the same information. ```tsx {8} title="Customizing_the_column_sort_icon" import { useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; /// ... renderSortIcon(){ const { columnSortInfo } = useInfiniteHeaderCell(); if (!columnSortInfo) { return ' 🤷‍♂️'; } return columnSortInfo.dir === 1 ? '▲' : '▼'; }, ``` **Example: Custom sort icon for the name column** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { id: { field: 'id', // will be used as default label in column header defaultWidth: 100, }, name: { header: 'Name', // custom column header label valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`, renderSortIcon: () => { const { columnSortInfo } = useInfiniteHeaderCell(); // eslint-disable-line if (!columnSortInfo) { return ' 🤷‍♂️'; } return columnSortInfo.dir === 1 ? '▲' : '▼'; }, }, }; type Developer = { id: number; firstName: string; lastName: string; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const domProps = { style: { minHeight: 300, }, }; export default function App() { return ( primaryKey="id" data={dataSource}> debugId="column-sort-icon-example" domProps={domProps} columns={columns} /> ); } ``` ### Customizing the Menu Icon For customizing the menu icon, use the [column.renderMenuIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderMenuIcon) function. Inside that function you can either use the object passed as a parameter to get information about the column ```tsx {1} title="Customizing_the_menu_icon" renderMenuIcon({ column }) { return `🔧 ${column.id}`; } ``` or you can use the [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) hook to get the same information. ```tsx {8} title="Customizing_the_menu_icon" import { useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; /// ... renderMenuIcon(){ const { column } = useInfiniteHeaderCell(); return `🔧 ${column.id}`; }, ``` **Example: Custom menu icon for the name and age columns** Hover over the header for the `Name` and `Age` columns to see the custom menu icon. Also, the id column has `renderMenuIcon: false` set, so it doesn't show a column menu at all. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, renderMenuIcon: false, }, name: { header: 'Name', // custom column header label valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`, // custom menu icon renderMenuIcon: () =>
🌎
, }, age: { field: 'age', header: 'Age', renderMenuIcon: ({ column }) => { return `🔧 ${column.id}`; }, }, }; type Developer = { id: number; firstName: string; lastName: string; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const domProps = { style: { minHeight: 300, }, }; export default function App() { return ( primaryKey="id" data={dataSource}> debugId="column-menu-icon-example" domProps={domProps} columns={columns} /> ); } ``` If you don't want to show a column menu (icon) at all, you can set the [column.renderMenuIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderMenuIcon) prop to `false`. Also, see the [column.renderMenuIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderMenuIcon) docs for an example on how to use the api to open the column menu. ### Customizing the Filter Icon For customizing the filter icon, use the [column.renderFilterIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderFilterIcon) function. Inside that function you can either use the object passed as a parameter to get information about the `filtered` state of the column ```tsx {1} title="Customizing_the_filter_icon" renderFilterIcon({ filtered }) { return filtered ? '🔍' : ''; } ``` or you can use the [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) hook to get the same information. ```tsx {8} title="Customizing_the_menu_icon" import { useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; /// ... renderMenuIcon(){ const { filtered } = useInfiniteHeaderCell(); return filtered ? '🔥' : ''; }, ``` In addition, you can use the `filtered` property in the [column.header](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) function to determine if the column is filtered or not and render a different header label. If specified, the [column.renderFilterIcon](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderFilterIcon) function prop is called even if the column is not currently filtered. **Example: Custom filter icons for salary and name columns** The `salary` column will show a bolded label when filtered. The `firstName` column will show a custom filter icon when filtered. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { field: 'salary', type: 'number', header: ({ filtered }) => { return filtered ? Salary : 'Salary'; }, renderFilterIcon: () => { return null; }, }, firstName: { field: 'firstName', renderFilterIcon: ({ filtered }) => { return filtered ? '🔥' : ''; }, }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" > debugId="column-filter-icon-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` #### Changing the display of filters Infinite Table allows very deep cusstomization of the column header, including the filters. For example, you might not want to display the column filters under the column header, but rather in a separate menu popover. This section shows how to do that. You can use [showColumnFilters=false](https://infinite-table.com/docs/reference/infinite-table-props.md#showColumnFilters) to hide the filters from under the column header. Next, you can use the [column.renderHeader](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderHeader) function to render a custom filter icon that opens a filter popover when clicked. You don't need to re-implement the filter editor, you have acces to it via the `renderBag.filterEditor` property. The code below shows how to do this. **Example: Custom display of column filters** ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableColumn, useInfiniteHeaderCell, alignNode, useInfinitePortalContainer, } from '@infinite-table/infinite-react'; import { createPortal } from 'react-dom'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const FilterIcon = () => ( ); function ColumnFilterMenuIcon() { const { renderBag, htmlElementRef: alignToRef, column, } = useInfiniteHeaderCell(); const portalContainer = useInfinitePortalContainer(); const [visible, setVisible] = React.useState(false); React.useEffect(() => { if (!domRef.current || !alignToRef.current) { return; } alignNode(domRef.current, { alignTo: alignToRef.current, alignPosition: [['TopRight', 'BottomRight']], }); }); const domRef = React.useRef(null); return (
{ if (e.nativeEvent) { // @ts-ignore e.nativeEvent.__insideMenu = column.id; } }} onPointerDown={(event) => { event.stopPropagation(); if (visible) { setVisible(false); return; } setVisible(true); function handleMouseDown(event: MouseEvent) { // @ts-ignore if (event.__insideMenu !== column.id) { setVisible(false); document.documentElement.removeEventListener( 'mousedown', handleMouseDown, ); } } document.documentElement.addEventListener('mousedown', handleMouseDown); }} > {createPortal(
{renderBag.filterEditor}
, portalContainer!, )}
); } const customHeaderWithFilterMenu: InfiniteTableColumn['renderHeader'] = ({ renderBag, }) => { return ( <> {renderBag.header}
{renderBag.filterIcon} {renderBag.menuIcon} ); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, renderHeader: customHeaderWithFilterMenu, }, salary: { field: 'salary', type: 'number', renderHeader: customHeaderWithFilterMenu, }, firstName: { field: 'firstName', renderHeader: customHeaderWithFilterMenu, }, stack: { field: 'stack', renderHeader: customHeaderWithFilterMenu }, currency: { field: 'currency', renderHeader: customHeaderWithFilterMenu }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} shouldReloadData={{ filterValue: false, sortInfo: false, groupBy: false, pivotBy: false, }} > debugId="custom-column-filter-display-example" showColumnFilters={false} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ### Customizing the Selection Checkbox For customizing the selection checkbox in the column header, use the [column.renderHeaderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderHeaderSelectionCheckBox) function. If you want another column, other than the group column, to show a selection checkbox, you have to also set the [column.renderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) prop to `true`. **Example: Custom header checkbox selection for columns** The group column, as well as the `stack` column display a custom selection checkbox in the column header. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTableProps, InfiniteTablePropColumns, DataSourceProps, } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { renderSelectionCheckBox: true, renderHeaderSelectionCheckBox: ({ renderBag }) => { // render the default value and decorate it return [{renderBag.selectionCheckBox}]; }, field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', }, canDesign: { field: 'canDesign', }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', renderHeaderSelectionCheckBox: ({ renderBag }) => { // render the default value and decorate it return [{renderBag.selectionCheckBox}]; }, defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { return ( data={dataSource} groupBy={defaultGroupBy} selectionMode="multi-row" primaryKey="id" > debugId="column-header-selection-checkbox-example" columns={columns} domProps={domProps} groupColumn={groupColumn} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` --- # Column Menus > Columns have menus that allow quick actions - the menus can be customized or hidden altogether. Canonical page: https://infinite-table.com/docs/learn/columns/column-menus All columns in the Infinite Table have a default menu, which can be customized or hidden altogether. ## Customise the menu items To customize the column menu (for all columns, or for a specific column), use the [`getColumnMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getColumnMenuItems) prop. This function is called with an array of menu items (which are the default items) and it should the final array of menu items - so you can return the default items as is, or you can adjust the default items to fit your needs. ```tsx title="Customizing-column-menu" function getColumnMenuItems(items, { column }) { if (column.id === 'firstName') { // you can adjust the default items for a specific column items.splice(0, 0, { key: 'firstName', label: 'First name menu item', onClick: () => { console.log('Hey there!'); }, }); } // or for all columns items.push({ key: 'hello', label: 'Hello World', onClick: () => { alert('Hello World from column ' + column.id); }, }); return items; } ``` [`getColumnMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getColumnMenuItems) can return an empty array, in which case, the column menu will not be shown - however, people will still be able to click the menu icon to trigger the column context menu. If you want to dynamically decide whether a column should show a menu or not, you can use the [`columns.renderMenuIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderMenuIcon) prop. **Example: Custom column menu items and custom menu icon** In this example, the currency and preferredLanguage columns have a custom icon for triggering the column context menu. In addition, the `preferredLanguage` column has a custom header that shows a button for triggering the column context menu. ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { currency: { field: 'currency', // custom menu icon renderMenuIcon: () =>
🌎
, }, preferredLanguage: { field: 'preferredLanguage', defaultWidth: 350, header: ({ columnApi, renderLocation }) => { // if we're inside the column menu with all columns, return only the col name if (renderLocation === 'column-menu') { return 'Preferred Language'; } // but for the real column header // return this custom content return ( <> Preferred Language{' '} ); }, // custom menu icon renderMenuIcon: () =>
🌎
, }, salary: { field: 'salary', // hide the menu icon renderMenuIcon: false, }, country: { field: 'country', }, id: { field: 'id', defaultWidth: 80, renderMenuIcon: false }, firstName: { field: 'firstName', }, }; export default function ColumnContextMenuItems() { return ( <> primaryKey="id" data={dataSource}> debugId="getColumnMenuItems-example" columnHeaderHeight={70} columns={columns} getColumnMenuItems={(items, { column }) => { if (column.id === 'firstName') { // you can adjust the default items for a specific column items.splice(0, 0, { key: 'firstName', label: 'First name menu item', onAction: () => { console.log('Hey there!'); }, }); } items.push( { key: 'hello', label: 'Hello World', onAction: () => { alert('Hello World from column ' + column.id); }, }, { key: 'translate', label: 'Translate', menu: { items: [ { key: 'translateToEnglish', label: 'English', onAction: () => { console.log('Translate to English'); }, }, { key: 'translateToFrench', label: 'French', onAction: () => { console.log('Translate to French'); }, }, ], }, }, ); return items; }} /> ); } ``` As you can see in the demo above, you can use [`getColumnMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getColumnMenuItems) to return the default items (received as the first parameter to the function), or another totally different array. We chose to pass the default items to the function, so you can use them as a starting point and adjust them to your needs. Each item in the array you return from [`getColumnMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getColumnMenuItems) should have a `key` and a `label` property. Additionally, you can specify an `onAction` function, which will be called when the user clicks the menu item. It's also possible to create items with submenus. For this, specify a `menu` property in the item, with an `items` array. Each item in the `items` array should have a `key` and a `label` property, as you would expect. ```tsx {8} title="Menu_items_with_submenus" function getColumnMenuItems(items, { column }) { const items = [ { key: 'translate', label: 'Translate', menu: { items: [ { key: 'translateToEnglish', label: 'English', onAction: () => { console.log('Translate to English'); }, }, { key: 'translateToFrench', label: 'French', onAction: () => { console.log('Translate to French'); }, }, ], }, }, ]; return items; } ``` ## Custom menu icon To customize the menu icon, use the [`columns.renderMenuIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderMenuIcon) prop. This prop can be a boolean or a function that returns a `ReactNode`. ```tsx title="custom-menu-icon" const columns = { name: { field: 'firstName', renderMenuIcon: () =>
🌎
, }, salary: { field: 'salary', renderMenuIcon: false, }, }; ``` For a custom menu icon 🌠 you don't have to hook up the `mousedown`/`click` in order to show or hide the menu - all this is done for you - just render your custom `ReactNode` and you're good to go. --- # Column Order > Change column order by drag-and-drop - drag columns around and reorder them live Canonical page: https://infinite-table.com/docs/learn/columns/column-order React `Infinite Table` allows columns to be reordered in the grid by drag-and-drop. Drag columns around (start dragging the colum header) to change their order and arrange them in the desired position. Column ordering via drag & drop works by default. You don't have to specify an initial column order or any other callback props to update the column order. The default behavior of the component is to initially display all columns that are provided in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) object (in the iteration order of the object keys). If using the [default uncontrolled column order](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnOrder) is not enough, try using the controlled [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) prop, which gives you full control over the order of the columns - in this case, you have to update the column order as a result of user interaction, by specifying [`onColumnOrderChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnOrderChange). **Example: Column reordering via drag & drop with controlled `columnOrder`** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, }; export default function App() { const [columnOrder, setColumnOrder] = useState([ 'firstName', 'country', 'team', 'company', 'department', 'companySize', ]); return ( <>

Current column order:{' '}

{columnOrder.join(', ')}.

Drag column headers to reorder.

data={dataSource} primaryKey="id"> debugId="columnOrder-example" columns={columns} columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} columnDefaultWidth={200} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; ``` Column order can also be used in order to limit/modify the visible columns. Specify a limited number of columns in the [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) array and only those columns will be displayed. For more advanced control on visibility, see [`columnVisibility`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnVisibility). The [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) array can contain any number of columns, even duplicate columns or random strings - the behavior is that any column ids which are not found in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) object are ignored, while columns mentioned multiple times will be included multiple times, as indicated in the column order. Displaying the same column twice is a perfectly valid use case. **Example: Advanced column order example** In this example, [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) is used as a controlled property, also as a way of limiting the visible columns. ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, }; export default function App() { const [columnOrder, setColumnOrder] = useState([ 'firstName', 'country', 'team', 'company', 'firstName', 'not existing column', 'companySize', ]); return ( <>

Current column order:{' '}

{JSON.stringify(columnOrder)}.

Note: if the column order contains columns that don't exist in the `columns` definition, they will be skipped.

data={dataSource} primaryKey="id"> debugId="columnOrder-advanced-example" columns={columns} columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} columnDefaultWidth={200} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; ``` The [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) prop can either be an array of strings (column ids) or the boolean `true`. When `true`, all columns present in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) object will be displayed, in the iteration order of the object keys - in the example above, try clicking the `"Click to reset column order"` button. For all of the above examples, [`columnVisibility`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnVisibility) will also be taken into account, as it is the last source of truth for the visibility of a column. Using [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) in combination with [`columnVisibility`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnVisibility) is very powerful - for example, you can have a specific column order even for columns which are not visible at a certain moment, so when they will be made visible, you'll know exactly where they will be displayed. --- # Column Rendering > Customize column rendering for Infinite Table DataGrid to match your app and use custom components. Column styling and formatting, conditional rendering... Canonical page: https://infinite-table.com/docs/learn/columns/column-rendering Columns render the [field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) value of the data they are bound to. This is the default behavior, which can be customized in a number of ways that we're exploring below. If you want to explicitly use the TypeScript type definition for columns, import the `InfiniteTableColumn` type ``` import { InfiniteTableColumn } from '@infinite-table/infinite-react' ``` Note that it's a generic type, so when you use it, you have to bind it to your `DATA_TYPE` (the type of your data object). When using custom rendering or custom components for columns, make sure all your rendering logic is [controlled](https://reactjs.org/docs/forms.html#controlled-components) and that it doesn't have local/transient state. This is important because `InfiniteTable` uses virtualization heavily, in both _column cells and column headers_, so **custom components can and will be unmounted and re-mounted multiple times**, during the virtualization process (triggered by user scrolling, sorting, filtering and a few other interactions). ## Change the value using `valueGetter` The simplest way to change what's being rendered in a column is to use the `valueGetter` prop and return a new value for the column. ```tsx const nameColumn: InfiniteTableColumn = { header: 'Employee Name', valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`, }; ``` The [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) prop is a function that takes a single argument - an object with `data` and `field` properties. Note that the [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) is only called for non-group rows, so the `data` property is of type `DATA_TYPE`. **Example: Column with custom valueGetter** ```tsx import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, name: { header: 'Full Name', valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-valueGetter-example" columns={columns} columnDefaultWidth={200} /> ); } ``` The column value getter should not return JSX or other markup, because the value return by [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) will be used when the column is sorted (when sorting is done client-side and not remotely). For more in-depth information on sorting see [the column sorting page](./column-sorting). ## Use [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) and [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) to display custom content The next step in customizing the rendering for a column is to use the [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) or the [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) props. In those functions, you have access to more information than in the [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. For example, you have access to the current value of `groupBy` and `pivotBy` props. [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) and [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) can return any value that React can render. The [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) and [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) functions are called with an object that has the following properties: - `data` - the data object (of type `DATA_TYPE | Partial | null`) for the row. - `rowInfo` - very useful information about the current row: - `rowInfo.collapsed` - if the row is collased or not. - `rowInfo.groupBy` - the current group by for the row - `rowInfo.indexInAll` - the index of the row in the whole data set - `rowInfo.indexInGroup` - the index of the row in the current group - `rowInfo.value` - the value (only for group rows) that will be rendered by default in group column cells. - ... there are other useful properties that we'll document in the near future - `column` - the current column being rendered - `columnsMap` - the `Map` of columns available to the table. Note these might not be all visible. The keys in this map will be column ids. - `fieldsToColumn` a `Map` that links `DataSource` fields to columns. Columns bound to fields (so with [`columns.field`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) specified) will be included in this `Map`. - `api` - A reference to the [Infinite Table API](https://infinite-table.com/docs/reference/api/index.md) object. [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) is the last function called in the rendering pipeline for a column cell, while [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) is called before render, towards the beginning of the [rendering pipeline (read more about this below)](#rendering-pipeline). Avoid over-writing [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) for special columns (like group columns) unless you know what you're doing. Special columns use the `render` function to render additional content inside the column (eg: collapse/expand tool for group rows). The [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) function allows you to override this additional content. So if you specify this function, it's up to you to render whatever content, including the collapse/expand tool. However, there are easier ways to override the collapse/expand group icon, like using [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon). Inside the [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) and [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) functions (and other rendering functions), you can use the [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) hook to retrieve the same params that are passed to the render functions. This is especially useful when inside those functions you render a custom component that needs access to the same information. ```tsx type Developer = { country: string; name: string; id: string }; const CountryInfo = () => { const { data, rowInfo, value } = useInfiniteColumnCell(); return
Country: {value}
; }; const columns = { country: { field: 'country', renderValue: () => , }, }; ``` **Example: Column with custom renderValue** ```tsx import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, InfiniteTableColumnRenderValueParam, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderValue: ({ data, rowInfo }) => { if (rowInfo.isGroupRow) { return <>{rowInfo.value} stuff; } return 🎇 {data?.stack}; }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const defaultGroupBy: DataSourceGroupBy[] = [{ field: 'stack' }]; const groupColumn: InfiniteTablePropGroupColumn = { defaultWidth: 250, renderValue: ({ rowInfo, }: InfiniteTableColumnRenderValueParam) => { if (rowInfo.isGroupRow) { return ( <> Grouped by {rowInfo.value} ); } return null; }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource} defaultGroupBy={defaultGroupBy} > debugId="column-renderValue-example" groupColumn={groupColumn} columns={columns} columnDefaultWidth={200} /> ); } ``` Changing the group icon using `render`. The icon can also be changed using [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon). **Example: Column with render - custom expand/collapse icon** This snippet shows overriding the group collapse/expand tool via the [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) function. ```tsx import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const defaultGroupBy: DataSourceGroupBy[] = [{ field: 'stack' }]; const groupColumn: InfiniteTablePropGroupColumn = { defaultWidth: 250, render: ({ rowInfo, toggleCurrentGroupRow }) => { if (rowInfo.isGroupRow) { const { collapsed } = rowInfo; const expandIcon = ( {collapsed ? ( <> ) : ( )} ); return (
toggleCurrentGroupRow()} > Grouped by {rowInfo.value} {expandIcon}
); } return null; }, }; export default function ColumnCustomRenderExample() { return ( <> primaryKey="id" data={dataSource} defaultGroupBy={defaultGroupBy} > debugId="column-render-example" groupColumn={groupColumn} columns={columns} columnDefaultWidth={200} /> ); } ``` **Example: Column with custom expand/collapse tool via renderGroupIcon** This snippet shows how you can override the group collapse/expand tool via the [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon) function. ```tsx import { InfiniteTable, DataSource, DataSourcePropGroupBy, InfiniteTablePropColumns, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const groupBy: DataSourcePropGroupBy = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}, }, }, { field: 'preferredLanguage' }, ]; const groupColumn: InfiniteTableColumn = { renderGroupIcon: ({ rowInfo, toggleCurrentGroupRow }) => { return (
{rowInfo.isGroupRow ? (rowInfo.collapsed ? '👇' : '👉') : ''}
); }, }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="column-renderGroupIcon-example" columns={columns} groupColumn={groupColumn} /> ); } ``` ## Using hooks for custom rendering Inside the [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) functions, you can use hooks - both provided by `InfiniteTable` and any other `React` hooks. ### Hook: [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) When you're inside a rendering function for a column cell, you can use [useInfiniteColumnCell hook](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) to get access to the current cell's rendering information - the argument passed to the [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) or [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) functions. ```tsx import { useInfiniteColumnCell, InfiniteTableColumn, } from '@infinite-table/infintie-react'; function CustomName() { const { data, rowInfo } = useInfiniteColumnCell(); return ( <> {data.firstName}, {data.lastName} ); } const nameColumn: InfiniteTableColumn = { header: 'Employee Name', renderValue: () => , }; ``` **Example: Column with render & useInfiniteColumnCell** ```tsx import { InfiniteTable, DataSource, useInfiniteColumnCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { HTMLProps } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; function CustomCell(_props: HTMLProps) { const { value, data } = useInfiniteColumnCell(); let emoji = '🤷'; switch (value) { case 'photography': emoji = '📸'; break; case 'cooking': emoji = '👨🏻‍🍳'; break; case 'dancing': emoji = '💃'; break; case 'reading': emoji = '📚'; break; case 'sports': emoji = '⛹️'; break; } const label = data?.stack === 'frontend' ? '⚛️' : ''; return ( {emoji} + {label} ); } const columns: InfiniteTablePropColumns = { id: { field: 'id', maxWidth: 80 }, firstName: { field: 'firstName' }, hobby: { field: 'hobby', // we're not using the arg of the render function directly // but CustomCell uses `useInfiniteColumnCell` to retrieve it instead render: () => , }, }; export default function ColumnRenderWithHooksExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-render-hooks-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ### Hook: [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) For column headers, you can use [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) hook to get access to the current header's rendering information - the argument passed to the [`columns.header`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) function. ```tsx import { useInfiniteHeaderCell, InfiniteTableColumn, } from '@infinite-table/infintie-react'; function CustomHeader() { const { column } = useInfiniteHeaderCell(); return {column.field}; } const nameColumn: InfiniteTableColumn = { header: 'Employee Name', field: 'firstName', header: () => , }; ``` **Example: Column Header with render & useInfiniteHeaderCell** ```tsx import { InfiniteTable, DataSource, useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const HobbyHeader: React.FC = function () { const { column } = useInfiniteHeaderCell(); return {column?.field} 🤷📸👨🏻‍🍳💃📚⛹️; }; const columns: InfiniteTablePropColumns = { id: { field: 'id', maxWidth: 80 }, stack: { field: 'stack', }, hobby: { field: 'hobby', components: { HeaderCell: HobbyHeader, }, }, }; export default function ColumnHeaderExampleWithHooks() { return ( <> primaryKey="id" data={dataSource}> debugId="column-header-hooks-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ## Use [column.components](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components) to customize the column There are cases when custom rendering via the [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) props is not enough and you want to fully control the column cell and render your own custom component for that. For such scenarios, you can specify `column.components.HeaderCell` and `column.components.ColumnCell`, which will use those components to render the DOM nodes of the column header and column cells respectively. ```tsx import { InfiniteTableColumn } from '@infinite-table/infintie-react'; const ColumnCell = (props: React.HTMLProps) => { const { domRef, rowInfo } = useInfiniteColumnCell(); return (
{props.children}
); }; const HeaderCell = (props: React.HTMLProps) => { const { domRef, sortTool } = useInfiniteHeaderCell(); return (
{sortTool} First name
); }; const nameColumn: InfiniteTableColumn = { header: 'Name', field: 'firstName', components: { ColumnCell, HeaderCell, }, }; ``` When using custom components, make sure you get `domRef` from the corresponding hook ([`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) for column cells and [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) for header cells) and pass it on to the final `JSX.Element` that is the DOM root of the component. ```tsx // inside a component specified in column.components.ColumnCell const { domRef } = useInfiniteColumnCell(); return
...
; ``` Also you have to make sure you spread all other `props` you receive in the component, as they are `HTMLProps` that need to end-up in the DOM (eg: `className` for theming and default styles, etc). Both [components.ColumnCell](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell) and [components.HeaderCell](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.HeaderCell) need to be declared with `props` being of type `HTMLProps`. **Example: Custom components** ```tsx import { InfiniteTable, DataSource, useInfiniteColumnCell, useInfiniteHeaderCell, InfiniteTablePropColumnTypes, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const DefaultHeaderComponent: React.FunctionComponent< React.HTMLProps > = (props) => { const { column, domRef, columnSortInfo } = useInfiniteHeaderCell(); const style = { ...props.style, border: '1px solid #fefefe', }; let sortTool = ''; switch (columnSortInfo?.dir) { case undefined: sortTool = '👉'; break; case 1: sortTool = '👇'; break; case -1: sortTool = '☝🏽'; break; } return (
{/* here you would usually have: */} {/* {props.children} {sortTool} */} {/* but in this case we want to override the default sort tool as well (which is part of props.children) */} {column.field} {sortTool}
); }; const StackComponent: React.FunctionComponent< React.HTMLProps > = (props) => { const { value, domRef } = useInfiniteColumnCell(); const isFrontEnd = value === 'frontend'; const emoji = isFrontEnd ? '⚛️' : '💽'; const style = { padding: '5px 20px', border: `1px solid ${isFrontEnd ? 'red' : 'green'}`, ...props.style, }; return (
{props.children}
{emoji}
); }; const columnTypes: InfiniteTablePropColumnTypes = { default: { // override all columns to use these components components: { HeaderCell: DefaultHeaderComponent, }, }, }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderValue: ({ data }) => 'Stack: ' + data?.stack, components: { HeaderCell: DefaultHeaderComponent, ColumnCell: StackComponent, }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-components-example" columns={columns} columnTypes={columnTypes} /> ); } ``` If you're using the [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) hook inside the [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) or [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) functions (and not as part of a custom component in [`columns.components.ColumnCell`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell)), you don't need to pass on the `domRef` to the root of the DOM you're rendering (same is true if you're using [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) inside the [`columns.header`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) function). If the above [`columns.components`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components) is still not enough, read about the rendering pipeline below. ## Rendering pipeline The rendering pipeline for columns is a series of functions defined on the column that are called while rendering. All the functions that have the word `render` in their name will be called with an object that has a `renderBag` property, which contains values that will be rendered. The default [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) function (the last one in the pipeline) ends up rendering a few things: - a `value` - generally comes from the [field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) the column is bound to - a `groupIcon` - for group columns - a `selectionCheckBox` - for columns that have [`columns.renderSelectionCheckBox`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) defined (combined with row selection) When the rendering process starts for a column cell, all the above end up in the `renderBag` object. ### Rendering pipeline - `renderBag.value` As already mentioned, the `value` defaults to the value of the column [field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) for the current row. If the column is not bound to a field, you can define a [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter). The [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) only has access to `{data, field?}` in order to compute a value and return it. After the [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) is called, the [valueFormatter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) is next in the rendering pipeline. This is called with more details about the current cell ```tsx const column: InfiniteTableColumn = { // the valueGetter can be useful when rows are nested objects // or you want to compose multiple values from the row valueGetter: ({ data }) => { return data.person.salary * 10; }, valueFormatter: ({ value, isGroupRow, data, field, rowInfo, rowSelected, rowActive, }) => { // the value here is what the `valueFormatter` returned return `USD ${value}`; }, }; ``` After [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) and [valueFormatter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) are called, the resulting value is the actual value used for the cell. This value will also be assigned to `renderBag.value` When [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) and [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) are called by `InfiniteTable`, both `value` and `renderBag` will be available as properties to the arguments object. ```tsx {3,12} const column: InfiniteTableColumn = { valueGetter: () => 'world', renderValue: ({ value, renderBag, rowInfo }) => { // at this stage, `value` is 'world' and `renderBag.value` has the same value, 'world' return {value}; }, render: ({ value, renderBag, rowInfo }) => { // at this stage `value` is 'world' // but `renderBag.value` is world, as this was the value returned by `renderValue` return
Hello {renderBag.value}!
; }, }; ``` After the [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) function is called, the following are also called (if available): - [renderGroupValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue) - for group rows - [renderLeafValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderLeafValue) - for leaf rows You can think of them as an equivalent to [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue), but narrowed down to group/non-group rows. Inside those functions, the `renderBag.value` refers to the value returned by the [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) function. ### Rendering pipeline - `renderBag.groupIcon` In a similar way to `renderBag.value`, the `renderBag.groupIcon` is also piped through to the [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) function. ```tsx {2,9} const column: InfiniteTableColumn = { renderGroupIcon: ({ renderBag, toggleGroupRow }) => { return <> [ {renderBag.groupIcon} ] ; }, render: ({ renderBag }) => { return ( <> {/* use the groupIcon from the renderBag */} {renderBag.groupIcon} {renderBag.value} ); }, }; ``` Inside [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon), you have access to `renderBag.groupIcon`, which is basically the default group icon - so you can use that if you want, and build on that. Also inside [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon), you have access to `toggleGroupRow` so you can properly hook the collapse/expand behaviour to your custom group icon. ### Rendering pipeline - `renderBag.selectionCheckBox` Like with the previous properties of `renderBag`, you can customize the `selectionCheckBox` (used when multiple selection is configured) to be piped-through - for columns that specify [`columns.renderSelectionCheckBox`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox). ```tsx {2,25} const column: InfiniteTableColumn = { renderSelectionCheckBox: ({ renderBag, rowSelected, isGroupRow, toggleCurrentRowSelection, toggleCurrentGroupRowSelection, }) => { const toggle = isGroupRow ? toggleCurrentGroupRowSelection : toggleCurrentRowSelection; // you could return renderBag.groupIcon to have the default icon const selection = rowSelected === null ? '-' // we're in a group row with indeterminate state if rowSelected === null : rowSelected ? 'x' : 'o'; return
[ {selection} ]
; }, render: ({ renderBag }) => { return ( <> {/* use the selectionCheckBox from the renderBag */} {renderBag.selectionCheckBox} {renderBag.groupIcon} {renderBag.value} ); }, }; ``` To recap, here is the full list of the functions in the rendering pipeline, in order of invocation: 1. [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) - doesn't have access to `renderBag` 2. [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) - doesn't have access to `renderBag` 3. [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon) - can use all properties in `renderBag` 4. [`columns.renderSelectionCheckBox`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) - can use all properties in `renderBag` 5. [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) - can use all properties in `renderBag` 6. [`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue) - can use all properties in `renderBag` 7. [`columns.renderLeafValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderLeafValue) - can use all properties in `renderBag` 8. [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) - can use all properties in `renderBag` Additionally, the [`columns.components.ColumnCell`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell) custom component does have access to the `renderBag` via [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) --- # Column Sorting > Configure column sorting with single and multiple sorting and custom sort functions. Both client-side and server-side sorting is supported Canonical page: https://infinite-table.com/docs/learn/columns/column-sorting Docs coming soon --- # Column Types > Column types are blueprints for generalizing column configuration and code reuse. Canonical page: https://infinite-table.com/docs/learn/columns/column-types Column types allow you to specify common properties for multiple columns easily. Things like [minWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.minWidth), [maxWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.maxWidth), [defaultFlex](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultFlex) and [header](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.header) are all available. For a full list, see [columnTypes](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) reference. You specify the type of a column via the [column.type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) property: ```tsx type Person = { name: string; dob: string; age: number; } const columns = { age: { field: 'age', type: 'custom', }, date: { field: 'dob', // will be type default }, name: { field: 'name', // will have both of those types type: ['default', 'custom'] } } const columnTypes = { default: { width: 200 }, custom: { align: 'center' } } ``` Properties defined in a column have precedence over the properties defined in the [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes). Also, if a column has no [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) specified, it will default to the `default` type. If you don't want a column to have the `default` type, use [column.type=null](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) or [column.type=[]](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) The column [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) property can be an array - in this case, types are applied in the order they are specified, later types overriding properties of earlier ones. If the `default` type is not specified in the array, it will not be applied to the column - if you want to apply it as well, use [type=['default', 'any', 'other', 'types', 'after']](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) ## Column Type properties order and precedence When a column has multiple column types, they are applied in order, from left to right, with later types overriding properties of earlier ones - think of the behavior as very similar to `Object.assign`. Assume a column has the following types: ```tsx const columns = { salary: { type: ['number', 'currency'], }, }; const numberFormatter = new Intl.NumberFormat(); const columnTypes = { number: { renderValue: ({ value }) => numberFormatter.format(value), // makes 12345 render as 12,345 }, currency: { renderValue: ({ value }) => `USD: ${value}`, // makes 12345 render as USD: 12345 }, }; ``` Although the `salary` column has both the `number` and `currency` types, and both those types have the `renderValue` property defined, only the `currency` `renderValue` function will be called. In other words, the rendering is not piped from one column type to the next. This is applied for all properties, like `render`, `style`, etc. The `renderValue` function (and other similar functions) has access to the `column` object, so you can manually access all the column types. --- # Column Sizing Canonical page: https://infinite-table.com/docs/learn/columns/fixed-and-flexible-size Columns are a core concept for `Infinite Table` and sizing columns is an important topic to master. Here is a summary of how columns can be sized: - fixed-sized columns can be specified via [`columns.defaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth) - flexible columns need [`columns.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) - [`columns.minWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.minWidth) specifies the minimum size for a column - [`columns.maxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.maxWidth) is for the maximum width a column can take - default values are available for all of the above: - [`columnDefaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultWidth) gives all columns (that are otherwise unconfigured) a default size - [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth) specifies the minimum width for all columns (that don't have one) - [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth) specifies the maximum width for all columns (that don't have one) For fine-grained controlled-behavior on column sizing, use the controlled [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) prop (for uncontrolled variant, see [`defaultColumnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing)). If you want to get updates to columns changing size as a result of user interaction, use [`onColumnSizingChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnSizingChange). Use [`columnDefaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultWidth) to configure the default column width. If a column is not sized otherwise, this will be applied. The default value for [`columnDefaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultWidth) is `200` (pixels). For setting a minimum and maximum width for all columns, use [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth) (defaults to `30`) and [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth) (defaults to `2000`) respectively. ## Understanding default column sizing The easiest way to get started and specify a sizing behavior for columns is to use [column.defaultWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth), [column.defaultFlex](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) and/or [`columnDefaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultWidth) (including related pros for specifying limits, like [column.minWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.minWidth), [column.maxWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.maxWidth) and [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth) / [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth)). Those properties have `default` in their name because after the initial rendering of a column, you can't change its size by updating those values - more technically, [column.defaultWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth) and [column.defaultFlex](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) are uncontrolled props. We suggest you use those to get started and if you don't have care about responding to the user changing the widths of those columns via drag&drop. As long as you're not using [`onColumnSizingChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnSizingChange) to be notified of column size changes, you're probably good with those. ## Controlled column sizing However, once you start using [`onColumnSizingChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnSizingChange) and want to have full control of column sizing (maybe you want to restore it later to the state the user had it when the app was closed), you probably want to use controlled [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing). The [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) prop is an object of column ids to column sizing objects. Those sizing objects can have the following properties: - [flex](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.flex) - use this for flexible columns. Behaves like the flex CSS property. - [width](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.width) - use this for fixed sized columns - [minWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.minWidth) - specifies the minimum width of the column. Useful for flexible columns or for restricting users resizing both fixed and flexible columns. - [maxWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.maxWidth) - specifies the maximum width of the column. Useful for flexible columns or for restricting users resizing both fixed and flexible columns. If a column is not specified in the [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) prop (or its uncontrolled variant), or sized otherwise (eg: via the column type), it will have a fixed size, defaulting to [`columnDefaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultWidth) (which also defaults to `200` if no value is passed in). You can also specify a [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth) and [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth) - those will be applied for all columns (namely for those that dont explicitly specify other min/max widths). ```tsx const columnSizing: InfiniteTablePropColumnSizing = { country: { flex: 1, // minWidth is optional minWidth: 200, }, city: { width: 400, // and so is maxWidth maxWidth: 500, }, salary: { flex: 3, }, }; // any column not specified in the columnSizing (or defaultColumnSizing) prop // will have fixed width (defaulting to `columnDefaultWidth`, which in turn defaults to 200px) ``` You might find specifying the column size outside the column object to be a bit verbose to start with, but it will be easier to manage in many cases and is much more flexible. For example, when the user resizes a column via drag & drop and you want to persist the new column sizes, you don't have to update the whole `columns` object but instead update [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) alone. The same principle is true for [`columnPinning`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnPinning) and other column-level props. The `columnSizing` prop also has an uncontrolled version, namely [`defaultColumnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing). ## Using flexible column sizing The way flex sizing is implemented is similar to how CSS flexbox algorithm works. Explore this section to find out more details. Imagine you have `1000px` of space available to the viewport of `InfiniteTable` and you have 3 columns: - a fixed column `100px` wide - name it col `A` - a fixed column `300px` wide - name it col `B` - a flexible column with `flex: 1` - name it col `F1` - a flexible column with `flex: 2` - name it col `F2` The space remaining for the flexible columns is `1000px - 400px = 600px` and the sum of all flex values is `3`, that means each `flex` unit will be `600px / 3 = 200px`. This means columns will have the following sizes: - col `A` will be `100px` - col `B` will be `300px` - col `F1` will be `200px` ( so a flex unit) - col `F2` will be `400px` ( so the equivalent of `2` flex units) If the browser changes the layout of the component, so `InfiniteTable` has only `700px` available, then a flex unit would be `(700px - 400px) / 3 = 100px`. This means columns will have the following sizes: - col `A` will be `100px` - col `B` will be `300px` - col `F1` will be `100px` ( so a flex unit) - col `F2` will be `200px` ( so the equivalent of `2` flex units) The flexbox algorithm also uses [`viewportReservedWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#viewportReservedWidth) to determine the width of the viewport to use for sizing columns - you can use [viewportReservedWidth=100](https://infinite-table.com/docs/reference/infinite-table-props.md#viewportReservedWidth) to always have a `100px` reserved area that won't be used for flexing columns. **Example: Using viewportReservedWidth to reserve whitespace when you have flexible columns** This example has a `viewportReservedWidth` of `50px`. ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; const defaultColumnSizing: InfiniteTablePropColumnSizing = { country: { flex: 1 }, city: { flex: 1 }, salary: { flex: 2 }, }; export default function App() { const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current viewport reserved width: {viewportReservedWidth}px.

data={dataSource} primaryKey="id"> debugId="viewportReservedWidth-example" columns={columns} columnDefaultWidth={50} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} defaultColumnSizing={defaultColumnSizing} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` Take a look at the snippet below to see column sizing at work with flexible and fixed columns. **Example: Using controlled columnSizing** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [columnSizing, setColumnSizing] = React.useState({ country: { width: 100 }, city: { flex: 1, minWidth: 100 }, salary: { flex: 2, maxWidth: 500 }, }); const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current column sizing:{' '}

{JSON.stringify(columnSizing, null, 2)}
Viewport reserved width: {viewportReservedWidth} -{' '}

data={dataSource} primaryKey="id"> debugId="columnSizing-example" columns={columns} columnDefaultWidth={50} columnSizing={columnSizing} onColumnSizingChange={setColumnSizing} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` You might find [`viewportReservedWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#viewportReservedWidth) useful for advanced configuration when you have flexible columns. When he user is performing a column resize (via drag & drop), [`onViewportReservedWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#onViewportReservedWidth) is called when the resize is finished (not the case for resizing with the **SHIFT** key pressed, when adjacent columns share the space between them). You can also size (generated) group columns by using their [column.id](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.id) property. For [groupRenderStrategy="multi-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy), if no `id` is specified in the group column configuration, each column will have a generated id like this: `"group-by-${field}"`. For [groupRenderStrategy="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy), if no `id` is specified in the [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) it will default to: `"group-by"`. ## Resizing columns via drag & drop Columns are user-resizable via drag & drop. If you don't want a column to be resizable, specify [column.resizable=false](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.resizable) By default, all columns are resizable since [`resizableColumns`](https://infinite-table.com/docs/reference/infinite-table-props.md#resizableColumns) defaults to `true`. The [`resizableColumns`](https://infinite-table.com/docs/reference/infinite-table-props.md#resizableColumns) prop controls the behavior for all columns that don't explicitly specify their [column.resizable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.resizable) property. When initially rendered, columns are displayed with their [`columns.defaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth) (you can also use [`columnDefaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultWidth)) or [`columns.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex). Flexible columns take up available space taking into account their flex value, as detailed above. When the user is resizing columns (or column groups), the effect is seen in real-time, so it's very easy to adjust the columns to desired widths. After the user drops the resize handle to the desired position, [`onColumnSizingChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnSizingChange) is being called, to allow the developer to react to column sizing changes. Also [`onViewportReservedWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#onViewportReservedWidth) is called as well when the resize is finished (not the case for resizing with the **SHIFT** key pressed, when adjacent columns share the space between them). When flexible columns are resized, they are kept flexible even after the resize. Note however that their flex values will be different to the original flex values and will reflect the new proportions each flex column is taking up at the moment of the resize. More exactly, the new flex values will be the actual pixel widths. As an example, say there are 2 flex columns, first one with flex `1` and second one with flex `3` and they have an available space of `800px`. ```ts const columns = { first: { flex: 1, field: 'one' }, second: { flex: 2, field: 'two' }, }; ``` Initially they will occupy `200px` and `600px` respectively. If the user resizes them to be of equal size, [`onColumnSizingChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnSizingChange) will be called with an object like ```ts { first: { flex: 400 }, second: {flex: 400 } } ``` since those are the actual widths measured from the DOM. This works out well, even if the available space of the table grows, as the proportions will be the same. ### Resize Restrictions When resizing, the user needs to drag the resize handle to adjust the columns to new sizes. While doing so, the resize handle has a (green) color to indicate everything is okay. However, when restrictions are hit (either column [min](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.minWidth) or [max](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.maxWidth) widths), the resize handle turns red to indicate further resizing is not possible. ### Sharing space on resize By default when resizing a specific column, the following columns are pushed to the right (when making the column wider) or moved to the left (when making the column narrower). For sharing space between resizable columns when resizing, the user needs to **hold the SHIFT key** when grabbing the resize handle. When the handle is dropped and the resize confirmed, [`onColumnSizingChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnSizingChange) is called, but [`onViewportReservedWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#onViewportReservedWidth) is not called for this scenario, since the reserved width is preserved. ### Resizing column groups Just as columns are being resized, it is also possible to resize column groups. For this, the user needs to hover over the right border of the column group and start dragging the resize handle. For multi-level column groups, it's possible to resize any of them. Just grab the handle from the desired group and start dragging. The handle height will indicate which column group is being resized. If a column group has at least one resizable column, it can be resized. When resizing, the space is shared proportionally betweem all resizable columns in the group. Once a min/max limit has been reached for a certain column in the group, the column respects the limit and the other columns keep resizing as usual. When the min/max limit has been reached for all columns in the group, the resize handle turns red to indicate further resizing is no longer possible. **Example: Resizing column groups** Try resizing the `Finance` and `Regional Info` column groups. The columns in the `Finance` group can be resized an extra `30px` (they have a `maxWidth` of `130px`). ```tsx import { InfiniteTable, DataSource, InfiniteTableColumnGroup, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { currency: { field: 'currency', columnGroup: 'finance', maxWidth: 130, }, salary: { field: 'salary', columnGroup: 'finance', maxWidth: 130, }, country: { field: 'country', columnGroup: 'regionalInfo', maxWidth: 400, }, preferredLanguage: { field: 'preferredLanguage', columnGroup: 'regionalInfo', }, id: { field: 'id', defaultWidth: 80 }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, }; const columnGrous: Record = { regionalInfo: { header: 'Regional Info', }, finance: { header: 'Finance', columnGroup: 'regionalInfo', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-groups-example" columnGroups={columnGrous} columns={columns} columnDefaultWidth={100} /> ); } ``` ### Customizing the resize handle colors It's possible to customize the resize handle colors and width. For adjusting the handle colors, use the following CSS variables: - `--infinite-resize-handle-hover-background` - the color of the resize handle when it's in a `green`/all good state. - `--infinite-resize-handle-constrained-hover-background` - the color of the resize handle when it has reached a min/max constraint. You can also adjust the width of the resize handle: - `--infinite-resize-handle-width` - the width of the `green`/`red` column resize handle. Defaults to `2px` - `--infinite-resize-handle-active-area-width` - the width of the area you can hover over in order to grab the resize handle. Defaults to `20px`. The purpose of this active area is to make it easier to grab the resize handle. ## Auto-sizing columns For sizing columns to the width of their content, you can use [`autoSizeColumnsKey`](https://infinite-table.com/docs/reference/infinite-table-props.md#autoSizeColumnsKey) to declaratively auto-size columns: - when [`autoSizeColumnsKey`](https://infinite-table.com/docs/reference/infinite-table-props.md#autoSizeColumnsKey) is a `string` or `number` and the value of the prop is changed, all columns will be auto-sized. - when [`autoSizeColumnsKey`](https://infinite-table.com/docs/reference/infinite-table-props.md#autoSizeColumnsKey) is an object, it needs to have a `key` property (of type `string` or `number`), so whenever the `key` changes, the columns will be auto-sized. Specifying an object for [`autoSizeColumnsKey`](https://infinite-table.com/docs/reference/infinite-table-props.md#autoSizeColumnsKey) gives you more control over which columns are auto-sized and if the size measurements include the header or not. When an object is used, the following properties are available: - `key` - mandatory property, which, when changed, triggers the update - `includeHeader` - optional boolean, - decides whether the header will be included in the auto-sizing calculations. If not specified, `true` is assumed. - `columnsToSkip` - a list of column ids to skip from auto-sizing. If this is used, all columns except those in the list will be auto-sized. - `columnsToResize` - the list of column ids to include in auto-sizing. If this is used, only columns in the list will be auto-sized. **Example: Auto-sizing columns** ```tsx import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function GroupByExample() { const [key, setKey] = React.useState(0); const [includeHeader, setIncludeHeader] = React.useState(false); const autoSizeColumnsKey = React.useMemo(() => { return { includeHeader, key, }; }, [key, includeHeader]); return ( <>
primaryKey="id" data={dataSource}> debugId="autoSizeColumnsKey-example" autoSizeColumnsKey={autoSizeColumnsKey} columns={columns} columnDefaultWidth={200} /> ); } ``` --- # Common Issues > Avoid common pitfalls and issues when using the component. Learn how to use it properly to perform smooth and avoid jank. Canonical page: https://infinite-table.com/docs/learn/common-issues/ As people have started using `` we've noticed a few issues keep popping up. While we're trying to refine our API to be easier to use and understand, developers using the component still need to be aware of some design decisions and conventions used in the component. ## Issue: Performance degradation because props are new on every render Passing new props on every render to the `` component or to the `` component can be a performance bottleneck: ```ts ``` Instead pass the **same** reference when things do change - stored in state or any other place: ```ts const [groupBy, setGroupBy] = useState([{ field: 'country' }]); ; ``` When in dev mode, you can set `localStorage.debug = "*"` in your localstorage to see potential issues logged to the console. For example, you might see: `InfiniteTable:rerender Triggered by new values for the following props +1s columns` ## Issue: State inside custom components rendered in cells is lost while scrolling When using custom rendering or custom components for columns, make sure all your rendering logic is [controlled](https://reactjs.org/docs/forms.html#controlled-components) and that it doesn't have any local or transient state. This is important because `InfiniteTable` makes heavy use of virtualization, in both _column cells and column headers_, so **custom components can and will be unmounted and re-mounted multiple times**, during the virtualization process (triggered by user scrolling, sorting, filtering and a few other interactions). --- # Compare React DataGrids > Honest comparison of Infinite Table against AG Grid, TanStack Table, and MUI X Data Grid. Find the right React data grid for your project. Canonical page: https://infinite-table.com/docs/learn/compare/ Infinite Table was built for React from the ground up. Columns are props. Sorting, grouping, and filtering are controlled or uncontrolled values — the same pattern you use for an ``. Cell renderers are plain JSX. State lives in React. When you change a prop, the grid re-renders. Other excellent data grids take different approaches — multi-framework support, headless logic libraries, deep Material UI integration — and those approaches are the right choice for many teams. This section compares Infinite Table with three popular alternatives so you can decide which fits your project. ## Comparisons - [Infinite Table vs AG Grid](https://infinite-table.com/docs/learn/compare/ag-grid.md) — The most established enterprise data grid. Multi-framework, huge feature set. AG Grid's breadth is unmatched; Infinite Table focuses on a small, composable, React-declarative API surface. - [Infinite Table vs TanStack Table](https://infinite-table.com/docs/learn/compare/tanstack-table.md) — A headless, MIT-licensed logic library. Total rendering control — you build the UI, the virtualization, and the keyboard navigation. Infinite Table ships those built-in. - [Infinite Table vs MUI X Data Grid](https://infinite-table.com/docs/learn/compare/mui-x-data-grid.md) — A rendered data grid from the Material UI ecosystem. Excellent MUI integration. Infinite Table is design-system agnostic and includes grouping and pivoting without a Premium tier. ## Quick Comparison | | Infinite Table | AG Grid | TanStack Table | MUI X Data Grid | |---|---|---|---|---| | **Built for React** | Yes — from the ground up | Multi-framework (JS, Angular, Vue, React) | Headless — multi-framework hooks | Yes — React only | | **API style** | Small, composable props (controlled + uncontrolled) | Large, comprehensive configuration surface | Headless hooks, you provide all JSX | Declarative props, controlled + uncontrolled | | **Cell renderers** | JSX components | AG Grid component interface (React supported) | You build all rendering | JSX components | | **Frameworks** | React | React, Angular, Vue, JS | All Most Popular | React | | **Virtualization** | Row + column | Row + column | BYO (separate package) | Row (column in Pro+) | | **Grouping** | Included (free) | Enterprise license | Logic only (free) | Premium plan ($599/dev/yr) | | **Pivoting** | Included (free) | Enterprise license | Logic only (free) | Premium plan ($599/dev/yr) | | **Tree data** | Included (free) | Enterprise license | Logic only (free) | Pro plan ($299/dev/yr) | | **License** | Free with footer; paid removes footer | Community MIT; Enterprise proprietary | MIT | Community MIT; Pro/Premium/Enterprise proprietary | | **Paid license** | [$395/dev/year](https://infinite-table.com/pricing) | [~$999/dev/year](https://www.ag-grid.com/license-pricing) | Free | [$299/dev/year (Pro)](https://mui.com/pricing/) | Feature availability is based on each product's official documentation as of mid-2026. Always verify on the vendor's site before making a purchasing decision. ## How to decide **Pick Infinite Table** if you want a data grid that feels like a native React component — a small, composable API of declarative props, controlled state, and JSX renderers — with grouping, pivoting, and aggregations included out of the box, no enterprise license required. **Pick AG Grid** if you need multi-framework support (Angular, Vue, and React under one grid), the widest possible feature surface (charting, clipboard, server-side row model), or your team already knows AG Grid from other projects and values the breadth it provides. **Pick TanStack Table** if you want total control over rendering and are prepared to build your own UI layer, virtualization, keyboard navigation, and accessibility. Best for design-system component libraries or lightweight tables that don't need complex built-in features. **Pick MUI X Data Grid** if your application is already built on Material UI and you value automatic theme integration with MUI's design system, or you need the broader MUI X component suite (date pickers, charts, tree view) under a single license. How Infinite Table's React-declarative surface compares with AG Grid's multi-framework approach. A rendered, virtualized DataGrid versus a headless table library you assemble yourself. What ships in Infinite Table's free build versus MUI X Community, Pro, and Premium. Grouping, aggregations, and pivoting — included in every Infinite Table build. ## Help us keep these comparisons up-to-date These pages are our reading of each product's public documentation and pricing as of mid-2026. We want them to stay accurate. If you work on AG Grid, TanStack Table, or MUI X — or you've spotted something that's wrong, outdated, or missing context — please tell us. We will update the page. - [File a correction issue](https://github.com/infinite-table/infinite-react/issues/new?template=compare_page_correction.md&title=Compare%20page%20correction%3A%20) - [Edit the source on GitHub](https://github.com/infinite-table/infinite-react/tree/master/www/content/docs/learn/compare) and open a pull request - Email [admin@infinite-table.com](mailto:admin@infinite-table.com?subject=Compare%20page%20correction) with the page URL and what should change --- # Infinite Table vs AG Grid > A detailed comparison of Infinite Table and AG Grid for React. Two different approaches to building data grids — and when each one is the right choice. Canonical page: https://infinite-table.com/docs/learn/compare/ag-grid [AG Grid](https://www.ag-grid.com/) is the most established commercial data grid on the market, used across React, Angular, Vue, and plain JavaScript. It has a massive feature surface and a large community. The AG Grid team has been shipping for over a decade and the result is an impressively comprehensive product. The core difference is not price. It's how each grid relates to React. ## Two different approaches to React AG Grid supports four frameworks from a single codebase. That multi-framework architecture is a genuine strength — it means your organisation can standardise on one grid across Angular, Vue, and React projects. The trade-off is that AG Grid's core is framework-agnostic: state and rendering live inside the grid engine, with a React adapter layer on top. Configuration goes through a `gridOptions` object, and many operations use imperative API calls like `api.setColumnDefs()` or `api.refreshCells()`. Infinite Table is built for React, and that choice shapes the API. Columns, sorting, grouping, and filtering are React props — controlled or uncontrolled, like any React form component. Cell renderers are plain JSX. The grid participates in React's component tree, re-rendering when props change. Neither approach is wrong. They reflect different design priorities: AG Grid optimises for framework reach and breadth; Infinite Table optimises for feeling native to React. ## What this looks like in code With AG Grid, updating columns typically goes through the API: ```tsx // AG Grid: update columns via the imperative API const onButtonClick = () => { gridRef.current.api.setColumnDefs(newColumnDefs); gridRef.current.api.refreshCells({ force: true }); }; ``` With Infinite Table, the same operation is a state change: ```tsx // Infinite Table: update columns via React state const [columns, setColumns] = useState(initialColumns); const onButtonClick = () => { setColumns(newColumns); // grid re-renders automatically }; primaryKey="id" data={dataSource}> columns={columns} /> ``` The same `value` / `onChange` pattern you use for a React `` works for the entire grid. ## API surface: breadth vs composability AG Grid covers a huge number of enterprise use cases, and its API reflects that breadth. The [`GridOptions` interface](https://www.ag-grid.com/react-data-grid/grid-options/) spans hundreds of props across 25+ categories — from row grouping and pivoting to charting, clipboard, and server-side row models. For teams that need that coverage, it's all there. That breadth naturally comes with complexity. Over a decade of multi-framework development, the API has grown to include multiple ways to configure the same behaviour — for example, overlays can be customised through `overlayComponent`, or through the older `loadingOverlayComponent` and `noRowsOverlayComponent`, or through template strings, all of which still work. Row grouping alone involves roughly 20 grid-level props. These aren't flaws — they're the result of supporting many years of backwards compatibility across four frameworks, and teams that rely on those options are glad they exist. Infinite Table makes a different trade-off: keep the API surface small and composable. - **Function props as building blocks.** The [`groupColumn`](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md) prop can be a column object (single group column) or a function (called for each generated column). One prop, two behaviours, composed through the same mechanism. - **Controlled and uncontrolled variants.** Want to manage sorting yourself? Pass `sortInfo` (controlled). Want the grid to handle it? Pass `defaultSortInfo` (uncontrolled). The same pattern as `value` vs `defaultValue` on a React ``. - **Fewer props to coordinate.** Infinite Table ships grouping, pivoting, and aggregations — but with a smaller configuration surface. The bet is that fewer, more composable props are easier to learn and reason about for React teams. These are genuinely different philosophies. AG Grid's large surface means there's usually a dedicated prop for any specific requirement. Infinite Table's smaller surface means you compose general-purpose building blocks to get there. ## Architecture | | Infinite Table | AG Grid | |---|---|---| | **Built for** | React | Multi-framework (JS, Angular, Vue, React) | | **React integration** | Native — renders through React's reconciler | Framework-agnostic core with React adapter | | **API style** | Declarative props, controlled + uncontrolled | Comprehensive configuration object, imperative API | | **Cell renderers** | Plain JSX components | AG Grid component interface (React components supported) | | **State management** | Lives in React (useState, context, external stores) | Lives inside the grid; synced to React via callbacks | | **TypeScript** | Written in TypeScript, first-class types | Written in TypeScript, first-class types | | **Virtualization** | Row + column | Row + column | ## Feature Comparison | Feature | Infinite Table (free) | AG Grid Community (free) | AG Grid Enterprise (paid) | |---|---|---|---| | Sorting (single + multi) | ✅ | ✅ | ✅ | | Column filtering | ✅ | ✅ | ✅ | | Column resizing | ✅ | ✅ | ✅ | | Column reordering | ✅ | ✅ | ✅ | | Column pinning | ✅ | ✅ | ✅ | | Column grouping (headers) | ✅ | ✅ | ✅ | | Row grouping | ✅ | 🔴 | ✅ | | Aggregations | ✅ | 🔴 | ✅ | | Pivoting | ✅ | 🔴 | ✅ | | Tree data | ✅ | 🔴 | ✅ | | Master-detail | ✅ | 🔴 | ✅ | | Lazy loading | ✅ | 🔴 | ✅ (server-side row model) | | Live pagination | ✅ | 🔴 | ✅ | | Row + column virtualization | ✅ | ✅ | ✅ | | Cell editing | ✅ | ✅ | ✅ | | Cell selection | ✅ | 🔴 | ✅ (range selection) | | Row selection | ✅ | ✅ | ✅ | | Context menus | ✅ | 🔴 | ✅ | | Keyboard navigation | ✅ | ✅ | ✅ | | Theming (CSS variables) | ✅ | ✅ | ✅ | | Excel export | 🔴 | 🔴 | ✅ | | Clipboard | 🔴 | 🔴 | ✅ | | Integrated charting | 🔴 | 🔴 | ✅ | | Server-side row model | ✅ | 🔴 | ✅ | | Status bar / sidebar panels | 🔴 | 🔴 | ✅ | AG Grid Community (MIT) does not include row grouping, pivoting, aggregations, tree data, or master-detail. Those require AG Grid Enterprise. Infinite Table includes all of these in the free build (with a "Powered by Infinite Table" footer). A paid license key removes the footer. ## Pricing | | Infinite Table | AG Grid Enterprise | |---|---|---| | **Starting price** | [$395/dev/year](https://infinite-table.com/pricing) | [~$999/dev/year](https://www.ag-grid.com/license-pricing) | | **Volume discount** | 5% at 3 devs, 10% at 5, 15% at 10 | Contact sales | | **Deployment license** | None required | None required | | **Free tier** | All features, footer displayed | Community edition (grouping/pivot excluded) | | **Support** | Email (paid license) | Zendesk (Enterprise license) | ## When AG Grid is the better choice - **Multi-framework projects.** If you need the same data grid across Angular, Vue, and React codebases, AG Grid is the clear choice — Infinite Table is React-only. Standardising on one grid across frameworks saves training time and keeps behaviour consistent. - **The widest feature surface.** AG Grid Enterprise includes built-in charting, clipboard, Excel export, column tool panels, status bars, and a full server-side row model with partial store. If you need several of these features, AG Grid covers them all in one package. No other grid matches this breadth. - **Enormous community and ecosystem.** With over 1M weekly npm downloads and 13k+ GitHub stars, AG Grid has the deepest community resources, Stack Overflow coverage, and third-party integrations of any data grid. Ecosystem maturity matters — and AG Grid's is unmatched. - **Your team already knows AG Grid.** If your developers are experienced with AG Grid's API and patterns from other projects, that familiarity has real value. Switching to a different grid has a learning cost, and AG Grid's comprehensive documentation makes it possible to find an answer for almost any scenario. - **You need the dedicated configuration options.** AG Grid's large API means there's often a purpose-built prop for a specific edge case. If you regularly need that level of fine-grained, per-feature configuration, the breadth of the API is a strength. ## When Infinite Table is the better fit - **You want the grid to feel like React.** Infinite Table's API is props, controlled state, and JSX — the same patterns you use in every other React component. If your team thinks in React, Infinite Table fits that mental model. - **You prefer a small, composable API.** Infinite Table ships grouping, pivoting, and aggregations with a compact configuration surface. Function props as building blocks, controlled/uncontrolled patterns for state, composable rather than exhaustive. A different bet from AG Grid's comprehensive approach — one that suits teams who want fewer props to learn and coordinate. - **You need grouping, pivoting, and aggregations without an enterprise license.** These are included in Infinite Table's free build. AG Grid reserves them for the Enterprise tier. - **Simpler licensing.** One plan, one key for the whole team, no deployment license. Install the package, render your first DataGrid, and learn how `` and `` work together. Grouping, aggregations, and pivoting — included without an Enterprise license. Use Infinite Table free with a footer, or buy a license to remove it and get email support. Read AG Grid's official React getting started guide. ## Help us keep this comparison up-to-date This page is our reading of AG Grid's public docs and pricing as of mid-2026. We want it to stay accurate. If you work on AG Grid — or you've spotted something that's wrong, outdated, or missing context — please tell us. We will update the page. - [Edit this page on GitHub](https://github.com/infinite-table/infinite-react/edit/master/www/content/docs/learn/compare/ag-grid.page.md) and open a pull request - [File a correction issue](https://github.com/infinite-table/infinite-react/issues/new?template=compare_page_correction.md&title=Compare%20page%20correction%3A%20AG%20Grid) - Email [admin@infinite-table.com](mailto:admin@infinite-table.com?subject=Compare%20page%20correction%3A%20AG%20Grid) with the URL and what should change --- # Infinite Table vs MUI X Data Grid > A detailed comparison of Infinite Table and MUI X Data Grid. React-native design approaches, features across tiers, and when MUI X Data Grid is the better choice. Canonical page: https://infinite-table.com/docs/learn/compare/mui-x-data-grid [MUI X Data Grid](https://mui.com/x/react-data-grid/) is a React data grid from the Material UI team. It's part of the broader MUI X suite (date pickers, charts, tree view) and follows Material Design conventions. Like Infinite Table, it's React-only and uses a declarative, prop-driven API. These two grids have more in common architecturally than either has with AG Grid or TanStack Table. Both render through React, both use props and controlled state, both support JSX cell renderers. The differences are in design-system coupling, feature availability across tiers, and how each grid's API is structured. ## Where they diverge **Design-system coupling.** MUI X Data Grid is built on Material UI. It inherits your MUI theme tokens — palette, spacing, typography — automatically. This is a major advantage if your app already uses MUI. If your app uses a different design system, you'll be adding MUI's styling infrastructure (`@emotion`, theme provider, `sx` prop) as dependencies alongside your existing stack. Infinite Table is design-system agnostic. Theming is done through CSS variables — you can integrate with Tailwind, vanilla CSS, or any design system without extra dependencies. There's no coupling to a specific component library. **Feature availability.** MUI X uses a four-tier model: Community (free), Pro ($299/dev/yr), Premium ($599/dev/yr), and Enterprise ($1,399/dev/yr). Core features like column resizing, pinning, and tree data require at least Pro. Grouping, pivoting, and aggregations require Premium. Infinite Table includes all of these in the package (only one package, no separate community and enterprise packages). A "Powered by Infinite Table" footer is displayed; a [paid license ($395/dev/year)](https://infinite-table.com/pricing) removes it. **Data layer separation.** Infinite Table splits data management and rendering into two React components — `` and ``. The `` handles fetching, sorting, grouping, pivoting, and filtering; the `` handles rendering. You can even use `` with your own custom component. MUI X Data Grid is a single component that handles both data and rendering internally. ## Architecture | | Infinite Table | MUI X Data Grid | |---|---|---| | **Framework** | React | React | | **Design system** | Agnostic — CSS variables | Material UI — MUI theme system | | **Component model** | Two components: `` + `` | Single `` / `` / `` component | | **API style** | Declarative props, controlled + uncontrolled | Declarative props, controlled + uncontrolled | | **Cell renderers** | JSX components via column `render` prop | JSX components via `renderCell` slot | | **TypeScript** | Written in TypeScript | Written in TypeScript | | **Virtualization** | Row + column | Row virtualization; column virtualization in Pro+ | | **Packages** | Single package, all features | Separate packages per tier | ## Feature Comparison | Feature | Infinite Table (free) | MUI X Community (free) | MUI X Pro ($299/dev/yr) | MUI X Premium ($599/dev/yr) | |---|---|---|---|---| | Sorting (single + multi) | ✅ | ✅ (single) | ✅ (multi) | ✅ (multi) | | Column filtering | ✅ | ✅ (single) | ✅ (multi) | ✅ (multi) | | Column resizing | ✅ | 🔴 | ✅ | ✅ | | Column reordering | ✅ | 🔴 | ✅ | ✅ | | Column pinning | ✅ | 🔴 | ✅ | ✅ | | Column grouping (headers) | ✅ | ✅ | ✅ | ✅ | | Row grouping | ✅ | 🔴 | 🔴 | ✅ | | Aggregations | ✅ | 🔴 | 🔴 | ✅ | | Pivoting | ✅ | 🔴 | 🔴 | ✅ | | Tree data | ✅ | 🔴 | ✅ | ✅ | | Master-detail | ✅ | 🔴 | ✅ | ✅ | | Row virtualization | ✅ | ✅ | ✅ | ✅ | | Column virtualization | ✅ | 🔴 | ✅ | ✅ | | Cell editing | ✅ | ✅ | ✅ | ✅ | | Cell selection | ✅ | 🔴 | 🔴 | ✅ | | Row selection | ✅ | ✅ | ✅ | ✅ | | Keyboard navigation | ✅ | ✅ | ✅ | ✅ | | Lazy loading | ✅ | 🔴 | ✅ (server-side) | ✅ | | Live pagination | ✅ | 🔴 | 🔴 | 🔴 | | Context menus | ✅ | 🔴 | 🔴 | 🔴 | | Excel export | 🔴 | 🔴 | 🔴 | ✅ | | Clipboard (copy/paste) | 🔴 | 🔴 | 🔴 | ✅ | MUI X uses a tiered model: row grouping, pivoting, aggregations, and cell selection require the Premium plan ($599/dev/year). Column resizing, pinning, reordering, tree data, and master-detail require at least Pro ($299/dev/year). Feature details are from the [MUI pricing page](https://mui.com/pricing/). Infinite Table includes all of these features in the free Community build. ## Pricing | | Infinite Table | MUI X Pro | MUI X Premium | MUI X Enterprise | |---|---|---|---|---| | **Price** | [$395/dev/year](https://infinite-table.com/pricing) | [$299/dev/year](https://mui.com/pricing/) | [$599/dev/year](https://mui.com/pricing/) | [$1,399/dev/year](https://mui.com/pricing/) | | **Grouping + pivoting** | ✅ (free) | 🔴 | ✅ | ✅ | | **Tree data + master-detail** | ✅ (free) | ✅ | ✅ | ✅ | | **Column resizing + pinning** | ✅ (free) | ✅ | ✅ | ✅ | | **Deployment license** | None | None | None | None | | **Support** | Email (paid) | Community | Priority over Community | Priority over Pro | ## When MUI X Data Grid is the better choice - **You're already in the MUI ecosystem.** If your app uses Material UI, MUI X Data Grid inherits your MUI theme automatically — palette, spacing, typography, dark mode — with zero configuration. Infinite Table uses CSS variables and won't pick up MUI theme tokens automatically. - **You need the full MUI X suite.** MUI X includes date pickers, charts, tree view, and a scheduler under a single license. If you need multiple MUI X components, a Pro or Premium license covers them all. - **Material Design consistency.** The Data Grid follows Material Design patterns by default. If your design spec is Material Design, MUI X is the most natural fit. - **Large community.** MUI has a very large user community — millions of weekly npm downloads for Material UI. More community resources, tutorials, and third-party integrations. - **Column-level features at the Pro tier.** If you need column resizing, pinning, reordering, and tree data but not grouping or pivoting, MUI X Pro at $299/dev/year covers those features along with all other MUI X Pro components. ## When Infinite Table is the better fit - **You need grouping, pivoting, and aggregations without the Premium tier.** These are free in Infinite Table's Community build. MUI X requires Premium ($599/dev/year) for the same features. - **You're not using Material UI.** If your app uses Tailwind, vanilla CSS, or another design system, Infinite Table may be a simpler fit — it uses plain CSS variables and works with any styling approach without adding a design-system dependency. - **Data layer separation.** Infinite Table's `` / `` split gives you a clean separation between data management (fetching, sorting, grouping, pivoting, filtering) and rendering. You can even replace `` with your own component and keep the data layer. - **Column virtualization on the free tier.** Infinite Table virtualizes both rows and columns by default. MUI X Community only virtualizes rows up to 100 rows; column virtualization and unlimited row virtualization require Pro. - **Live pagination and context menus.** Infinite Table includes built-in live pagination and context menus. MUI X does not offer equivalents at any tier. - **Single-tier licensing.** Infinite Table has one plan with all features included. MUI X offers four tiers, which gives you flexibility to pay only for what you need — but also means checking which tier covers each feature. Install the package, render your first DataGrid, and learn how `` and `` work together. Theme Infinite Table without a Material UI theme provider — CSS variables only. Use Infinite Table free with a footer, or buy a license to remove it and get email support. Read MUI X Data Grid's official documentation. ## Help us keep this comparison up-to-date This page is our reading of MUI X Data Grid's public docs and [pricing page](https://mui.com/pricing/) as of mid-2026. We want it to stay accurate. If you work on MUI X — or you've spotted something that's wrong, outdated, or missing context — please tell us. We will update the page. - [Edit this page on GitHub](https://github.com/infinite-table/infinite-react/edit/master/www/content/docs/learn/compare/mui-x-data-grid.page.md) and open a pull request - [File a correction issue](https://github.com/infinite-table/infinite-react/issues/new?template=compare_page_correction.md&title=Compare%20page%20correction%3A%20MUI%20X%20Data%20Grid) - Email [admin@infinite-table.com](mailto:admin@infinite-table.com?subject=Compare%20page%20correction%3A%20MUI%20X%20Data%20Grid) with the URL and what should change --- # Infinite Table vs TanStack Table > A detailed comparison of Infinite Table and TanStack Table. Declarative rendered grid vs headless logic library — architecture, features, and when TanStack Table is the better choice. Canonical page: https://infinite-table.com/docs/learn/compare/tanstack-table [TanStack Table](https://tanstack.com/table) (formerly React Table) is a popular, MIT-licensed headless table library. It provides table logic — sorting, filtering, grouping, pagination — but no UI. You bring your own JSX, your own styles, your own virtualization, your own keyboard navigation. Infinite Table is a fully rendered React data grid. You pass props — columns, data, grouping configuration — and get a complete, virtualized, keyboard-navigable grid with theming out of the box. Both are valid approaches. The question is where you want to spend your engineering time. ## Two ends of a spectrum Data grids range from fully rendered components to headless logic libraries. At one end is AG Grid — a comprehensive, multi-framework grid that handles everything. At the other end is TanStack Table — headless hooks that give you total rendering control and require you to build everything visible. Infinite Table sits in the middle: **a declarative React component that ships the grid**. You get a React-native API (props, controlled state, JSX cell renderers) without having to construct the table markup, virtualization, focus management, and accessibility yourself. ```tsx // TanStack Table: you provide all the JSX const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }); return ( {table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map(header => ( ))} ))} {table.getRowModel().rows.map(row => ( {row.getVisibleCells().map(cell => ( ))} ))}
{flexRender(header.column.columnDef.header, header.getContext())}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
); ``` ```tsx // Infinite Table: declarative props, grid ships complete primaryKey="id" data={dataSource}> columns={columns} />
``` Both examples display tabular data. The first gives you total control over every `` and ``. The second gives you a working grid — virtualized, keyboard-navigable, themeable — in two components. ## Architecture | | Infinite Table | TanStack Table | |---|---|---| | **Type** | Rendered React component | Headless logic library | | **What you get** | Full UI: virtualized grid, headers, cells, scrollbars, keyboard nav, theming | Table state + utilities; you provide all DOM and styling | | **API style** | Declarative props (controlled + uncontrolled) | Hooks that return row/cell models; you render everything | | **Cell rendering** | JSX — pass a React component as a column prop | JSX - Use the `FlexRender` component | | **Frameworks** | React | All Most Popular | | **Virtualization** | Built-in row + column virtualization | Not included; pair with [TanStack Virtual](https://tanstack.com/virtual) or your own | | **TypeScript** | First-class | First-class | | **Bundle** | Single package, includes CSS | Tiny core; total size depends on what you build on top | ## Feature Comparison | Feature | Infinite Table | TanStack Table | |---|---|---| | Sorting | ✅ | 🔴 (logic only) | | Column filtering | ✅ | 🔴 (logic only) | | Row grouping | ✅ | 🔴 (logic only) | | Aggregations | ✅ | 🔴 (logic only) | | Pivoting | ✅ | 🔴 (logic only) | | Tree data | ✅ | 🔴 (logic only) | | Column resizing | ✅ | 🔴 (logic helpers) | | Column reordering | ✅ | 🔴 | | Column pinning | ✅ | 🔴 (logic helpers) | | Cell editing | ✅ | 🔴 | | Cell selection | ✅ | 🔴 | | Row selection | ✅ | 🔴 (logic only) | | Keyboard navigation | ✅ | 🔴 | | Context menus | ✅ | 🔴 | | Master-detail | ✅ | 🔴 | | Lazy loading / live pagination | ✅ | 🔴 (pagination logic available) | | Row + column virtualization | ✅ | 🔴 (use TanStack Virtual) | | Theming | ✅ | 🔴 | "Logic only" means TanStack Table handles the state and computations, but you write all the JSX, CSS, event handlers, and accessibility attributes. This is powerful but requires substantial development effort for a production-grade data grid. ## What "headless" means in practice With TanStack Table, building a production data grid involves: 1. Rendering the ``, ``, ``, ``, `
` (or `
`-based layout) yourself. 2. Wiring up virtualization (typically TanStack Virtual) for large datasets. 3. Building filter UIs, sort indicators, group expand/collapse toggles, resize handles, and column reorder drag-and-drop. 4. Handling keyboard navigation and ARIA attributes for accessibility. 5. Styling everything from scratch or integrating with your design system. This is the right approach when you need pixel-perfect control or are building a design-system component library. But it means weeks of work to reach feature parity with a rendered grid — and that code becomes yours to maintain. With Infinite Table, grouping, pivoting, filtering, virtualization, keyboard navigation, and theming work the moment you render the component. You customise through props and JSX cell renderers, not by rebuilding the grid's internals. ## Pricing TanStack Table is MIT-licensed and free. There is no paid tier. Infinite Table is also free — all features included — but displays a "Powered by Infinite Table" footer. A [paid license ($395/dev/year)](https://infinite-table.com/pricing) removes the footer and adds email support. ## When TanStack Table is the better choice - **Total rendering control.** You need pixel-perfect custom UI, or you're building a table component for a design-system library where the rendered output must match your design spec exactly. - **Multi-framework.** TanStack Table works across React, Vue, Solid, and Svelte. Infinite Table is React-only. - **Minimal bundle.** If you only need sorting and basic filtering on a small dataset (no virtualization, no grouping), TanStack Table's core is smaller than any full grid component. - **Zero restrictions.** MIT license with no footer, no license key, no terms beyond MIT. - **Existing investment.** If your team has already built a mature grid UI on top of TanStack Table, migrating to a rendered grid may not justify the effort. ## When Infinite Table is the better fit - **You want to ship the grid, not build it.** Infinite Table delivers a production-ready grid — virtualized, keyboard-navigable, themed — out of the box. You focus on your product, not on re-implementing table infrastructure. - **You want native React API feel without the assembly work.** TanStack Table gives you hooks and row models; you assemble the JSX. Infinite Table gives you declarative props and controlled state — the same patterns you use in every other React component — but ships the complete UI so you don't have to build it yourself. Of course you still have a lot of control over column cells, headers, filters, etc. - **Complex data features built in.** Master-detail, tree grids, lazy loading, live pagination, cell editing, cell selection, and context menus are all included. Building these on top of TanStack Table is a significant engineering project. - **Accessibility and keyboard support.** Infinite Table includes keyboard navigation and focus management. With TanStack Table, you implement these yourself. Install the package, render your first DataGrid, and learn how `` and `` work together. Built-in grouping, aggregations, and pivoting — without writing the UI layer yourself. Use Infinite Table free with a footer, or buy a license to remove it and get email support. Read TanStack Table's official introduction. ## Help us keep this comparison up-to-date This page is our reading of TanStack Table's public docs as of mid-2026. We want it to stay accurate. If you work on TanStack Table — or you've spotted something that's wrong, outdated, or missing context — please tell us. We will update the page. - [Edit this page on GitHub](https://github.com/infinite-table/infinite-react/edit/master/www/content/docs/learn/compare/tanstack-table.page.md) and open a pull request - [File a correction issue](https://github.com/infinite-table/infinite-react/issues/new?template=compare_page_correction.md&title=Compare%20page%20correction%3A%20TanStack%20Table) - Email [admin@infinite-table.com](mailto:admin@infinite-table.com?subject=Compare%20page%20correction%3A%20TanStack%20Table) with the URL and what should change --- # Using Context Menus > InfiniteTable DataGrid allows you to easily configure context menus for any row and cell in the table and for the whole table body. Canonical page: https://infinite-table.com/docs/learn/context-menus/using-context-menus The easiest way to configure a context menu is to provide the [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) callback function and use it to return the menu items you want to show in the context menu. ```tsx const getCellContextMenuItems = ({ column, value }) => { if (column.id === 'currency') { return [ { label: `Convert ${value}`, key: 'currency-convert', }, ]; } if (column.id === 'age') { return null; } return [ { label: `Welcome ${value}`, key: 'hi', }, ]; }; data={data} primaryKey="id"> getCellContextMenuItems={getCellContextMenuItems} columns={columns} /> ; ``` **Example: Using context menus** Right-click any cell in the table to see the custom context menu. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { stack: { field: 'stack', header: 'Stack', }, firstName: { field: 'firstName', header: 'Name', }, age: { field: 'age', header: 'Age', }, hobby: { field: 'hobby', header: 'Hobby', }, preferredLanguage: { header: 'Language', field: 'preferredLanguage', }, }; export default function App() { return ( <> primaryKey="id" data={dataSource}> debugId="basic-cells-context-menu-example" columns={columns} getCellContextMenuItems={({ data, column }) => { return [ { key: 'hello', label: `Hello, ${data?.lastName} ${data?.firstName}`, onClick: () => { alert(`Hello, ${data?.lastName} ${data?.firstName}`); }, }, { key: 'col', label: `Current clicked column: ${column.header}`, }, { key: 'learn', label: `Learn`, menu: { items: [ { key: 'backend', label: 'Backend', onClick: () => { alert( `Learn Backend, ${data?.lastName} ${data?.firstName}`, ); }, }, { key: 'frontend', label: 'Frontend', onClick: () => { alert( `Learn Frontend, ${data?.lastName} ${data?.firstName}`, ); }, }, ], }, }, ]; }} /> ); } ``` The [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) function can return one of the following: - `null` - no custom context menu will be displayed, the default context menu will be shown (default event behavior not prevented) - `[]` - an empty array - no custom context menu will be displayed, but the default context menu is not shown - the default event behavior is prevented - `Array` - an array of menu items to be displayed in the context menu - each `MenuItem` should have: - a unique `key` property, - a `label` property with the value to display in the menu cell - it's called `label` because this is the name of the default column in the context menu - an optional `onClick` callback function to handle the click event on the menu item. In addition, if you need to configure the context menu to have other columns rather than the default column (named `label`), you can do so by returning an object with `columns` and `items`: ```tsx const getCellContextMenuItems = () => { return { columns: [{ name: 'label' }, { name: 'lcon' }], items: [ { label: 'Welcome', icon: '👋', key: 'hi', onAction: () => { // do something }, hideMenuOnAction: true, }, { label: 'Convert', icon: '🔁', key: 'convert', }, ], }; }; ``` **Example: Customising columns in the context menu** Right-click any cell in the table to see a context menu with multiple columns (`icon`, `label` and `description`). ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { stack: { field: 'stack', header: 'Stack', }, firstName: { field: 'firstName', header: 'Name', }, age: { field: 'age', header: 'Age', }, hobby: { field: 'hobby', header: 'Hobby', }, preferredLanguage: { header: 'Language', field: 'preferredLanguage', }, }; export default function App() { return ( <> primaryKey="id" data={dataSource}> debugId="custom-columns-context-menu-example" columns={columns} columnDefaultEditable getCellContextMenuItems={({ data, column }) => { const columns = [ { name: 'icon' }, { name: 'label' }, { name: 'description' }, ]; return { columns, items: [ { key: 'hello', icon: '👋', label: `Hello, ${data?.lastName} ${data?.firstName}`, description: `This is a description for ${data?.lastName}`, onClick: () => { alert(`Hello, ${data?.lastName} ${data?.firstName}`); }, }, { key: 'col', icon: '🙌', label: `Column: ${column.header}`, description: `Current clicked column: ${column.header}`, }, { key: 'learn', icon: '📚', label: `Learn`, description: `Learn more about ${data?.preferredLanguage}`, menu: { columns, items: [ { key: 'backend', label: 'Backend', icon: '👨‍💻', description: 'In the Backend', onClick: () => { alert( `Learn Backend, ${data?.lastName} ${data?.firstName}`, ); }, }, { key: 'frontend', label: 'Frontend', icon: '👨‍💻', description: 'In the Frontend', onClick: () => { alert( `Learn Frontend, ${data?.lastName} ${data?.firstName}`, ); }, }, ], }, }, ], }; }} /> ); } ``` ## Context Menus for the Table Body You might want to show a context menu for the table body, when the user right-clicks outside of any existing cell. For this, you can use the [`getContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getContextMenuItems) prop. This function has almost the same signature as [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems), with the following differences in the object passed as first parameter: - all cell-related properties (`column`, `data`, `value`, etc) can be `undefined` - it contains an `event` property with the original event object for the right-click event **Example: Context menu for outside cells** Right-click outside cells in the table to see a context menu for the table body. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'Name', defaultWidth: 120, }, age: { field: 'age', header: 'Age', defaultWidth: 100, }, preferredLanguage: { header: 'Language', defaultWidth: 120, field: 'preferredLanguage', }, }; export default function App() { return ( <> primaryKey="id" data={dataSource}> debugId="table-context-menu-example" columns={columns} getContextMenuItems={({ data, column }) => { if (!data) return [ { key: 'add', label: 'Add Item', onClick: () => { alert('Add Item'); }, }, ]; return [ { key: 'hello', label: `Hello, ${data?.lastName} ${data?.firstName}`, onClick: () => { alert(`Hello, ${data?.lastName} ${data?.firstName}`); }, }, { key: 'col', label: `Current clicked column: ${column?.header}`, }, { key: 'learn', label: `Learn`, menu: { items: [ { key: 'backend', label: 'Backend', onClick: () => { alert( `Learn Backend, ${data?.lastName} ${data?.firstName}`, ); }, }, { key: 'frontend', label: 'Frontend', onClick: () => { alert( `Learn Frontend, ${data?.lastName} ${data?.firstName}`, ); }, }, ], }, }, ]; }} /> ); } ``` ## Hiding the Context Menu To hide the context menu when you click a menu item, you can use the `hideMenuOnAction` property on the menu item. Alternatively, you can use the object passed in as a parameter to the `item.onAction` callback function to hide the menu: ```tsx {12} const getCellContextMenuItems = () => { return { items: [ { label: 'Hello', key: 'hi', onAction: ({ key, hideMenu }) => { // do something console.log('Hello'); // hide the menu hideMenu(); }, }, ], }; }; ``` The third option is to use the [`hideContextMenu`](https://infinite-table.com/docs/reference/api/index.md#hideContextMenu) function in the [API](https://infinite-table.com/docs/reference/api/index.md). --- # Column Editors > Learn how to use configure editors for columns in Infinite Table Canonical page: https://infinite-table.com/docs/learn/editing/column-editors For now, Infinite Table comes with a default built-in editor that's rendered when editing starts on any editable cell. It's very easy to configure columns with your own custom editors via the [`columns.components.Editor`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.Editor) property. ```tsx const columns: InfiniteTablePropColumns = { canDesign: { field: 'canDesign', defaultEditable: true, components: { // don't forget to provide an implementation // for the BoolEditor component Editor: BoolEditor, }, }, id: { field: 'id', }, }; ``` For now, we're not shipping any extra editors with Infinite Table. There are a few reasons for that: - we want to keep our bundle size small - we're aware people have their own preferences - especially **select/combo boxes** and **date pickers** are very complex components on their own and there are many different popular alternatives many teams already use in their projects So in this page and other parts of the docs, we'll use some popular alternatives, to show how to integrate them with Infinite Table. ## Using Date Editors A common use-case is integrating date editors, so in the following example we'll use the [MUI X Date Picker](https://mui.com/x/react-date-pickers/date-picker/) component. **Example: Using MUI X Date Picker for editing dates in the DataGrid** This is a basic example integrating with the [MUI X Date Picker](https://mui.com/x/react-date-pickers/date-picker/) - click any cell in the **Birth Date** column to show the date picker. ```ts import { InfiniteTable, DataSource, useInfiniteColumnEditor, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import { StyledEngineProvider } from '@mui/material/styles'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import * as _emotionStyled from '@emotion/styled'; import * as _emotionReact from '@emotion/react'; import * as React from 'react'; type Developer = { birthDate: Date; id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; }; const DATE_FORMAT = 'YYYY-MM-DD'; const DateEditor = () => { const { value, confirmEdit, cancelEdit } = useInfiniteColumnEditor(); const day = dayjs(value); return ( { if (day) { confirmEdit(day.toDate()); } }} /> ); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, birthDate: { field: 'birthDate', header: 'Birth Date', // we need to specify the type of the column as "date" type: 'date', defaultEditable: true, defaultWidth: 200, components: { Editor: DateEditor, }, style: ({ inEdit }) => { return inEdit ? { padding: 0 } : {}; }, renderValue: ({ value }: { value: Date }) => { return {dayjs(value).format(DATE_FORMAT)}; }, }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="date-editor-example" columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', birthDate: new Date(1997, 0, 1), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', birthDate: new Date(1993, 3, 10), currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', birthDate: new Date(1997, 10, 30), currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', birthDate: new Date(1990, 5, 20), currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', birthDate: new Date(1990, 3, 20), currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', birthDate: new Date(2002, 3, 20), currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', birthDate: new Date(1992, 11, 12), currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', birthDate: new Date(1990, 9, 5), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', birthDate: new Date(1990, 9, 15), currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', birthDate: new Date(1990, 4, 18), currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` ## Configure Editors for Column Types When you have more than one column that needs to use the same editor, you can use the [column types](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) and associate the editor with the column type. After defining your generic column types, make sure you assign them to the columns that need that specific type **Example: Using MUI X Date Picker with custom 'date' type columns** This is a basic example integrating with the [MUI X Date Picker](https://mui.com/x/react-date-pickers/date-picker/) - click any cell in the **Birth Date** or **Date Hired** columns to show the date picker. This example uses the [column types](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) to give each date column the same editor and styling. ```ts import { InfiniteTable, DataSource, useInfiniteColumnEditor, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import { StyledEngineProvider } from '@mui/material/styles'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import * as _emotionStyled from '@emotion/styled'; import * as _emotionReact from '@emotion/react'; import * as React from 'react'; type Developer = { birthDate: Date; dateHired: Date; id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; }; const DATE_FORMAT = 'YYYY-MM-DD'; const DateEditor = () => { const { value, confirmEdit, cancelEdit } = useInfiniteColumnEditor(); const day = dayjs(value); return ( { if (day) { confirmEdit(day.toDate()); } }} /> ); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, birthDate: { field: 'birthDate', header: 'Birth Date', // we need to specify the type of the column as "date" type: 'date', }, dateHired: { field: 'dateHired', header: 'Date Hired', // we need to specify the type of the column as "date" type: 'date', }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const columnTypes = { date: { defaultEditable: true, components: { Editor: DateEditor, }, defaultWidth: 200, style: ({ inEdit }: { inEdit: boolean }) => { return inEdit ? { padding: 0 } : {}; }, renderValue: ({ value }: { value: Date }) => { return {dayjs(value).format(DATE_FORMAT)}; }, }, }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-types-date-editor-example" columnTypes={columnTypes} columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', birthDate: new Date(1997, 0, 1), dateHired: new Date(2023, 0, 1), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', birthDate: new Date(1993, 3, 10), dateHired: new Date(2022, 5, 10), currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', birthDate: new Date(1997, 10, 30), dateHired: new Date(2021, 8, 29), currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', birthDate: new Date(1990, 5, 20), dateHired: new Date(2021, 8, 20), currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', birthDate: new Date(1990, 3, 20), dateHired: new Date(2023, 11, 12), currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', birthDate: new Date(2002, 3, 20), dateHired: new Date(2022, 2, 22), currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', birthDate: new Date(1992, 11, 12), dateHired: new Date(2022, 1, 12), currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', birthDate: new Date(1990, 9, 5), dateHired: new Date(2022, 1, 5), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', birthDate: new Date(1990, 9, 15), dateHired: new Date(2022, 10, 1), currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', birthDate: new Date(1990, 4, 18), dateHired: new Date(2023, 3, 18), currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` --- # Custom Editor > Writing a custom editor for a inline editing in Infinite Table for React Canonical page: https://infinite-table.com/docs/learn/editing/custom-editor For writing a custom editor, you can use the [`useInfiniteColumnEditor`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnEditor) hook. For any column (or [column type](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) - which can then get applied to multiple columns), you can specify a custom editor component to be used for editing the column's value, via the [column.components.editor](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.editor) property. ```tsx {10} const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultEditable: false, }, firstName: { field: 'firstName', components: { // this is using a custom editor component editor: CustomEditor, }, }, age: { field: 'age', type: 'number', defaultEditable: false, }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; ``` The editor component should use the [`useInfiniteColumnEditor`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnEditor) hook to have access to cell-related information and to confirm, cancel or reject the edit. ```tsx {3} title="CustomEditor.tsx" import { useInfiniteColumnEditor } from '@infinite-table/infinite-react'; const CustomEditor = () => { const { initialValue, confirmEdit, cancelEdit } = useInfiniteColumnEditor(); const domRef = React.useRef(null); const onKeyDown = useCallback((event: React.KeyboardEvent) => { const { key } = event; if (key === 'Enter' || key === 'Tab') { confirmEdit(domRef.current?.value); } else if (key === 'Escape') { cancelEdit(); } else { event.stopPropagation(); } }, []); return (
); }; ``` Inside any custom editor component, you can use the [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) hook to get access to the cell-related information. **Example: Using a custom editor** In this example, the `salary` column is configured with a custom editor component. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, useInfiniteColumnEditor, } from '@infinite-table/infinite-react'; import { useRef, useCallback } from 'react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const CustomEditor = () => { const { initialValue, confirmEdit, cancelEdit } = useInfiniteColumnEditor(); const domRef = useRef(null); const onKeyDown = useCallback((event: React.KeyboardEvent) => { const { key } = event; if (key === 'Enter' || key === 'Tab') { confirmEdit(domRef.current?.value); } else if (key === 'Escape') { cancelEdit(); } else { event.stopPropagation(); } }, []); return (
); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { components: { // reference to the custom editor component Editor: CustomEditor, }, defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="custom-editor-example" columns={columns} columnDefaultEditable /> ); } ``` ## Using Custom Date Editors A common use-case is integrating date editors, so in the following example we'll use the [MUI X Date Picker](https://mui.com/x/react-date-pickers/date-picker/) component. **Example: Using MUI X Date Picker for editing dates in the DataGrid** This is a basic example integrating with the [MUI X Date Picker](https://mui.com/x/react-date-pickers/date-picker/) - click any cell in the **Birth Date** column to show the date picker. ```ts import { InfiniteTable, DataSource, useInfiniteColumnEditor, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import { StyledEngineProvider } from '@mui/material/styles'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import * as _emotionStyled from '@emotion/styled'; import * as _emotionReact from '@emotion/react'; import * as React from 'react'; type Developer = { birthDate: Date; id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; }; const DATE_FORMAT = 'YYYY-MM-DD'; const DateEditor = () => { const { value, confirmEdit, cancelEdit } = useInfiniteColumnEditor(); const day = dayjs(value); return ( { if (day) { confirmEdit(day.toDate()); } }} /> ); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, birthDate: { field: 'birthDate', header: 'Birth Date', // we need to specify the type of the column as "date" type: 'date', defaultEditable: true, defaultWidth: 200, components: { Editor: DateEditor, }, style: ({ inEdit }) => { return inEdit ? { padding: 0 } : {}; }, renderValue: ({ value }: { value: Date }) => { return {dayjs(value).format(DATE_FORMAT)}; }, }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="date-editor-example" columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', birthDate: new Date(1997, 0, 1), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', birthDate: new Date(1993, 3, 10), currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', birthDate: new Date(1997, 10, 30), currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', birthDate: new Date(1990, 5, 20), currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', birthDate: new Date(1990, 3, 20), currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', birthDate: new Date(2002, 3, 20), currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', birthDate: new Date(1992, 11, 12), currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', birthDate: new Date(1990, 9, 5), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', birthDate: new Date(1990, 9, 15), currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', birthDate: new Date(1990, 4, 18), currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` --- # Excel-like Editing > Configuring the DataGrid to use Excel-like editing via keyboard shortcuts Canonical page: https://infinite-table.com/docs/learn/editing/excel-like-editing InfiniteTable offers support for Excel-like editing. This means users can simply start typing in an editable cell and the editor is displayed and updated immediately (no `Enter` key is required to start typing). This behavior is achieved by using the [Instant Edit keyboard shorcut](https://infinite-table.com/docs/learn/keyboard-navigation/keyboard-shortcuts.md#instant-edit). ```ts {4,12} import { DataSource, InfiniteTable, keyboardShortcuts } from '@infinite-table/infinite-react'; function App() { return primaryKey="id" data={dataSource}> columns={columns} keyboardShortcuts={[ keyboardShortcuts.instantEdit ]} /> } ``` The `instantEdit` keyboard shorcut is configured (by default) to respond to any key (via the special `*` identifier which matches anything) and will start editing the cell as soon as a key is pressed. This behavior is the same as in Excel, Google Sheets, Numbers or other spreadsheet software. **Example** Click on a cell and then start typing to edit the cell. ```ts import { InfiniteTable, DataSource, DataSourceData, keyboardShortcuts, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage', header: 'Language' }, country: { field: 'country', header: 'Country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id', defaultEditable: false }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardShortcuts() { return ( <> primaryKey="id" data={dataSource}> debugId="keyboard-shortcuts-instant-edit-example" columns={columns} columnDefaultEditable keyboardShortcuts={[keyboardShortcuts.instantEdit]} /> ); } ``` To confirm the editing, press the `Enter` key. ## Simulating formulas with `column.valueGetter` You can use the [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) property to simulate formulas in your cells. For example, you might want to have a column that multiplies or divides a value by a constant. ```ts {6} const columns = { salary: { field: 'salary' }, salaryK: { valueGetter: ({data}) => data.salary / 1000 } } ``` **Example** Edit the `salary` column and see the `Salary (thousands)` col update. ```ts import { InfiniteTable, DataSource, DataSourceData, keyboardShortcuts, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage', header: 'Language' }, salary: { field: 'salary', type: 'number', }, salaryK: { valueGetter: ({ data }) => data.salary / 1000, header: 'Salary (thousands)', type: 'number', defaultEditable: false, }, country: { field: 'country', header: 'Country', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id', defaultEditable: false }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardShortcuts() { return ( <> primaryKey="id" data={dataSource}> debugId="keyboard-shortcuts-instant-edit-with-valuegetter-example" columns={columns} columnDefaultEditable keyboardShortcuts={[keyboardShortcuts.instantEdit]} /> ); } ``` --- # Inline Editing Flow > Flow chart of inline editing - understand the flow of operations when performing edits in Infinite Table for React Canonical page: https://infinite-table.com/docs/learn/editing/inline-edit-flow Editing is described in great detail in the [Inline Editing](https://infinite-table.com/docs/learn/editing/inline-editing) page - so make sure you read that first. This page is just a chart that describes the editing flow with the most important steps: - starting the edit - via the API or by user interaction (which triggers the API call) - checking if the cell is editable - async checks are also supported - retrieving the value to edit - stopping the edit - via API or by user interaction - an edit can be cancelled - value discarded - an edit can be rejected - value rejected with error - an edit can be accepted - value accepted and passed to the persit layer - persisting the edit - defaulting to updating data to the data source - a custom persist function can be provided via [`persistEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#persistEdit) - notifying the user of the result of the edit - `onEditCancelled` - `onEditRejected` - `onEditAccepted` - after accepting the edit, either the persist success or error is called - `onEditPersistSuccess` - `onEditPersistError` ```mmd graph TD; startEdit-->editable; editable--"yes"-->editable_yes; editable--"no"-->done; editable_yes--column.getValueToEdit--->editing_active editing_active--"stopEdit({ cancel })"-->cancel editing_active--"stopEdit({ reject })"-->reject editing_active--"stopEdit({ value? })"-->should_accept_edit cancel-->onEditCancelled reject-->onEditRejected onEditCancelled-->done should_accept_edit--yes-->value_accepted should_accept_edit--no-->onEditRejected value_accepted --"column.getValueToPersist(async)"--> persist_value persist_value--no--> default_persist persist_value--yes--> custom_persist default_persist-->onEditPersistSuccess custom_persist-->onEditPersistSuccess custom_persist-->onEditPersistError onEditPersistSuccess-->done onEditPersistError-->done onEditRejected-->done startEdit["API.startEdit({rowIndex, columnId})"] editable{"editable?(async)"} editing_active(["Editing active"]) editable_yes(["Yes"]) cancel("Cancel - value discarded") reject("Reject - value rejected with error") onEditCancelled["onEditCancelled()"] onEditRejected["onEditRejected()"] should_accept_edit{"shouldAcceptEdit?(async)"} value_accepted(["onEditAccepted()"]) persist_value{"props.persistEdit defined?"} default_persist["dataSourceApi.updateData(...)"] custom_persist["props.persistEdit(...) async"] onEditPersistSuccess["onEditPersistSuccess()"] onEditPersistError["onEditPersistError()"] ``` --- # Editing > Learn how to use inline editing to update your data with Infinite Table for React Canonical page: https://infinite-table.com/docs/learn/editing/overview By default, editing is not enabled. To enable editing globally, you can use the [`columnDefaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultEditable) boolean prop on the `InfiniteTable` component. This will enable the editing on all columns. Or you can be more specific and choose to make individual columns editable via the [column.defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) prop. This overrides the global [`columnDefaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultEditable). **Example: Inline Editing in action** All columns (except id) are editable. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, stack: { field: 'stack', contentFocusable: true, header: 'Stack', }, firstName: { field: 'firstName', header: 'Name', }, age: { field: 'age', header: 'Age', }, hobby: { field: 'hobby', header: 'Hobby', }, preferredLanguage: { header: 'Language', field: 'preferredLanguage', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="inline-editing-example" columns={columns} columnDefaultEditable /> ); } ``` Read about how you can configure various editors for your columns. A picture is worth a thousand words - see a chart for the editing flow. The [column.defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) property can be either a `boolean` or a `function`. If it is a function, it will be called when an edit is triggered on the column. The function will be called with a single object that contains the following properties: - `value` - the current value of the cell (the value currently displayed, so after [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) have been applied) - `rawValue` - the current value of the cell, but before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `data` - the data object (of type `DATA_TYPE`) for the current row - `rowInfo` - the row info object that underlies the row - `column` - the current column on which editing is invoked - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSourceApi` - - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) The function can return a boolean value or a Promise that resolves to a boolean value - this means you can asynchronously decide whether the cell is editable or not. Making [column.defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) a function gives you the ability to granularly control which cells are editable or not (even within the same column, based on the cell value or other values you have access to). In addition to the flags mentioned above, you can use the [`editable`](https://infinite-table.com/docs/reference/infinite-table-props.md#editable) prop on the `InfiniteTable` component. This overrides all other properties and when it is defined, is the only source of truth for whether something is editable or not. The [`editable`](https://infinite-table.com/docs/reference/infinite-table-props.md#editable) prop allows you to centralize editing logic in one place. It has the same signature as the [column.defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) function. ## Start Editing Editing can be started either by user interaction or programmatically via the [API](https://infinite-table.com/docs/reference/api/index.md). The user can start editing by double-clicking on a cell or by pressing the `Enter` key while the cell is active (see [Keyboard Navigation for Cells](docs/learn/keyboard-navigation/navigating-cells)). To start editing programmatically, use the [{`startEdit({ columnId, rowIndex })`}](https://infinite-table.com/docs/reference/api/index.md#startEdit) method. **Example: Starting an Edit via the API** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, InfiniteTableApi, } from '@infinite-table/infinite-react'; import { useCallback, useRef, useState } from 'react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { const [activeRowIndex, setActiveRowIndex] = useState(2); const apiRef = useRef | null>(null); const onReady = useCallback( ({ api }: { api: InfiniteTableApi }) => { apiRef.current = api; }, [], ); return ( <> primaryKey="id" data={dataSource}> debugId="api-inline-editing-custom-edit-value-example" onReady={onReady} columns={columns} columnDefaultEditable activeRowIndex={activeRowIndex} onActiveRowIndexChange={setActiveRowIndex} /> ); } ``` Either way, be it user interaction or API call, those actions will trigger checks to see if the cell is editable - taking into account the [`columnDefaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultEditable), [column.defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) or [`editable`](https://infinite-table.com/docs/reference/infinite-table-props.md#editable) props, as described in the paragraphs above. Only if the result is `true` will the cell editor be displayed. ## Customize Edit Value When Editing Starts When editing starts, the column editor is displayed with the value that was in the cell. This (initial) edit value can be customized via the [column.getValueToEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.getValueToEdit) prop. This allows you to start editing with a different value than the one that is displayed in the cell - and even with a value fetched asynchronously. ```tsx const columns = { salary: { field: 'salary', // this can return a Promise getValueToEdit: ({ value, data, rowInfo, column }) => { // suppose the value is a string like '$1000' // but we want to start editing with the number 1000 return value.replace('$', ''); }, }, }; ``` **Example: Inline Editing with custom getter for edit value** Try editing the salary column - it has a custom getter for the edit value, which removes the curency string. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="inline-editing-custom-edit-value-example" columns={columns} columnDefaultEditable /> ); } ``` ## Finishing an Edit An edit is generally finished by user interaction - either the user confirms the edit by pressing the `Enter` key or cancels it by pressing the `Escape` key. As soon as the edit is confirmed by the user, the `InfiniteTable` needs to decide whether the edit should be accepted or not. In order to decide (either synchronously or asynchronously) whether an edit should be accepted or not, you can use the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) prop or the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) alternative. When neither the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) nor the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) are defined, all edits are accepted by default. Once an edit is accepted, the [`onEditAccepted`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditAccepted) callback prop is called, if defined. When an edit is rejected, the [`onEditRejected`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditRejected) callback prop is called instead. The accept/reject status of an edit is decided by using the `shouldAcceptEdit` props described above. However an edit can also be cancelled by the user pressing the `Escape` key in the cell editor - to be notified of this, use the [`onEditCancelled`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditCancelled) callback prop. **Example: Using shouldAcceptEdit to decide whether a value is acceptable or not** In this example, the `salary` column is configured with a [shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) function property that rejects non-numeric values. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="inline-editing-custom-edit-value-example" columns={columns} columnDefaultEditable /> ); } ``` ## Persisting an Edit By default, accepted edits are persisted to the `DataSource` via the [DataSourceAPI.updateData](https://infinite-table.com/docs/reference/datasource-api/index.md#updateData) method. To change how you persist values (which might include persisting to remote locations), use the [`persistEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#persistEdit) function prop on the `InfiniteTable` component. The [`persistEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#persistEdit) function prop can return a `Promise` for async persistence. To signal that the persisting failed, reject the promise or resolve it with an `Error` object. After persisting the edit, if all went well, the [`onEditPersistSuccess`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditPersistSuccess) callback prop is called. If the persisting failed (was rejected), the [`onEditPersistError`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditPersistError) callback prop is called instead. --- # Changing the Data Source Canonical page: https://infinite-table.com/docs/learn/examples/change-datasource This example show how you can change the data source and the columns of the DataGrid. Find out more about how to work with data - both client-side and server-side. See our page on using and configuring columns. It shows you how to use and customize columns to your needs. **Example** In this demo you can toggle between 2 data sources and 2 sets of columns. ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import { useMemo, useState } from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetPrefix: string; streetNo: string; department: string; team: string; salary: number; currency: number; age: number; email: string; }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; export const employeeColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, streetName: { field: 'streetName' }, streetNo: { field: 'streetNo', type: 'number' }, currency: { field: 'currency', type: 'number', }, email: { field: 'email' }, }; const developerColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country', // specifying a style here for the column // note: it will also be "picked up" by the group column // if you're grouping by the 'country' field style: { color: 'tomato', }, }, canDesign: { field: 'canDesign' }, salary: { field: 'salary', type: 'number', }, }; const getDataSourceFor = (name: 'employee' | 'developer', size: string) => { if (size === '0') { return () => Promise.resolve([]); } return () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/${name}s` + size) .then((r) => r.json()) .then((data: Employee[]) => data); }; }; export default function App() { const [dataSourceSize, setDataSourceSize] = useState('10k'); const [type, setType] = useState<'employee' | 'developer'>('employee'); const dataSource = useMemo(() => { return getDataSourceFor(type, dataSourceSize); }, [type, dataSourceSize]); return (

Please select datasource:

Please select the size of the datasource:

data={dataSource} primaryKey="id"> debugId="change-datasource-example" columns={type === 'developer' ? developerColumns : employeeColumns} columnDefaultWidth={150} />
); } ``` Infinite Table guarantees you that the user will NEVER see white space when scrolling horizontally or vertically. --- # Dynamic Pivoting Example Canonical page: https://infinite-table.com/docs/learn/examples/dynamic-pivoting-example This example showcases client-side grouping, pivoting and aggregation. These properties are changed dynamically at run-time via the UI. It also showcases different way of customizing columns based on dynamic conditions: - uses custom `number` and `currency` column types, to format values - has a custom border for rows that have `canDesign=yes` - the custom column type `number` has a background color based on the color input **Example** ```tsx files=["dynamic-advanced-pivoting-example.page.tsx","Settings.tsx","types.ts"] ``` ## Server-side Dynamic Pivoting Example This example is very similar with the above one, but pivoting, grouping and aggregation is done on the server-side. **Example** ```tsx files=["dynamic-pivoting-serverside-example.page.tsx","Settings.tsx","types.ts"] ``` --- # Live Updates Example Canonical page: https://infinite-table.com/docs/learn/examples/live-updates-example This example shows how you can update the grid data in real-time. Find out more about how to update data in real-time **Example** The DataSource has 10k items - use the **Start/Stop** button to see updates in real-time. In this example, we're updating 5 rows (in the visible viewport) every 30ms. The update rate could be much higher, but we're keeping it at current levels to make it easier to see the changes. ```tsx import * as React from 'react'; import '@infinite-table/infinite-react/index.css'; import { DataSourceApi, InfiniteTable, InfiniteTableApi, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; reposCount: number; }; const dataSource = () => { return fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/developers10k-sql`) .then((r) => r.json()) .then((data: Developer[]) => { return data; }); }; export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } const CURRENCIES = ['USD', 'CAD', 'EUR']; const stacks = ['frontend', 'backend', 'fullstack']; const updateRow = (api: DataSourceApi, data: Developer) => { const getDelta = (num: number): number => Math.ceil(0.2 * num); const initialData = data; if (!initialData) { return; } const salaryDelta = getDelta(initialData?.salary); const reposCountDelta = getDelta(initialData?.reposCount); const newSalary = initialData.salary + getRandomInt(-salaryDelta, salaryDelta); const newReposCount = initialData.reposCount + getRandomInt(-reposCountDelta, reposCountDelta); const newData: Partial = { id: initialData.id, salary: newSalary, reposCount: newReposCount, currency: CURRENCIES[getRandomInt(0, CURRENCIES.length - 1)] || CURRENCIES[0], stack: stacks[getRandomInt(0, stacks.length - 1)] || stacks[0], age: getRandomInt(0, 100), }; api.updateData(newData); }; const ROWS_TO_UPDATE_PER_FRAME = 5; const UPDATE_INTERVAL_MS = 30; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', style: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return {}; } return { color: 'black', background: value > 80 ? 'tomato' : value > 60 ? 'orange' : value > 40 ? 'yellow' : value > 20 ? 'lightgreen' : 'green', }; }, }, salary: { field: 'salary', type: 'number', }, reposCount: { field: 'reposCount', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default function App() { const [running, setRunning] = React.useState(false); const [apis, onReady] = React.useState<{ api: InfiniteTableApi; dataSourceApi: DataSourceApi; }>(); const intervalIdRef = React.useRef(null); React.useEffect(() => { const { current: intervalId } = intervalIdRef; if (!running || !apis) { return clearInterval(intervalId); } intervalIdRef.current = setInterval(() => { const { dataSourceApi, api } = apis!; const { renderStartIndex, renderEndIndex } = api.getVerticalRenderRange(); const dataArray = dataSourceApi.getRowInfoArray(); const data = dataArray .slice(renderStartIndex, renderEndIndex) .map((x) => x.data as Developer); for (let i = 0; i < ROWS_TO_UPDATE_PER_FRAME; i++) { const row = data[getRandomInt(0, data.length - 1)]; if (row) { updateRow(dataSourceApi, row); } } return () => { clearInterval(intervalIdRef.current); intervalIdRef.current = null; }; }, UPDATE_INTERVAL_MS); }, [running, apis]); return ( data={dataSource} primaryKey="id"> debugId="realtime-updates-example" domProps={domProps} onReady={onReady} columnDefaultWidth={130} columnMinWidth={50} columns={columns} /> ); } ``` --- # Performance with Many Rows and Columns Canonical page: https://infinite-table.com/docs/learn/examples/performance-many-rows-and-columns This example showcases a DataGrid with **10.000 rows** and **12 columns**. Find out more about how to work with data - both client-side and server-side. See our page on using and configuring columns. It shows you how to use and customize columns to your needs. **Example** DataGrid with 10k rows and 12 columns. Adding more columns will not affect performance, as the DataGrid uses virtualization for both rows and **columns**. ```tsx import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import { useMemo, useState } from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetPrefix: string; streetNo: string; department: string; team: string; salary: number; currency: number; age: number; email: string; }; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, streetName: { field: 'streetName' }, streetNo: { field: 'streetNo' }, currency: { field: 'currency' }, email: { field: 'email' }, }; const getDataSourceFor = (size: string) => { if (size === '0') { return () => Promise.resolve([]); } return () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees' + size) .then((r) => r.json()) .then((data: Employee[]) => data); }; }; export default function App() { const [dataSourceSize, setDataSourceSize] = useState('10k'); const dataSource = useMemo(() => { return getDataSourceFor(dataSourceSize); }, [dataSourceSize]); return (

Please select the size of the datasource:

data={dataSource} primaryKey="id"> debugId="many-rows-and-columns-example" columns={columns} columnDefaultWidth={150} />
); } ``` Infinite Table guarantees you that the user will NEVER see white space when scrolling horizontally or vertically. --- # Sparklines Example Canonical page: https://infinite-table.com/docs/learn/examples/using-sparklines This example shows how to use integrate a sparkline component in a DataGrid column. For this demo, we're using the [`react-sparklines`](https://www.npmjs.com/package/react-sparklines) library. The most important part is the [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) property, which allows you to render a custom React component for the cell value. ```tsx {11-26} title="Using column.renderValue to render a sparkline" const columns = { // ... other columns id: { field: 'id', defaultWidth: 100, }, bugFixes: { field: 'bugFixes', header: 'Bug Fixes', defaultWidth: 300, renderValue: ({ value, data }) => { const color = data?.department === 'IT' || data?.department === 'Management' ? 'tomato' : '#253e56'; return ( ); }, }, } ``` **Example: Using a sparkline component** This demo renders a sparkline and changes the color of the sparkline based on the `department` field in the row (red for IT or Management, blue for everything else). ```tsx import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import { Sparklines, SparklinesLine } from 'react-sparklines'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; bugFixes: number[]; streetName: string; streetPrefix: string; streetNo: string; department: string; team: string; salary: number; currency: number; age: number; email: string; }; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, bugFixes: { field: 'bugFixes', header: 'Bug Fixes', defaultWidth: 300, renderValue: ({ value, data }) => { const color = data?.department === 'IT' || data?.department === 'Management' ? 'tomato' : '#253e56'; return ( ); }, }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees10k') .then((r) => r.json()) .then((data: Employee[]) => { return data.map((employee) => { return { ...employee, bugFixes: [...Array(10)].map(() => Math.round(Math.random() * 100)), }; }); }); }; export default function App() { return ( data={dataSource} primaryKey="id"> debugId="using-sparklines-example" columns={columns} columnDefaultWidth={150} /> ); } ``` --- # Filtering > Learn how to configure client-side and server-side filtering in Infinite Table for React Canonical page: https://infinite-table.com/docs/learn/filtering/ Filtering allows you to limit the rows available in the table. Both client-side and server-side filtering are available in Infinite Table - but the way the are configured is pretty similar, so this page documents the common parts, while pointing to the respective pages for the differences. ## Configuring Filters for Columns The most common way to use filtering in Infinite Table is by configuring filters for columns (this works both for client-side and server-side filtering). You specify an uncontrolled [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue) on the `` component (or the controlled version, [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue)) and the specified value will be used as the initial filter. Based on the [column type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type), the correct filter editor is displayed in the column header, along with the correct operator icon. In the UI, you can change the operator being used for the filter. ```tsx title="Specifying an initial filter value for the DataSource" data={...} defaultFilterValue={[ { field: 'age', filter: { operator: 'gt', value: 30, type: 'number' } } ]} > columns={...} /> ``` If you don't need to specify some initial filters, but want the column filter bar to be visible, you need to specify `defaultFilterValue = []` (or the controlled `filterValue = []`). Specifying any of those props will make the column filter bar visible. Whenever filters change, [`onFilterChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onFilterChange) will be called with the new filter value - note however, it might not be called immediately, due to the [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) prop. The above snippet will show a `number` filter for the `age` column. There are two filter types available at this stage in Infinite Table: - `string` - with the following operators available: `contains`, `eq`, `startsWith` and `endsWith` - `number` - with the following operators available: `eq`,`neq`, `gt`, `gte`, `lt` and `lte` ## Defining Filterable Columns By default, all columns are filterable. If you want to make columns by default not filterable, use the [`columnDefaultFilterable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultFilterable) prop and set it to `false`. You can specifically configure each column by using the [defaultFilterable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFilterable) property - this overrides the global [`columnDefaultFilterable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultFilterable) prop. ## Defining a Filter Type for a Column Besides being filterable, a column can decide what type of filter it will display. Use the [`columns.type`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) property to specify the type of filter the column will use. Using the `type` property also configures the data type of the column, which in turn determines the sort type. If the type of filter you want to show does not match the column [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type), you can specify the filter with the [column.filterType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.filterType) property. Only use this when the type of the data differs from the type of the filter (eg: you have a numeric column, with a custom filter type). ## Understanding Filter Types A filter type is a concept that defines how a certain type of data is to be filtered. A filter type will have - a `key` - the key used to define the filter in the [`filterTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes) object - a `label`, - an array of values considered to be empty values - when any of these values is used in the filter, the filter will not be applied. - an array of `operators` - a default operator. Let's imagine you have a `DataSource` with developers, each with a `salary` column, and for that column you want to allow `>`, `>=`, `<` and `<=` comparisons (operators). For this, you would define the following filter type: ```tsx const filterTypes = { income: { label: 'Income', emptyValues: ['', null, undefined], defaultOperator: 'gt', operators: [ { name: 'gt', label: 'Greater than', fn: ({ currentValue, filterValue, emptyValues }) => { if (emptyValues.has(currentValue)) { return true; } return currentValue > filterValue; }, }, { name: 'gte', //... }, { name: 'lt', //... }, { name: 'lte', //... }, ], }, }; ``` Each operator for a certain filter type needs to at least have a `name` and `fn` defined. The `fn` property is a function that will be called when client-side filtering is enabled, with an object that has the following properties: - `currentValue` - the cell value of the current row for the column being filtered - `filterValue` - the value of the filter editor - `emptyValues` - the array of values considered to be empty values for the filter type - `data` - the current row data object - `typeof DATA_TYPE` - `index` - the index of the current row in the table - `number` - `dataArray` - the array of all rows originally in the table - `typeof DATA_TYPE[]` - `field?` - the field the current column is bound to (can be undefined if the column is not bound to a field) **Example: Client-side filtering in action with custom filter type** The `salary` column has a custom filter type, with the following operators: `gt`, `gte`, `lt` and `lte`. ```ts import * as React from 'react'; import { DataSourceData, DataSource, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', filterType: 'salary', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency', defaultFilterable: false }, }; function getIcon(icon: string) { return () => (
{icon}
); } const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" filterTypes={{ salary: { defaultOperator: 'gt', emptyValues: ['', null, undefined], operators: [ { name: 'gt', label: 'Greater Than', components: { Icon: getIcon('>'), }, fn: ({ currentValue, filterValue }) => { return currentValue > filterValue; }, }, { name: 'gte', components: { Icon: getIcon('>='), }, label: 'Greater Than or Equal', fn: ({ currentValue, filterValue }) => { return currentValue >= filterValue; }, }, { name: 'lt', components: { Icon: getIcon('<'), }, label: 'Less Than', fn: ({ currentValue, filterValue }) => { return currentValue < filterValue; }, }, { name: 'lte', components: { Icon: getIcon('<='), }, label: 'Less Than or Equal', fn: ({ currentValue, filterValue }) => { return currentValue <= filterValue; }, }, ], }, }} > debugId="filter-custom-filter-type-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ## Specifying the filter mode As already mentioned, filtering can happen either client-side or server-side. If the DataSource [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) property is a function (and not an array or a `Promise`), then the filtering will happen server-side by default. However, you can explicitly specify where the filtering should happen by setting the [`filterMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode) property on the `` component - possible values are - `filterMode="local"` - filtering will happen client-side - `filterMode="remote"` - filtering will happen remotely and the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) will be passed as a property to the parameter object sent to the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function. Explicitly specify [`filterMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode) as either `"local"` or `"remote"` if you want to change the default behavior. ## Filtering Columns Not Bound to a Field If a column is not bound to a `field`, it can still be used for filtering, even client-side filtering, if it is configured with a [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter). If you don't need a default filter value, the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) that's set when the user interacts with the column filter will use the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) to filter values. If however, you need initial filtering by that column, the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) needs to specify a `valueGetter` itself. ```tsx defaultFilterValue={[ { id: 'salary', valueGetter: ({ data }) => data.salary, filter: { operator: 'gt', value: '', type: 'number', } }, ]} ``` **Example: Filtering a column not bound to a field** The `salary` column is not bound to a `field` - however, it can still be used for filtering, as it's configured with a `valueGetter`. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 70, defaultFilterable: false, }, salary: { // we're intentionally using not binding this column to a `field` type: 'number', header: 'Salary', valueGetter: ({ data }) => data.salary, }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[ // if you want the salary column to be filtered by default // you need to pass a valueGetter to the filter value // if you don't need a default filter, when you start filtering by // the column, the filter value will use the valueGetter of the column { id: 'salary', valueGetter: ({ data }) => data.salary, filter: { operator: 'gt', value: '', type: 'number', }, }, ]} filterDelay={0} filterMode="local" > debugId="filter-column-with-id-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ## Customizing the Filter Icon for Columns Columns can customize the filter icon by using the [`columns.renderFilterIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderFilterIcon) property. **Example: Custom filter icons for salary and name columns** The `salary` column will show a bolded label when filtered. The `firstName` column will show a custom filter icon when filtered. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { field: 'salary', type: 'number', header: ({ filtered }) => { return filtered ? Salary : 'Salary'; }, renderFilterIcon: () => { return null; }, }, firstName: { field: 'firstName', renderFilterIcon: ({ filtered }) => { return filtered ? '🔥' : ''; }, }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" > debugId="column-filter-icon-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` Learn how to use filtering in the browser. Figure out how to use filtering with server-side integration. --- # Extending existing filters > Learn how to extend existing filters and filter types for your Infinite Table React DataGrid Canonical page: https://infinite-table.com/docs/learn/filtering/extending-existing-filters By default `InfiniteTable` has the following default filter types: - `string` - `number` and each of them has a collection of operators that are supported - see [`filterTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes) for the respective list of supported operators. You may find those operators limiting - but it's easy to extend them and add new operators or even new filter types. ## Adding new operators to existing filter types You can import `defaultFilterTypes` from the root of the package. ```ts title="Adding a new operator to the string filter type" import { defaultFilterTypes } from '@infinite-table/infinite-react'; // add new operators for the `string` filter type defaultFilterTypes.string.operators.push({ name: 'notContains', component: { Icon: ReactComponentForIcon } label: 'Not Contains', fn: ({currentValue, filterValue }) => { return typeof currentValue === 'string' && typeof filterValue == 'string' && !currentValue.toLowerCase().includes(filterValue.toLowerCase()) } }) ``` When you import the named `defaultFilterTypes` value and extend it, that will affect all `InfiniteTable` components in your application. If you don't want that, you need to use the `filterTypes` prop of the `` component. Either build an entirely new object for `filterTypes`, or start by cloning `defaultFilterTypes` and extend it. **Example: Enhanced string filter type - new 'Not includes' operator** The `string` columns have a new `Not includes` operator. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, defaultFilterTypes, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; defaultFilterTypes.string.operators.push({ name: 'Not contains', label: 'Not Contains', fn: ({ currentValue, filterValue, emptyValues }) => { if ( emptyValues.includes(currentValue) || emptyValues.includes(filterValue) ) { return true; } return ( typeof currentValue === 'string' && typeof filterValue == 'string' && !currentValue.toLowerCase().includes(filterValue.toLowerCase()) ); }, }); const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, salary: { field: 'salary', type: 'number', }, currency: { field: 'currency', defaultFilterable: false }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" > debugId="customised-default-filter-types-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ## Adding new filter types If the existing filter types are not enough, it's easy to add new ones. As already mentioned, you can either update the value of `defaultFilterTypes` or use the `filterTypes` prop of the `` component. Updating the value `defaultFilterTypes` will affect all your `InfiniteTable` DataGrid components. ```ts title="Adding a new filter type by updating defaultFilterTypes" import { defaultFilterTypes } from '@infinite-table/infinite-react'; defaultFilterTypes.bool = { defaultOperator: 'eq', emptyValues: [null], operators: [ { name: 'eq', label: 'Equals', fn: ({ currentValue, filterValue }) => currentValue === filterValue, }, ], }; ``` ```ts title="Adding a new filter type by using the filterTypes prop" import { DataSource } from '@infinite-table/infinite-react'; currentValue === filterValue, }, ], }, }} />; ``` When passing `filterTypes` to the `` component, the object will be merged with the `defaultFilterTypes`. As a result, the existing `string` and `number` filterTypes will be preserved, unless explicitly overridden. **Example: Writing a `bool` filter type with a custom filter editor** The `canDesign` column is using a custom `bool` filter type with a custom filter editor. ```ts import * as React from 'react'; import { InfiniteTable, InfiniteTablePropColumns, DataSource, components, useInfiniteColumnFilterEditor, } from '@infinite-table/infinite-react'; const { CheckBox } = components; type Developer = { id: number; firstName: string; canDesign: boolean; stack: string; hobby: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', canDesign: true, stack: 'frontend', hobby: 'gaming', }, { id: 2, firstName: 'Jane', canDesign: false, stack: 'backend', hobby: 'reading', }, { id: 3, firstName: 'Jack', canDesign: true, stack: 'frontend', hobby: 'gaming', }, { id: 4, firstName: 'Jill', canDesign: false, stack: 'backend', hobby: 'reading', }, { id: 5, firstName: 'Seb', canDesign: false, stack: 'backend', hobby: 'reading', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, canDesign: { field: 'canDesign', filterType: 'bool', renderValue: ({ value }) => (value ? 'Yes' : 'No'), }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, }; const domProps = { style: { height: '100%', }, }; function BoolFilterEditor() { const { value, setValue, className } = useInfiniteColumnFilterEditor(); return (
{ if (value === true) { // after the value was true, make it go to indeterminate state newValue = null; } if (value === null) { // from indeterminate, goto false newValue = false; } setValue(newValue); }} />
); } export default () => { return ( <> data={dataSource} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterTypes={{ bool: { defaultOperator: 'eq', emptyValues: [null], components: { FilterEditor: BoolFilterEditor, FilterOperatorSwitch: () => null, }, operators: [ { name: 'eq', label: 'Equals', fn: ({ currentValue, filterValue }) => currentValue === filterValue, }, ], }, }} > debugId="checkbox-filter-editor-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} />
); }; ``` --- # Client-side Filtering > Learn how to configure client-side filtering for your Infinite Table React DataGrid Canonical page: https://infinite-table.com/docs/learn/filtering/filtering-client-side The most common way to use filtering in Infinite Table is by configuring filters for columns (this works both for client-side and server-side filtering). If the DataSource [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) property is a function (and not an array or a `Promise`), then the filtering will happen server-side by default. To force client-side filtering, you can explicitly set the [filterMode="local"](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode) property on the `` component. The possible values for this prop are: - `filterMode="local"` - filtering will happen client-side - `filterMode="remote"` - filtering will happen remotely and the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) will be passed as a property to the parameter object sent to the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function. ## Showing the Column Filters In order to show the column filter editors in the column headers, you need to specify either the uncontrolled [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue) property or the controlled [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) version. **Example: Client-side filtering in action** This example shows remote data with local filtering - it sets `filterMode="local"` on the `` component. In addition, the `filterDelay` property is set to `0` for instant feedback. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, defaultFilterTypes, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; defaultFilterTypes.string.operators.push({ name: 'Not includes', label: 'Not Includes', fn: ({ currentValue, filterValue, emptyValues }) => { if ( emptyValues.includes(currentValue) || emptyValues.includes(filterValue) ) { return true; } return ( typeof currentValue === 'string' && typeof filterValue == 'string' && !currentValue.toLowerCase().includes(filterValue.toLowerCase()) ); }, }); const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 70, defaultFilterable: false, }, salary: { field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" > debugId="basic-local-filter-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` If you still want filtering to be enabled with the default functionality of using the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) (or uncontrolled [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue)), but want to hide the column filter editors, you can set the [showColumnFilters](https://infinite-table.com/docs/reference/datasource-props/index.md#showColumnFilters) property to `false`. ## Using Filter Types As already documented in the [Understanding Filter Types](./#understanding-filter-types) section, you can specify the types of the filters the `` will support, by using the [`filterTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes) property. The default filter types are `string` and `number` - read the next section to see how you can add new operators to those filter types. A filter type is basically a collection of operators available for a type of data. Each operator needs a name and a function that will be used to filter the data, when that operator is applied. ```tsx {5,14} title="Using_filter_types_for_filterValue" filterValue={[ { field: 'firstName', filter: { type: 'string', operator: 'includes', value: 'John' } }, { field: 'age', filter: { type: 'number', operator: 'gt', value: 30 } } ]} ``` The above filter value specifies that there are 2 filters applied: - the `firstName` column applies a filter that will only match rows with `firstName` containining the string `John` - the `age` column has an additional filter, that will only match rows with `age` greater than `30` If [`filterMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode) is set to `local`, then the filtering will happen client-side, using the filtering functions specified by `includes` operator in the `string` filter type and the `gt` operator in the `number` filter type. Here's a snippet of code from the `string` filter type showing the `includes` operator: ```tsx operators: [ { name: 'includes', components: { Icon: /* a React Component */ }, label: 'Includes', fn: ({ currentValue, filterValue }) => { return ( typeof currentValue === 'string' && typeof filterValue == 'string' && currentValue.toLowerCase().includes(filterValue.toLowerCase()) ); }, }, //... ] ``` Let's now look at another example, of implementing a custom `salary` filter type. For this, we override the `filterTypes` property of the `` component: ```tsx const filterTypes = { salary: { defaultOperator: 'gt', emptyValues: ['', null, undefined], operators: [ /*...*/ ] } } filterTypes={filterTypes} /> ``` When you specify new [`filterTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes), the default filter types of `string` and `number` are still available - unless the new object contains those keys and overrides them explicitly. **Example: Client-side filtering in action with custom filter type** The `salary` column has a custom filter type, with the following operators: `gt`, `gte`, `lt` and `lte`. ```ts import * as React from 'react'; import { DataSourceData, DataSource, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', filterType: 'salary', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency', defaultFilterable: false }, }; function getIcon(icon: string) { return () => (
{icon}
); } const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" filterTypes={{ salary: { defaultOperator: 'gt', emptyValues: ['', null, undefined], operators: [ { name: 'gt', label: 'Greater Than', components: { Icon: getIcon('>'), }, fn: ({ currentValue, filterValue }) => { return currentValue > filterValue; }, }, { name: 'gte', components: { Icon: getIcon('>='), }, label: 'Greater Than or Equal', fn: ({ currentValue, filterValue }) => { return currentValue >= filterValue; }, }, { name: 'lt', components: { Icon: getIcon('<'), }, label: 'Less Than', fn: ({ currentValue, filterValue }) => { return currentValue < filterValue; }, }, { name: 'lte', components: { Icon: getIcon('<='), }, label: 'Less Than or Equal', fn: ({ currentValue, filterValue }) => { return currentValue <= filterValue; }, }, ], }, }} > debugId="filter-custom-filter-type-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} />
); }; ``` ### Customizing Default Filter Types By default, the `string` and `number` filter types are available. You can import the default filter types like this: ```ts import { defaultFilterTypes } from '@infinite-table/infinite-react'; ``` If you want to make all your instances of `InfiniteTable` have new operators for those filter types, you can simply mutate the exported `defaultFilterTypes` object. **Example: Enhanced string filter type - new 'Not includes' operator** The `string` columns have a new `Not includes` operator. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, defaultFilterTypes, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; defaultFilterTypes.string.operators.push({ name: 'Not contains', label: 'Not Contains', fn: ({ currentValue, filterValue, emptyValues }) => { if ( emptyValues.includes(currentValue) || emptyValues.includes(filterValue) ) { return true; } return ( typeof currentValue === 'string' && typeof filterValue == 'string' && !currentValue.toLowerCase().includes(filterValue.toLowerCase()) ); }, }); const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, salary: { field: 'salary', type: 'number', }, currency: { field: 'currency', defaultFilterable: false }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" > debugId="customised-default-filter-types-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} />
); }; ``` When you specify new [`filterTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes), the default filter types of `string` and `number` are still available - unless the new object contains those keys and override them explicitly. ## Using a Filter Delay In order to save some resources, filtering is batched by default. This is controlled by the [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) prop, which, if not specified, defaults to `200` milliseconds. This means, any changes to the column filters, that happen inside a 200ms window (or the current value of [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay)), will be debounced and only the last value will be used to trigger a filter. If you want to prevent debouncing/batching filter values, you can set [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) to `0`. API calls to [`setColumnFilter`](https://infinite-table.com/docs/reference/api/index.md#setColumnFilter) or [`clearColumnFilter`](https://infinite-table.com/docs/reference/api/index.md#clearColumnFilter) are not batched. ## Using a Filter Function Instead of the Column Filters For client-side rendering, it's possible that instead of showing a column filter bar, you use a custom [`filterFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterFunction) to filter the data. In this case, the filtering will happen client-side ... of course 🤦‍♂️. **Example: Custom filterFunction example** Loads data from remote location but will only show rows that have `id > 100`. ```ts import * as React from 'react'; import { DataSourceData, DataSource, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', filterType: 'salary', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency', defaultFilterable: false }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" filterFunction={({ data }) => { return data.id > 100; }} > debugId="filter-function-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} />
); }; ``` --- # Server-side Filtering > Learn how to integrate server-side filtering with your InfiniteTable React DataGrid Canonical page: https://infinite-table.com/docs/learn/filtering/filtering-server-side If you're using a remote [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) prop (a function that returns a `Promise`) on the `` component, the filtering will happen server-side by default. You can explicitly configure server-side filtering by using [filterMode="remote"](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode). When remote filtering is enabled, the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function prop will be called with an object argument that includes the `filterValue` property, so the filters can be sent to the server for performing the correct filtering operations. Obviously the filtering can be combined with sorting, grouping, etc. It's up to the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function prop to send the correct parameters to the server for remote operations. The returned JSON can include both - a `totalCount` property (`number`) and - a `totalCountUnfiltered` property (also `number`) - to inform the `` of the size of the data, both with and without the applied filters. **Example: Server-side filtering example** All the filtering in this example happens server-side. This example also does server-side (multiple) sorting. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, DataSourcePropSortInfo, DataSourcePropFilterValue, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = ({ filterValue, sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; const defaultSortInfo: DataSourcePropSortInfo = [ { field: 'stack', dir: 1, }, { field: 'salary', dir: 1, }, ]; const defaultFilterValue: DataSourcePropFilterValue = []; const shouldReloadData = { sortInfo: true, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={defaultFilterValue} defaultSortInfo={defaultSortInfo} shouldReloadData={shouldReloadData} > debugId="server-side-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` When the filter value for a column matches the empty value - as specified in the [filterTypes.operator.emptyValues](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes) - that value is not sent to the server as part of the `filterValue` array. When doing server-side filtering, it's your responsability as a developer to make sure you're sending the correct filtering parameters to the server, in a way the server understands it. This means that the filter values, the filter type and the names of the operators are known to the server and there is a clear convention of what is supported or not. ## Batch filtering In order to reduce the number of requests sent to the server, filtering will be batched by default. Batching is controlled by the [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) prop, which, if not specified, defaults to `200` milliseconds. This means, any changes to the column filters, that happen inside a 200ms window (or the current value of [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay)), will be debounced and only the last value will be sent to the server. If you want to prevent debouncing/batching filter values, you can set [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) to `0`. --- # Providing a Custom Filter Editor > Writing a custom filter editor for a column in Infinite Table is straightforward. Canonical page: https://infinite-table.com/docs/learn/filtering/providing-a-custom-filter-editor Almost certainly, our current `string` and `number` filters are not enough for you. You will definitely need to write your custom filter editor. Fortunately, doing this is straightforward - it involves using the [`useInfiniteColumnFilterEditor`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnFilterEditor) hook. The next snippet shows our implementation of the `number` filter editor: ```tsx export function NumberFilterEditor() { const { ariaLabel, value, setValue, className, disabled } = useInfiniteColumnFilterEditor(); return ( { let value = isNaN(event.target.valueAsNumber) ? event.target.value : event.target.valueAsNumber; setValue(value as any as T); }} className={className} /> ); } ``` This `NumberFilterEditor` is configured in the `components.FilterEditor` property for the `number` filter type. If you want to import the `NumberFilterEditor`, you can do so with the following code: ```tsx import { components } from '@infinite-table/infinite-react'; const { NumberFilterEditor, StringFilterEditor } = components; ``` As an exercise, let's write a custom filter editor that shows a checkbox and uses that to filter the values. First step is to define the `bool` filter type: ```tsx {6} title="Defining the bool filter type with one emptyValue" filterTypes={{ bool: { label: 'Boolean', defaultOperator: 'eq', // when the filter checkbox is indeterminate state, that's mapped to `null` emptyValues: [null], operators: [ // operators will come here ], } }} ``` Note in the code above, we have `emptyValues: [null]` - so when the filter checkbox is in indeterminate state, it should show all the rows. Now it's time to define the operators - more exactly, just one operator, `eq`: ```tsx {7} title="Defining the eq operator" filterTypes={{ bool: { defaultOperator: 'eq', emptyValues: [null], operators: [ { name: 'eq', label: 'Equals', fn: ({ currentValue, filterValue }) => currentValue === filterValue, }, ], }, }} ``` The last part of the `bool` filter type will be to specify the `FilterEditor` component - this can be either specified as part of the filter type or as part of the operator definition (each operator can override the `components.FilterEditor`). ```tsx {6} title="Specifying the FilterEditor component" filterTypes={{ bool: { defaultOperator: 'eq', emptyValues: [null], components: { FilterEditor: BoolFilterEditor, FilterOperatorSwitch: () => null, }, operators: [ { name: 'eq', label: 'Equals', fn: ({ currentValue, filterValue }) => currentValue === filterValue, }, ], }, }} ``` Now it's time to write the actual `BoolFilterEditor` that the `bool` filter type is using: ```tsx {9} title="BoolFilterEditor" import { components, useInfiniteColumnFilterEditor, } from '@infinite-table/infinite-react'; const { CheckBox } = components; function BoolFilterEditor() { const { value, setValue, className } = useInfiniteColumnFilterEditor(); return (
{ if (value === true) { // after the value was true, make it go to indeterminate state newValue = null; } if (value === null) { // from indeterminate, goto false newValue = false; } setValue(newValue); }} />
); } ``` In the snippet above, note how we're using the [`useInfiniteColumnFilterEditor`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnFilterEditor) hook to get the current `value` of the filter and also to retrieve the `setValue` function that we need to call when we want to update filtering. **Example: Writing a `bool` filter type with a custom filter editor** The `canDesign` column is using a custom `bool` filter type with a custom filter editor. ```ts import * as React from 'react'; import { InfiniteTable, InfiniteTablePropColumns, DataSource, components, useInfiniteColumnFilterEditor, } from '@infinite-table/infinite-react'; const { CheckBox } = components; type Developer = { id: number; firstName: string; canDesign: boolean; stack: string; hobby: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', canDesign: true, stack: 'frontend', hobby: 'gaming', }, { id: 2, firstName: 'Jane', canDesign: false, stack: 'backend', hobby: 'reading', }, { id: 3, firstName: 'Jack', canDesign: true, stack: 'frontend', hobby: 'gaming', }, { id: 4, firstName: 'Jill', canDesign: false, stack: 'backend', hobby: 'reading', }, { id: 5, firstName: 'Seb', canDesign: false, stack: 'backend', hobby: 'reading', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, canDesign: { field: 'canDesign', filterType: 'bool', renderValue: ({ value }) => (value ? 'Yes' : 'No'), }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, }; const domProps = { style: { height: '100%', }, }; function BoolFilterEditor() { const { value, setValue, className } = useInfiniteColumnFilterEditor(); return (
{ if (value === true) { // after the value was true, make it go to indeterminate state newValue = null; } if (value === null) { // from indeterminate, goto false newValue = false; } setValue(newValue); }} />
); } export default () => { return ( <> data={dataSource} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterTypes={{ bool: { defaultOperator: 'eq', emptyValues: [null], components: { FilterEditor: BoolFilterEditor, FilterOperatorSwitch: () => null, }, operators: [ { name: 'eq', label: 'Equals', fn: ({ currentValue, filterValue }) => currentValue === filterValue, }, ], }, }} > debugId="checkbox-filter-editor-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} />
); }; ``` --- # Getting Started > Get help starting with Infinite Table for React. Our DataGrid component helps with sorting, filtering, row/column grouping, pivoting, aggregations ... Canonical page: https://infinite-table.com/docs/learn/getting-started/ > `Infinite Table` is a UI component for data virtualization - helps you display huge datasets of tabular data. It's built specifically for React from the ground up and with performance in mind. # Installation `Infinite Table` is available on the public [npm registry](https://www.npmjs.com/package/@infinite-table/infinite-react) - install it by running the following command: npm i @infinite-table/infinite-react ## Meet the Code This is an example to get you started with Infinite Table with minimal setup. ```ts import * as React from 'react'; import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Person = { Id: number; FirstName: string; Age: number; }; const columns: InfiniteTablePropColumns = { id: { // specifies which field from the data source // should be rendered in this column field: 'Id', type: 'number', defaultWidth: 80, }, firstName: { field: 'FirstName', header: 'First Name', }, age: { field: 'Age', type: 'number' }, }; const data: Person[] = [ { Id: 1, FirstName: 'Bob', Age: 3, }, { Id: 2, FirstName: 'Alice', Age: 50, }, { Id: 3, FirstName: 'Bill', Age: 5, }, ]; export default function App() { return ( data={data} primaryKey="Id"> debugId="meet-the-code" columns={columns} />
); } ``` Don't forget to import the CSS to see the component in action! ```tsx import '@infinite-table/infinite-react/index.css'; ``` ## Using the Components In the code snippet above, you notice we're using 2 components: - `DataSource` - this needs to be a parent (or ancestor, at any level) of the `InfiniteTable` component - it controls which `data` the table is rendering - `InfiniteTable` - the actual virtualized table component - needs to be inside a `DataSource` (can be at any level of nesting). Both components are named exports of the `@infinite-table/infinite-react` package. ## TypeScript Types Our `TypeScript` types are published as part of the package, as named exports from the root of the package. There are 2 components that you can use and import: - `InfiniteTable` - `DataSource` Each of those has types provided for all the props it exposes, with the pattern of `Prop`, so here are a few examples to clarify the rule: ```ts import { InfiniteTablePropColumns, // or accessible as InfiniteTableProps['columns'] // corresponding to the `columns` prop DataSourcePropGroupBy, // or accessible as DataSourceProps['groupBy'] // corresponding to the `groupBy` prop } from '@infinite-table/infinite-react'; ``` Read more about how to use our TypeScript types. ## Built for React from the ground-up `Infinite Table` is built specifically for React and is fully declarative and fully typed. When you use `Infinite Table`, it feels at-home in your React application - every prop has both a controlled and uncontrolled version so you get full control over every area of the component. This is an example of how you might configure `InfiniteTable` in a real-world application and puts together several functionalities: - grouping - aggregation - pinned columns - sorting - multiple selection - custom cell rendering ```ts 'use client'; import { InfiniteTable, DataSource, GroupRowsState, InfiniteTablePropColumnTypes, DataSourcePropRowSelection_MultiRow, InfiniteTableColumn, InfiniteTableColumnRenderValueParam, DataSourcePropAggregationReducers, DataSourceGroupBy, components, DataSourcePropFilterValue, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const { CheckBox } = components; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const showLogo = typeof window === 'undefined' ? true : window.location.host.startsWith('localhost:') || window.location.host.startsWith('infinite-table.com'); const avgReducer = { initialValue: 0, reducer: (acc: number, sum: number) => acc + sum, done: (value: number, arr: any[]) => arr.length ? Math.floor(value / arr.length) : 0, }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { field: 'salary', ...avgReducer, }, age: { field: 'age', ...avgReducer, }, currency: { field: 'currency', initialValue: new Set(), reducer: (acc: Set, value: string) => { acc.add(value); return acc; }, done: (value: Set) => { return value.size > 1 ? 'Mixed' : value.values().next().value; }, }, canDesign: { field: 'canDesign', initialValue: false, reducer: (acc: boolean | null, value: 'yes' | 'no') => { if (acc === null) { return acc; } if (acc === false && value === 'yes') { return null; } if (acc === true && value === 'no') { return null; } return acc; }, }, }; const flags = { 'United States': '🇺🇸', Canada: '🇨🇦', France: '🇫🇷', Germany: '🇩🇪', 'United Kingdom': '🇬🇧', 'South Africa': '🇿🇦', 'New Zealand': '🇳🇿', Sweden: '🇸🇪', China: '🇨🇳', Brazil: '🇧🇷', Turkey: '🇹🇷', Italy: '🇮🇹', India: '🇮🇳', Indonesia: '🇮🇩', Japan: '🇯🇵', Argentina: '🇦🇷', 'Saudi Arabia': '🇸🇦', Mexico: '🇲🇽', 'United Arab Emirates': '🇦🇪', }; function getColumns(): Record> { return { age: { field: 'age', header: 'Age', type: 'number', defaultWidth: 100, renderValue: ({ value }) => value, }, salary: { header: 'Compensation', field: 'salary', type: 'number', defaultWidth: 210, }, currency: { field: 'currency', header: 'Currency', defaultWidth: 100 }, preferredLanguage: { field: 'preferredLanguage', header: 'Programming Language', }, canDesign: { defaultWidth: 135, field: 'canDesign', header: 'Design Skills', renderMenuIcon: false, renderValue: ({ value }) => { return (
{value === null ? 'Some' : value === 'yes' ? 'Yes' : 'No'}
); }, }, country: { field: 'country', header: 'Country', renderValue: ({ value }) => { return ( {(flags as any)[value] || null} {value} ); }, }, firstName: { field: 'firstName', header: 'First Name' }, stack: { field: 'stack', header: 'Stack' }, city: { field: 'city', header: 'City' }, }; } // → 123.456,789 const groupColumn: InfiniteTableColumn = { header: 'Grouping', field: 'firstName', defaultWidth: 250, renderSelectionCheckBox: true, defaultFilterable: false, // in this function we have access to collapsed info // and grouping info about the current row - see rowInfo.groupBy renderValue: ({ value, rowInfo, }: InfiniteTableColumnRenderValueParam) => { if (!rowInfo.isGroupRow) { return value; } const groupBy = rowInfo.groupBy || []; const collapsed = rowInfo.collapsed; const currentGroupBy = groupBy[groupBy.length - 1]; if (currentGroupBy?.field === 'age') { return `🥳 ${value}${collapsed ? ' 🤷‍♂️' : ''}`; } return `${value}`; }, }; const defaultGroupRowsState = new GroupRowsState({ //make all groups collapsed by default collapsedRows: true, expandedRows: [ ['United States'], ['United States', 'backend'], ['France'], ['Turkey'], ], }); const columnTypes: InfiniteTablePropColumnTypes = { number: { align: 'end', style: () => { return {}; }, renderValue: ({ value, data, rowInfo }) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: rowInfo.isGroupRow && rowInfo.data?.currency === 'Mixed' ? 'USD' : data?.currency || 'USD', }).format(value); }, }, }; const defaultRowSelection: DataSourcePropRowSelection_MultiRow = { selectedRows: [['United States'], ['India'], ['France'], ['Turkey']], deselectedRows: [['United States', 'frontend']], defaultSelection: false, }; const defaultFilterValue: DataSourcePropFilterValue = [ { field: 'age', filter: { operator: 'gt', type: 'number', value: null, }, }, ]; const domProps = { style: { minHeight: '50vh', height: 600, margin: 5, }, }; export default function App() { const [{ min, max }, setMinMax] = useState({ min: 0, max: 0 }); const columns = React.useMemo(() => { const cols = getColumns(); if (cols.salary) { cols.salary.render = ({ renderBag, value }) => { const increase: number = Math.abs(max - min); const percentage = ((value - min) / increase) * 100; const alpha = Number((percentage / 100).toPrecision(2)) + 0.2; const backgroundColor = `rgba(255, 0, 0, ${alpha})`; return (
{renderBag.all}
); }; } return cols; }, [min, max]); const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', }, { field: 'stack' }, ], [], ); const Link = ({ children, href, }: { children: React.ReactNode; href: string; }) => { return ( {children} ); }; return ( <> {showLogo ? (
Go Back Home View source
) : null} data={dataSource} primaryKey="id" defaultFilterValue={defaultFilterValue} filterMode="local" useGroupKeysForMultiRowSelection defaultRowSelection={defaultRowSelection} defaultSortInfo={{ dir: -1, field: 'country', }} onDataArrayChange={(data) => { const min = Math.min(...data.map((data) => data.salary ?? 0)); const max = Math.max(...data.map((data) => data.salary ?? 0)); setMinMax({ min, max }); }} defaultGroupRowsState={defaultGroupRowsState} aggregationReducers={aggregationReducers} groupBy={groupBy} > debugId="full-demo" groupRenderStrategy="single-column" defaultColumnPinning={{ 'group-by': true, }} domProps={domProps} defaultActiveRowIndex={0} groupColumn={groupColumn} licenseKey={process.env.NEXT_PUBLIC_INFINITE_LICENSE_KEY} columns={columns} columnTypes={columnTypes} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ## Licensing You can use `@infinite-table/infinite-react` in 2 ways: - with a license - requests for license quotations and additional quotations must be made by email to admin@infinite-table.com. After purchasing, you will receive a `licenseKey` which you will provide as a prop when you instantiate Infinite Table. This will make the [Powered by Infinite Table](https://infinite-table.com) footer go away. - without a license, but it will include a [Powered by Infinite Table](https://infinite-table.com) link in the table footer. This way you can use it for free in any product, but make sure the footer is always visible when Infinite Table is visible. For demo purposes, we don't show any license error for embeds in [codesandbox.io](https://codesandbox.io) - which are used throughout this demo site. Check the demo below to see the license footer in action. ```ts live title="Invalid License Demo" files="invalid-license.page.tsx,data.tsx" ``` Read more about our licensing model and how you can use Infinite Table. ## About the Docs > We're grateful for the work done by the [team behind reactjs.org](https://github.com/reactjs/reactjs.org) and the new React documentation found at [beta.reactjs.org](https://beta.reactjs.org/) - we've built our documentation on their excellent work 🙏 and we're grateful for that. The documentation is versioned, and we will publish a new version of the documentation when there are any significant changes in the corresponding `@infinite-table/infinite-react` version. --- # Licensing Infinite Table > Free Usage and Licensing with Infinite Table Canonical page: https://infinite-table.com/docs/learn/getting-started/licensing ## Free Usage You can immediately download and use [Infinite Table from the NPM registry](https://www.npmjs.com/package/@infinite-table/infinite-react) free of charge. When using Infinite Table for free, you have access to **all the features** of Infinite Table, but you will see a [Powered by Infinite Table](https://infinite-table.com) link in the table footer. You can use it for free in any product, but make sure the footer is always visible when Infinite Table is visible. For demo purposes, we don't show any license error for embeds in [codesandbox.io](https://codesandbox.io) - which are used throughout this demo site. Check the demo below to see the license footer in action. **Example: Invalid License Demo** ```ts files=["invalid-license.page.tsx","data.tsx"] ``` ## Licensed Usage We invite you to try out Infinite Table and explore all the features it has to offer. We're confident it will be a useful addition to your React applications. We encourage companies 🙌 to purchase development licenses for their teams and help us improve the product and support the development of new features. See our pricing page for more information on how to purchase a license. --- # Getting Started Test > Get help starting with Infinite Table for React. Our DataGrid component helps with sorting, filtering, row/column grouping, pivoting, aggregations ... Canonical page: https://infinite-table.com/docs/learn/getting-started/test good morning `radu` how are you? ```hello``` [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) --- # TypeScript Types > Infinite Table types for TypeScript are published as part of the package, as named exports from the root of the package. Canonical page: https://infinite-table.com/docs/learn/getting-started/typescript-types Our `TypeScript` types are published as part of the package, as named exports from the root of the package. The 2 main components that you can need to use and import are: - `InfiniteTable` - `DataSource` ```tsx title="Importing InfiniteTable and DataSource components" import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; ``` In our TypeScript typings, those components are exported as generic components, so they need to be bound to the type of the data they are rendering. ```tsx type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; } const App = () => { return data={data} primaryKey="id"> columns={{...}} /> } ``` Throughout the documentation, we will use the `DATA_TYPE` placeholder to refer to the type of the data that the `InfiniteTable` and `DataSource` components are bound to. You can still use `InfiniteTable` in plain JavaScript, but you won't get all the type-checking benefits. Both `InfiniteTable` and `DataSource` components have types provided for most of the props they support. Generally the naming pattern is `Prop`, so here are a few examples to clarify the rule: ```ts import type { InfiniteTablePropColumns, // corresponding to the `columns` prop DataSourcePropGroupBy, // corresponding to the `groupBy` prop } from '@infinite-table/infinite-react'; ``` ## `DataSource` Types Here are a few examples for types for the `DataSource` component: - `DataSourcePropGroupBy` - the type for [DataSource.groupBy](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) ```tsx import type { DataSourcePropGroupBy } from '@infinite-table/infinite-react'; ``` - `DataSourcePropAggregationReducers` - the type for [DataSource.aggregationReducers](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) ```tsx import type { DataSourcePropAggregationReducers } from '@infinite-table/infinite-react'; ``` Not all the `DataSource` props have types exported that follow this convention, so you can always use `DataSourceProps` to get the type that define all the props. In this way you can access specific prop types by name - `DataSourceProps['groupBy']` - the type for [DataSource.groupBy](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) - `DataSourceProps['data']` - the type for [DataSource.data](https://infinite-table.com/docs/reference/datasource-props/index.md#data) - etc ## `InfiniteTable` Types Below you can find a few examples for types for the `InfiniteTable` component: - `InfiniteTablePropColumns` - the type for [InfiniteTable.columns](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) ```tsx import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; ``` - `InfiniteTablePropRowStyle` - the type for [InfiniteTable.rowStyle](https://infinite-table.com/docs/reference/infinite-table-props.md#rowStyle) ```tsx import type { InfiniteTablePropRowStyle } from '@infinite-table/infinite-react'; ``` - `InfiniteTablePropColumnGroups` - the type for [InfiniteTable.columnGroups](https://infinite-table.com/docs/reference/infinite-table-props.md#columnGroups) ```tsx import type { InfiniteTablePropColumnGroups } from '@infinite-table/infinite-react'; ``` Not all the `InfiniteTable` props have types exported that follow this convention, so you can always use `InfiniteTableProps` to get the type that define all the props the `InfiniteTable` component supports. In this way you can access specific prop types by name: - `InfiniteTableProps['columns']` - the type for [InfiniteTable.columns](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) - `InfiniteTableProps['columnSizing']` - the type for [InfiniteTable.columnSizing](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) - etc Worth mentioning is the `InfiniteTableColumn` prop, which defines the type for the table [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns). --- # Grouping and Pivoting Canonical page: https://infinite-table.com/docs/learn/grouping-and-pivoting/ Infinite Table comes with grouping and pivoting capabilities built-in. The `DataSource` component does the actual data grouping and pivoting - while the `InfiniteTable` component does the specialized rendering. Learn row grouping and explore the possibilities. Read thorough documentation covering pivoting and aggregation. **Example: Simple row grouping** ```ts files=["row-grouping-example.page.tsx","columns.ts"] ``` --- # Aggregations > Learn how to define & use aggregations on grouped rows in Infinite Table for React. Canonical page: https://infinite-table.com/docs/learn/grouping-and-pivoting/group-aggregations A natural next step when grouping data is **aggregating the grouped values**. We allow developers to define any number of aggregations and bind them to any column. The aggregations are defined on the `` component and are easily available at render time. A client-side aggregation needs a reducer function that accumulates the values in the data array and computes the final result. Throughout the docs, we might refer to aggregations as reducers - which, more technically, they are, since they reduce an array of values (from a group) to a single value. ## Client-Side Aggregations When using client-side aggregation, each [aggregation](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) can have the following: ### An initial value The `initialValue` is optional value to use as the initial (accumulator) value for the reducer function. You can think of aggregations as an "enhanced" version of [Array.reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce), so initial value should sound familiar. The `initialValue` can be a function - in this case it will be called to compute the initial value. ### A reducer function `reducer` is the function to call for each value in the (grouped) data array. It is called with the following arguments: - `accumulator` - the value returned by the previous call to the reducer function, or the `initialValue` if this is the first call. You return the new accumulator value from this function. - `value` - the value of the current item in the data array. If the aggregation has a `field`, this is the value of that field in the current item. Otherwise, value is the result of calling the `reducer.getter(data)` function (if one exists) or null if no getter is defined. - `dataItem` - the current item in the data array. - `index` - the index of the current item in the data array. ### A `field` property or a `getter` function For simple use-cases of client-side aggregations, a `field` is the way to go. This defines the field property (from the DATA_TYPE) to which the aggregation is bound. For more complex scenarios, the aggregation should have a `getter` function. If both a `field` and a `getter` are provided, the `getter` has higher priority and will be used. Use this `getter` function to compute the value the current item in the array brings to the aggregation. ```tsx title="Aggregation_custom_getter_function" // useful for retrieving nested values getter: (dataItem: Developer) => data.salary.net; ``` For using nested values inside aggregations, use the aggregation `getter` function. ### A completion `done` function The completion `done` function is optional - if specified, will be after iterating over all the values in the grouped data array. Can be used to change the final result of the aggregation. It is called with the following arguments: - `accumulator` - the value returned by the last call to the reducer function - `data` - the grouped data array. This is useful for computing averages, for example: ```tsx title="Done function for avg reducer" done: (acc, data) => acc / data.length; ``` ### Putting it all together Let's take a look at a simple example of aggregating two columns, one to display the avg and the other one should compute the sum of the salary column for grouped rows. ```tsx title="Average Aggregation" import { DataSource, InfiniteTable } from '@infinite-table/infinite-react'; const sum = (a: number, b: number) => a + b; const reducers = { avg: { initialValue: 0, field: 'age', reducer: sum, done: (acc, data) => Math.round(acc / data.length), }, sumAgg: { initialValue: 0, field: 'salary', reducer: sum } } function App() { return aggregationReducers={reducers} > {...} /> } ``` In the above example, note that aggregations are an object where the keys of the object are used to identify the aggregation and the values are the aggregation configuration objects, as described above. At run-time, you have access to the aggregation reducer results inside group rows - you can use the `rowInfo.reducerResults` object to access those values. For the example above, you change how group rows are rendered for a certain column and display the aggregation results in a custom way: ```tsx {9} title="Custom_group_row_rendering_for_the_country_column" country: { field: 'country', // define a custom renderGroupValue fn for the country column renderGroupValue: ({ rowInfo }) => { const { reducerResults = {} } = rowInfo; // note the keys in the reducerResults objects match the keys in the aggregationReducers object return `Avg age: ${reducerResults.avg}, total salary ${reducerResults.sumAgg}`; }, }, ``` **Example: Sum and average aggregation example** ```ts import { InfiniteTable, InfiniteTablePropColumns, DataSource, DataSourcePropAggregationReducers, DataSourceGroupBy, GroupRowsState, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const sum = (a: number, b: number) => a + b; // NOTE the type naming pattern DataSourceProp const reducers: DataSourcePropAggregationReducers = { avg: { initialValue: 0, field: 'age', reducer: sum, done: (acc, data) => Math.round(acc / data.length), }, sumAgg: { initialValue: 0, field: 'salary', reducer: sum, }, }; const columns: InfiniteTablePropColumns = { age: { field: 'age', header: 'Age (avg)' }, salary: { field: 'salary', type: 'number', header: 'Salary (sum)', }, country: { field: 'country', renderGroupValue: ({ rowInfo }) => { const { reducerResults = {} } = rowInfo; return `Avg age: ${reducerResults.avg}, total salary ${reducerResults.sumAgg}`; }, }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const defaultGroupRowsState = new GroupRowsState({ //make all groups collapsed by default collapsedRows: true, expandedRows: [], }); export default function App() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', column: { renderGroupValue: ({ value }) => value, }, }, { field: 'stack' }, ], [], ); return ( data={dataSource} primaryKey="id" defaultGroupRowsState={defaultGroupRowsState} aggregationReducers={reducers} groupBy={groupBy} > debugId="aggregations-simple-example" groupRenderStrategy="multi-column" columns={columns} columnDefaultWidth={250} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ## Server-Side Aggregations Server-side aggregations are defined in the same way as client-side aggregations (except the `reducer` function is missing), but the aggregation values are computed by the server and returned as part of the data response. For computing the grouping and aggregations on the server, the backend needs to know the grouping and aggregation configuration. As such, Infinite Table will call the [DataSource data](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function with an object that contains all the required info: - `groupBy` - the array of grouping fields, as passed to the `` component. - `pivotBy` - the array of pivot fields, as passed to the `` component. - `aggregationReducers` - the value of the [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) prop, as configured on the `` component. - `sortInfo` - the current [sorting information](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) for the data. For the lazy-loading use-case, there are other useful properties you can use from the object passed into the `data` function: - `groupKeys: string[]` - the group keys for the current group - the `data` fn is generally called lazily when the user expands a group row. This info is useful for fetching the data for a specific group. - `lazyLoadStartIndex` - provided when batching is also enabled via the [`lazyLoad`](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad) prop. This is the index of the first item in the current batch. - `lazyLoadBatchSize` - also used when batching is enabled. This is the number of items in the current batch. Besides the above information, if filtering is used, a `fiterValue` is also made available. In order to showcase the server-side aggregations, let's build an example similar to the above one, but let's lazily load group data. ```tsx {2} title="DataSourcewith lazyLoad enabled" ``` As soon a grouping and aggregations are no longer computed on the client, your `data` function needs to send those configurations on the backend, so it needs to get a bit more complicated: ```tsx title="Data_function_sending_configurations_to_the_backend" const data = ({ groupBy, aggregationReducers, sortInfo, groupKeys }) => { // it's important to send the current group keys - for top level, this will be [] const args: string[] = [`groupKeys=${JSON.stringify(groupKeys)}`]; // turn the sorting info into an array if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (sortInfo) { // the backend expects the sort info to be an array of field,dir pairs args.push( 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ), ); } if (groupBy) { // for grouping, send an array of objects with the `field` property args.push( 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))), ); } if (aggregationReducers) { args.push( 'reducers=' + JSON.stringify( // by convention, we send an array of reducers, each with `field` `name`(= "avg") and `id` // it's up to you to decide what the backend needs Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ), ); } const url = BASE_URL + `/developers10k-sql?` + args.join('&'); return fetch(url).then(r=>r.json()) } ``` When fetching without grouping (or with local grouping and aggregations), the `` component expects a flat array of data items coming from the server. However, when the grouping is happening server-side, the `` component expects a response that has the following shape: - `data` - the root array with grouping and aggregation info. Each item in the array should have the following: - `keys` - an array of the keys for the current group - eg `['USA']` or `['USA', 'New York']` - `data` - an object with all the common values for the group - eg `{ country: 'USA' }` or `{ country: 'USA', city: 'New York' }` - `aggregations` - an object with the aggregation values for the group - eg `{ age: 30, salary: 120300 }`. The keys in this object should match the keys in the [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) object. - `pivot` - pivoting information for the current group - more on that on the dedicated [Pivoting page](./pivoting/overview). When the user is expanding the last level, in order to see the leaf rows, the shape of the response is expected to be the same as when there is no grouping - namely an array of data items or an object where the `data` property is an array of data items. Let's put all of this into a working example. **Example** This showcases grouping and aggregations on the server - both the `age` and `salary` columns have an AVG aggregation defined. Grouping is done by the `country`, `city` and `stack` columns. ```tsx import { InfiniteTable, DataSource, DataSourceData, InfiniteTablePropColumns, GroupRowsState, DataSourceGroupBy, DataSourcePropAggregationReducers, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function RemotePivotExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', }, { field: 'city' }, { field: 'stack' }, ], [], ); return ( primaryKey="id" data={dataSource} groupBy={groupBy} aggregationReducers={aggregationReducers} defaultGroupRowsState={groupRowsState} lazyLoad={true} > debugId="grouping-and-aggregations-with-lazy-load-example" scrollStopDelay={10} groupRenderStrategy="single-column" hideEmptyGroupColumns columns={columns} columnDefaultWidth={220} /> ); } const dataSource: DataSourceData = ({ aggregationReducers, groupBy, groupKeys, sortInfo, }) => { // it's important to send the current group keys - for top level, this will be [] const args: string[] = [`groupKeys=${JSON.stringify(groupKeys)}`]; // turn the sorting info into an array if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (sortInfo) { // the backend expects the sort info to be an array of field,dir pairs args.push( 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ), ); } if (groupBy) { // for grouping, send an array of objects with the `field` property args.push( 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))), ); } if (aggregationReducers) { args.push( 'reducers=' + JSON.stringify( // by convention, we send an array of reducers, each with `field` `name` and `id` Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ), ); } return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers30k-sql?` + args.join('&'), ).then((r) => r.json()); }; ``` When the user is doing a sort on the table, the `` is fetched from scratch, but the expanded/collapsed state is preserved, and all the required groups that need to be re-fetched are reloaded as needed (if they are not eagerly included in the served data). --- # Grouping rows Canonical page: https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows You can use any `field` available in the `DataSource` to do the grouping - it can even be a `field` that is not a column. When using TypeScript, both `DataSource` and `InfiniteTable` components are generic and need to be rendered/instantiated with a `DATA_TYPE` parameter. The fields in that `DATA_TYPE` can then be used for grouping. ```tsx type Person = { name: string; age: number; country: string; id: string; } const groupBy = [{field: 'country'}] groupBy={groupBy}> /> ``` In the example above, we're grouping by `country`, which is a field available in the `Person` type. Specifying a field not defined in the `Person` type would be a type error. Additionally, a `column` object can be used together with the `field` to define how the group column should be rendered. ```tsx {4} const groupBy = [ { field: 'country', column: { // custom column configuration for group column width: 150, header: 'Country group', }, }, ]; ``` The example below puts it all together. Also see the [groupBy API reference](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) to find out more. **Example: Simple row grouping** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}, }, }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', // specifying a style here for the column // note: it will also be "picked up" by the group column // if you're grouping by the 'country' field style: { color: 'tomato', }, }, firstName: { field: 'firstName' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="row-grouping-example" columns={columns} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` In `groupBy.column` you can use any column property - so, for example, you can define a custom `renderValue` function to customize the rendering. ```tsx {5} const groupBy = [ { field: 'country', column: { renderValue: ({ value }) => <>Country: {value}, }, }, ]; ``` The generated group column(s) - can be one for all groups or one for each group - will inherit the `style`/`className`/renderers from the columns corresponding to the group fields themselves (if those columns exist). Additionally, there are other ways to override those inherited configurations, in order to configure the group columns: - use [`groupBy.column`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupBy.column) to specify how each grouping column should look for the respective field (in case of [groupRenderStrateg="multi-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy)) - use [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) prop - can be used as an object - ideal for when you have simple requirements and when [groupRenderStrateg="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) - as a function that returns a column configuration - can be used like this in either single or multiple group render strategy ## Controlling the collapse/expand state When you do grouping, by default, all row groups are expanded. Of course you have full control over this and you do this via the [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState)/[`defaultGroupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultGroupRowsState) props. If you simply want to specify the initial expanded/collapsed state, you should use the [`defaultGroupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultGroupRowsState) prop. ```tsx title="Specifying the default state for group rows" const defaultGroupRowsState: DataSourcePropGroupRowsStateObject = { collapsedRows: true, expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']], }; ``` The two properties in this object are `collapsedRows` and `expandedRows`, and each can have the following values: - `true` - meaning that all groups have this state - an array of arrays - representing the exceptions to the default value So if you have `collapsedRows` set to `true` and then `expandedRows` set to `[['Mexico'], ['Mexico', 'backend'], ['India']]` then all rows are collapsed by default, except the rows specified in the `expandedRows`. **Example: Everything is collapsed except a few rows** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, DataSourcePropGroupRowsStateObject, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}, }, }, { field: 'stack', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', // specifying a style here for the column // note: it will also be "picked up" by the group column // if you're grouping by the 'country' field style: { color: 'tomato', }, }, firstName: { field: 'firstName' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, }; const defaultGroupRowsState: DataSourcePropGroupRowsStateObject = { collapsedRows: true, expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']], }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy} defaultGroupRowsState={defaultGroupRowsState} > debugId="row-grouping-state-example" columns={columns} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` You can specify expand/collapse state at any level of nesting. Let's suppose by default all rows are collapsed - if you want a node to be visible then you have to specify all its parents as expanded. So having this ```tsx const defaultGroupRowsState = { collapsedRows: true, expandedRows: [['Mexico', 'backend']], }; ``` will show all rows as collapsed, and just as soon as you expand `Mexico` you will see the `backend` group row for Mexico to be expanded. This data format gives you ultimate flexibility and allows you to easily restore an expand/collpase state at a later time, if you wanted to. If you use the controlled [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState), make sure you update it by leveraging the [`onGroupRowsStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onGroupRowsStateChange) callback prop. **Example: Using controlled expanded/collapsed state for group rows** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}, }, }, { field: 'stack', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', // specifying a style here for the column // note: it will also be "picked up" by the group column // if you're grouping by the 'country' field style: { color: 'tomato', }, }, firstName: { field: 'firstName' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, }; export default function App() { const [groupRowsState, setGroupRowsState] = React.useState< GroupRowsState >(() => { const groupRowsState = new GroupRowsState({ collapsedRows: true, expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']], }); return groupRowsState; }); return ( <> data={dataSource} primaryKey="id" groupBy={groupBy} groupRowsState={groupRowsState} onGroupRowsStateChange={setGroupRowsState} > debugId="row-grouping-state-controlled-example" columns={columns} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` In addition to simple objects with the shape described above, the [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState)/[`defaultGroupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultGroupRowsState) can also be instanges of `GroupRowsState` class, which is exported by the Infinite Table package. This class is simply a wrapper around those objects, but it gives you additional utility methods. The [`onGroupRowsStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onGroupRowsStateChange) callback gives you an instance of [`GroupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#GroupRowsState) back as the single argument. If you're using plain objects, just do `groupRowsState.getState()` and you'll get the corresponding plain object for the current expand/collapse state. [`GroupRowsState`](https://infinite-table.com/docs/reference/type-definitions/index.md#GroupRowsState) give you some additional helper methods, which you can read about [here](https://infinite-table.com/docs/reference/type-definitions/index.md#GroupRowsState) ## Grouping strategies Multiple grouping strategies are supported by, `InfiniteTable` DataGrid: - multi column mode - multiple group columns are generated, one for each specified group field - single column mode - a single group column is generated, even when there are multiple group fields You can specify the rendering strategy explicitly by setting the [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) property to any of the following: `multi-column`, `single-column`. If you don't set it explicitly, it will choose the best default based on your configuration. ### Multiple groups columns When grouping by multiple fields, by default the component will render a group column for each group field ```tsx const groupBy = [ { field: 'age', column: { width: 100, renderValue: ({ value }) => <>Age: {value}, }, }, { field: 'companyName', }, { field: 'country', }, ]; ``` Let's see an example of how the component would render the table with the multi-column strategy. **Example: Multi-column group render strategy** ```ts files=["row-grouping-multi-column-example.page.tsx","columns.ts"] ``` For the `multi-column` strategy, you can use [`hideEmptyGroupColumns`](https://infinite-table.com/docs/reference/infinite-table-props.md#hideEmptyGroupColumns) in order to hide columns for groups which are currently not visible. **Example: Hide Empty Group Columns** ```ts files=["$DOCS/reference/hideEmptyGroupColumns-example.page.tsx","$DOCS/reference/employee-columns.ts"] ``` You can specify an `id` for group columns. This is helpful if you want to size those columns (via [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing)) or pin them (via [`columnPinning`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnPinning)) or configure them in other ways. If no `id` is specified, it will be generated like this: `"group-by-${field}"` ### Single group column You can group by multiple fields, yet only render a single group column. To choose this rendering strategy, specify [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) property to be `single-column` (or specify [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) as an object.) In this case, you can't override the group column for each group field, as there's only one group column being generated. However, you can specify a [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) property to customize the generated column. By default the generated group column will "inherit" many of the properties (the column style or className or renderers) of the columns corresponding to the group fields (if such columns exist, because it's not mandatory that they are defined). **Example: Single-column group render strategy** ```ts files=["row-grouping-single-column-example.page.tsx","columns.ts"] ``` If [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) is specified to an object and no [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is passed, the render strategy will be `single-column`. [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) can also be a function, which allows you to individually customize each group column - in case the `multi-column` strategy is used. You can specify an `id` for the single [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn). This is helpful if you want to size this column (via [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing)) or pin it (via [`columnPinning`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnPinning)) or configure it in other ways. If no `id` is specified, it will default to `"group-by"`. ## Customizing the group column There are many ways to customize the group column(s) and we're going to show a few of them below: ### Binding the group column to a `field` By default, group columns only show values in the group rows - but they are normal columns, so why not bind them to a [field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) of the `DATA_TYPE`? ```tsx {6,11} const groupColumn = { id: 'the-group', // can specify an id style: { color: 'tomato', }, field: 'firstName', // non-group rows will render the first name }; const columns = { theFirstName: { field: 'firstName', style: { // this style will also be applied in the group column, // since it is bound to this same `field` fontWeight: 'bold', }, }, }; ``` This makes the column display the value of the `field` in non-group/normal rows. Also, if you have another column bound to that `field`, the renderers/styling of that column will be used for the value of the group column, in non-group rows. **Example: Bind group column to a field** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, theFirstName: { field: 'firstName', style: { color: 'orange', }, renderLeafValue: ({ value }) => { return `${value}!`; }, }, stack: { field: 'stack', style: { color: 'tomato', }, }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; const groupColumn = { field: 'firstName' as keyof Developer, }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="bind-group-column-to-field-example" groupColumn={groupColumn} columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### Use `groupColumn` to customize rendering The [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) will inherit its own rendering and styling from the columns that are bound to the fields used in [`groupBy.field`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy.field). However, you can override any of those properties so you have full control over the rendering process. ```tsx {3,6} const groupColumn = { field: 'firstName', renderGroupValue: ({ value }) => { return `Group: ${value}`; }, renderLeafValue: ({ value }) => { return `First name: ${value}`; }, }; ``` **Example: Customize group column renderer** The column that renders the `firstName` has a custom renderer that adds a `.` at the end. The group column is bound to the same `firstName` field, but specifies a different renderer, which will be used instead. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTableColumn, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', style: { color: 'orange', }, renderValue: ({ value, rowInfo }) => rowInfo.isGroupRow ? null : `${value}.`, }, stack: { field: 'stack', style: { color: 'tomato', }, }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; const groupColumn: InfiniteTableColumn = { field: 'firstName', renderValue: ({ value }) => { return `First name: ${value}`; }, }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="group-column-custom-renderers-example" groupColumn={groupColumn} columns={columns} columnDefaultWidth={250} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` Learn more about customizing column rendering via multiple renderer functions. ## Hiding columns when grouping When grouping is enabled, you can choose to hide some columns. Here are the two main ways to do this: - use [`hideColumnWhenGrouped`](https://infinite-table.com/docs/reference/infinite-table-props.md#hideColumnWhenGrouped) - this will make columns bound to the group fields be hidden when grouping is active - use [`columns.defaultHiddenWhenGroupedBy`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultHiddenWhenGroupedBy) (also available on the column types, as [`columnTypes.defaultHiddenWhenGroupedBy`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultHiddenWhenGroupedBy)) - this is a column-level property, so you have more fine-grained control over what is hidden and when. Valid values for [`columns.defaultHiddenWhenGroupedBy`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultHiddenWhenGroupedBy) are: - `"*"` - when any grouping is active, hide the column that specifies this property - `true` - when the field this column is bound to is used in grouping, hides this column - `keyof DATA_TYPE` - specify an exact field that, when grouped by, makes this column be hidden - `{[k in keyof DATA_TYPE]: true}` - an object that can specify more fields. When there is grouping by any of those fields, the current column gets hidden. **Example: Hide columns when grouping** In this example, the column bound to `firstName` field is set to hide when any grouping is active, since the group column is anyways found to the `firstName` field. In addition, [`hideColumnWhenGrouped`](https://infinite-table.com/docs/reference/infinite-table-props.md#hideColumnWhenGrouped) is set to `true`, so the `stack` and `preferredLanguage` columns are also hidden, since they are grouped by. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, theFirstName: { field: 'firstName', style: { color: 'orange', }, // hide this column when grouping active // as the group column is anyways bound to this field defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', style: { color: 'tomato', }, }, preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; const groupColumn = { field: 'firstName' as keyof Developer, }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="hide-columns-when-grouping-example" groupColumn={groupColumn} columns={columns} hideColumnWhenGrouped columnDefaultWidth={250} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ## Sorting the group column When [groupRenderStrategy="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is used, the group column is sortable by default if all the columns that are involved in grouping are sortable. Sorting the group column makes the `sortInfo` have a value that looks like this: ```ts const sortInfo = [ { dir: 1, id: 'group-by', field: ['stack', 'age'], type: ['string', 'number'], }, ]; ``` [groupRenderStrategy="multi-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy), each group column is sortable by default if the column with the corresponding field is sortable. The [`columnDefaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultSortable) property can be used to override the default behavior. **Example: Group column with initial descending sorting** ```ts import { InfiniteTable, DataSource, DataSourcePropSortInfo, } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, theFirstName: { field: 'firstName', style: { color: 'orange', }, // hide this column when grouping active // as the group column is anyways bound to this field defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', style: { color: 'tomato', }, }, preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; const groupColumn = { field: 'firstName' as keyof Developer, }; const defaultSortInfo: DataSourcePropSortInfo = [ { field: ['stack', 'preferredLanguage'], dir: -1, id: 'group-by', }, ]; export default function App() { return ( data={dataSource} primaryKey="id" defaultSortInfo={defaultSortInfo} groupBy={groupBy} > debugId="group-column-sorted-initially-example" groupColumn={groupColumn} columns={columns} hideColumnWhenGrouped columnDefaultWidth={250} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers10') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` When a group column is configured and the `groupBy` fields are not bound to actual columns in the table, the group column will not be sortable by default. If you want to make it sortable, you have to specify a [`columns.sortType`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) array, of the same length as the `groupBy` array, that specifies the sort type for each group field. ## Aggregations When grouping, you can also aggregate the values of the grouped rows. This is done via the [DataSource.aggregationReducers](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) property. See the example below **Example: Grouping with aggregations** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { InfiniteTableColumn, InfiniteTablePropColumns, InfiniteTableColumnRenderValueParam, DataSourcePropAggregationReducers, DataSourceGroupBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const avgReducer = { initialValue: 0, reducer: (acc: number, sum: number) => acc + sum, done: (value: number, arr: any[]) => arr.length ? Math.floor(value / arr.length) : 0, }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { field: 'salary', ...avgReducer, }, age: { field: 'age', ...avgReducer, }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const groupColumn: InfiniteTableColumn = { header: 'Grouping', defaultWidth: 250, // in this function we have access to collapsed info // and grouping info about the current row - see rowInfo.groupBy renderValue: ({ value, rowInfo, }: InfiniteTableColumnRenderValueParam) => { if (!rowInfo.isGroupRow) { return value; } const groupBy = rowInfo.groupBy || []; const collapsed = rowInfo.collapsed; const currentGroupBy = groupBy[groupBy.length - 1]; if (currentGroupBy?.field === 'age') { return `🥳 ${value}${collapsed ? ' 🤷‍♂️' : ''}`; } return `🎉 ${value}`; }, }; const defaultGroupRowsState = new GroupRowsState({ //make all groups collapsed by default collapsedRows: true, expandedRows: [], }); export default function App() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', }, { field: 'stack' }, ], [], ); return ( data={dataSource} primaryKey="id" defaultGroupRowsState={defaultGroupRowsState} aggregationReducers={aggregationReducers} groupBy={groupBy} > debugId="grouping-with-aggregations-example" groupRenderStrategy="single-column" groupColumn={groupColumn} columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers10k') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` Each [reducer](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) from the `aggregationReducers` map can have the following properties: - `field` - the field to aggregate on - `getter(data)` - a value-getter function, if the aggregation values are are not mapped directly to a `field` - `initialValue` - the initial value to start with when computing the aggregation (for client-side aggregations only) - `reducer: string | (acc, current, data: DATA_TYPE, index)=>value` - the reducer function to use when computing the aggregation (for client-side aggregations only). For server-side aggregations, this will be a `string` - `done(value, arr)` - a function that is called when the aggregation is done (for client-side aggregations only) and returns the final value of the aggregation - `name` - useful especially in combination with [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy), as it will be used as the pivot column header. If an aggregation reducer is bound to a `field` in the dataset, and there is a column mapped to the same `field`, that column will show the corresponding aggregation value for each group row, as shown in the example above. If you want to prevent the user to expand the last level of group rows, you can override the `render` function for the group column **Example: Customized group expand on last group level** ```ts import { InfiniteTable, DataSource, DataSourcePropAggregationReducers, InfiniteTablePropColumns, DataSourceGroupBy, GroupRowsState, InfiniteTableGroupColumnFunction, InfiniteTableGroupColumnBase, InfiniteTableColumnCellContextType, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const avgReducer = { initialValue: 0, reducer: (acc: number, sum: number) => acc + sum, done: (value: number, arr: any[]) => arr.length ? Math.floor(value / arr.length) : 0, }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { field: 'salary', ...avgReducer, }, age: { field: 'age', ...avgReducer, }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; // TODO remove this after the next release //@ts-ignore const groupColumn: InfiniteTableGroupColumnFunction = (arg) => { const column = {} as Partial>; if (arg.groupIndexForColumn === arg.groupBy.length - 1) { column.render = (param: InfiniteTableColumnCellContextType) => { const { value, rowInfo } = param; if ( rowInfo.isGroupRow && rowInfo.groupBy?.length != rowInfo.rootGroupBy?.length ) { // we are on a group row that is the last grouping level return null; } return value; }; } return column; }; const defaultGroupRowsState = new GroupRowsState({ //make all groups collapsed by default collapsedRows: true, expandedRows: [], }); export default function App() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', }, { field: 'stack' }, ], [], ); return ( data={dataSource} primaryKey="id" defaultGroupRowsState={defaultGroupRowsState} aggregationReducers={aggregationReducers} groupBy={groupBy} > debugId="grouping-with-aggregations-discard-expand-example" groupRenderStrategy="multi-column" groupColumn={groupColumn} columns={columns} columnDefaultWidth={250} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers10k') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` Dive deeper into the aggregation reducers and how they work. ## Server side grouping with lazy loading Lazy loading becomes all the more useful when working with grouped data. The `DataSource` [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function is called with an object that has all the information about the current `DataSource` state(grouping/pivoting/sorting/lazy-loading, etc) - see the paragraphs above for details. Server side grouping needs two kinds of data responses in order to work properly: - response for **non-leaf row groups** - these are groups that have children. For such groups (including the top-level group), the `DataSource.data` function must return a promise that's resolved to an object with the following properties: - `totalCount` - the total number of records in the group - `data` - an array of objects that describes non-leaf child groups, each object has the following properties: - `keys` - an array of the group keys (usually strings) that uniquely identifies the group, from the root to the current group - `data` - an object that describes the common properties of the group - `aggregations` - an object that describes the aggregations for the current group - response for **leaf rows** - these are normal rows - rows that would have been served in the non-grouped response. The resolved object should have the following properties: - `data` - an array of objects that describes the rows - `totalCount` - the total number of records on the server, that are part of the current group Here's an example, that assumes grouping by `country` and `city` and aggregations by `age` and `salary` (average values): ```tsx //request: groupKeys: [] // empty keys array, so it's a top-level group groupBy: [{"field":"country"},{"field":"city"}] reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}] // lazyLoadStartIndex: 0, - passed if lazyLoad is configured with a batchSize // lazyLoadBatchSize: 20 - passed if lazyLoad is configured with a batchSize //response { cache: true, totalCount: 20, data: [ { data: {country: "Argentina"}, aggregations: {avgSalary: 20000, avgAge: 30}, keys: ["Argentina"], }, { data: {country: "Australia"}, aggregations: {avgSalary: 25000, avgAge: 35}, keys: ["Australia"], } //... ] } ``` Now let's expand the first group and see how the request/response would look like: ```tsx //request: groupKeys: ["Argentina"] groupBy: [{"field":"country"},{"field":"city"}] reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}] //response { totalCount: 4, data: [ { data: {country: "Argentina", city: "Buenos Aires"}, aggregations: {avgSalary: 20000, avgAge: 30}, keys: ["Argentina", "Buenos Aires"], }, { data: {country: "Argentina", city: "Cordoba"}, aggregations: {avgSalary: 25000, avgAge: 35}, keys: ["Argentina", "Cordoba"], }, //... ] } ``` Finally, let's have a look at the leaf/normal rows and a request for them: ```tsx //request groupKeys: ["Argentina","Buenos Aires"] groupBy: [{"field":"country"},{"field":"city"}] reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}] //response { totalCount: 20, data: [ { id: 34, country: "Argentina", city: "Buenos Aires", age: 30, salary: 20000, stack: "full-stack", firstName: "John", //... }, { id: 35, country: "Argentina", city: "Buenos Aires", age: 35, salary: 25000, stack: "backend", firstName: "Jane", //... }, //... ] } ``` When a row group is expanded, since `InfiniteTable` has the group `keys` from the previous response when the node was loaded, it will use the `keys` array and pass them to the `DataSource.data` function when requesting for the children of the respective group. You know when to serve last-level rows, because in that case, the length of the `groupKeys` array will be equal to the length of the `groupBy` array. **Example: Server side grouping with lazy loding** ```ts import { InfiniteTable, DataSource, DataSourceData, InfiniteTablePropColumns, GroupRowsState, DataSourceGroupBy, DataSourcePropAggregationReducers, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function RemotePivotExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', }, { field: 'city' }, { field: 'stack' }, ], [], ); return ( primaryKey="id" data={dataSource} groupBy={groupBy} aggregationReducers={aggregationReducers} defaultGroupRowsState={groupRowsState} lazyLoad={true} > debugId="server-side-grouping-with-lazy-load-example" scrollStopDelay={10} hideEmptyGroupColumns columns={columns} columnDefaultWidth={220} /> ); } const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, lazyLoadStartIndex, lazyLoadBatchSize, groupKeys = [], sortInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const startLimit: string[] = []; if (lazyLoadBatchSize && lazyLoadBatchSize > 0) { const start = lazyLoadStartIndex || 0; startLimit.push(`start=${start}`); startLimit.push(`limit=${lazyLoadBatchSize}`); } const args = [ ...startLimit, pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers30k-sql?` + args, ).then((r) => r.json()); }; ``` ## Eager loading for group row nodes When using lazy-loading together with batching, node data (without children) is loaded when a node (normal or grouped) comes into view. Only when a group node is expanded will its children be loaded. However, you can do this loading eagerly, by using the `dataset` property on the node you want to load. This can be useful in combination with using `dataParams.groupRowsState` from the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function - so your datasource can know which groups are expanded, and thus it can serve those groups already loaded with children. ```tsx {18} //request: groupKeys: [] // empty keys array, so it's a top-level group groupBy: [{"field":"country"},{"field":"city"}] reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}] // lazyLoadStartIndex: 0, - passed if lazyLoad is configured with a batchSize // lazyLoadBatchSize: 20 - passed if lazyLoad is configured with a batchSize //response { cache: true, totalCount: 20, data: [ { data: {country: "Argentina"}, aggregations: {avgSalary: 20000, avgAge: 30}, keys: ["Argentina"], // NOTE this dataset property used for eager-loading of group nodes dataset: { // the shape of the dataset is the same as the one normally returned by the datasource cache: true, totalCount: 4, data: [ { data: {country: "Argentina", city: "Buenos Aires"}, aggregations: {avgSalary: 20000, avgAge: 30}, keys: ["Argentina", "Buenos Aires"], }, { data: {country: "Argentina", city: "Cordoba"}, aggregations: {avgSalary: 25000, avgAge: 35}, keys: ["Argentina", "Cordoba"], }, ] } }, { data: {country: "Australia"}, aggregations: {avgSalary: 25000, avgAge: 35}, keys: ["Australia"], } //... ] } ``` --- # Customizing Pivot Columns Canonical page: https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/customizing-pivot-columns There are a number of ways to customize the generated pivot columns and we'll cover each of them in this page ## Inheriting from initial columns Pivoting is all about aggregations, so you need to specify the [reducers](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) that will aggregate your data. Each reducer can have a `field` property that specifies the field that will be used for aggregation. If the table [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) collection already has a column bound to the `field` used in the aggregation, the column configuration will be inherited by the generated pivot column. ```ts const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage', style: { color: 'blue' }, }, age: { field: 'age', style: { color: 'magenta', background: 'yellow', }, }, salary: { field: 'salary', type: 'number', style: { color: 'red', }, }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, id: { field: 'id' }, }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => { return Math.floor(arr.length ? sum / arr.length : 0); }, }; const aggregationReducers: DataSourceProps['aggregationReducers'] = { // will have the same configuration as the `salary` column avgSalary: { field: 'salary', ...avgReducer }, avgAge: { field: 'age', ...avgReducer, pivotColumn: { // will have the same configuration as the `preferredLanguage` column inheritFromColumn: 'preferredLanguage', // but specify a custom default width defaultWidth: 500, }, }, }; ``` **Example: Pivot columns inherit from original columns bound to the same field** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropAggregationReducers, InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const columnAggregations: DataSourcePropAggregationReducers = { avgSalary: { field: 'salary', name: 'Average salary', ...avgReducer, }, avgAge: { field: 'age', ...avgReducer, pivotColumn: { defaultWidth: 500, inheritFromColumn: 'firstName', }, }, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName', style: { fontWeight: 'bold', }, renderValue: ({ value }) => <>{value}!, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', header: 'Salary', style: { color: 'red' }, }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function PivotByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={columnAggregations} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivot-column-inherit-example" columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={180} /> ); }} ); } ``` --- # Pivoting Canonical page: https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview An enteprise-level feature `InfiniteTable` provides is the pivoting functionality. Combined with grouping and advanced aggregation, it unlocks new ways to visualize data. Pivoting is first defined at the `DataSource` level, via the [`pivotBy`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotBy) prop. It's an array of objects, each with a `field` property bound (so `pivotBy[].field` is keyof `DATA_TYPE`) to the `DataSource`. Pivoting generates columns based on the pivoting values, so you have to pass those generated columns into the `` component. You do that by using a `function` as a direct child of the `DataSource`, and in that function you have access to the generated `pivotColumns` array. Likewise for `pivotColumnGroups`. For more pivoting examples, see [our pivoting demos](https://infinite-table.com/docs/learn/examples/dynamic-pivoting-example.md) ```ts const pivotBy = [{ field: 'team' }] // field needs to be keyof DATA_TYPE both in `pivotBy` and `groupBy` const groupBy = [{field: 'department'}, {field: 'country'}] pivotBy={pivotBy} groupBy={groupBy}> { ({pivotColumns, pivotColumnGroups}) => { return pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} /> } } ``` **Example: Pivoting with avg aggregation** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourcePropAggregationReducers, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data) .then( (data) => new Promise((resolve) => { setTimeout(() => resolve(data), 1000); }), ); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, field: 'salary', reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const reducers: DataSourcePropAggregationReducers = { salary: avgReducer, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function GroupByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={reducers} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivoting-example" columns={columns} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={200} pivotTotalColumnPosition="end" /> ); }} ); } ``` ## Customizing Pivot Columns There are a number of ways to customize the pivot columns and [pivot column groups](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy.columnGroup). This is something you generally want to do, as they are generated and you might need to tweak column headers, size, etc. The default behavior for pivot columns generated for aggregations is that they inherit the properties of the original columns bound to the same field as the aggregation. ```ts const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => { return Math.floor(arr.length ? sum / arr.length : 0); }, }; const aggregationReducers: DataSourceProps['aggregationReducers'] = { // will have the same configuration as the `salary` column avgSalary: { field: 'salary', ...avgReducer }, avgAge: { field: 'age', ...avgReducer, pivotColumn: { // will have the same configuration as the `preferredLanguage` column inheritFromColumn: 'preferredLanguage', // but specify a custom default width defaultWidth: 500, }, }, }; ``` **Example: Pivot columns inherit from original columns bound to the same field** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropAggregationReducers, InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const columnAggregations: DataSourcePropAggregationReducers = { avgSalary: { field: 'salary', name: 'Average salary', ...avgReducer, }, avgAge: { field: 'age', ...avgReducer, pivotColumn: { defaultWidth: 500, inheritFromColumn: 'firstName', }, }, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName', style: { fontWeight: 'bold', }, renderValue: ({ value }) => <>{value}!, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', header: 'Salary', style: { color: 'red' }, }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function PivotByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={columnAggregations} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivot-column-inherit-example" columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={180} /> ); }} ); } ``` Another way to do it is to specify [`pivotBy.column`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy.column), as either an object, or (more importantly) as a function. If you pass an object, it will be applied to all pivot columns in the column group generated for the `field` property. ```tsx const pivotBy: DataSourcePivotBy[] = [ { field: 'country' }, { field: 'canDesign', column: { defaultWidth: 400 } }, ]; ; ``` In the above example, the `column.defaultWidth=400` will be applied to columns generated for all `canDesign` values corresponding to each country. This is good but not good enough as you might want to customize the pivot column for every value in the pivot. You can do that by passing a function to the `pivotBy.column` property. ```tsx const pivotBy: DataSourcePivotBy[] = [ { field: 'country' }, { field: 'canDesign', column: ({ column }) => { return { header: column.pivotGroupKey === 'yes' ? 'Designer' : 'Not a Designer', }; }, }, ]; ``` **Example: Pivoting with customized pivot column** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropAggregationReducers, InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, field: 'salary', reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const columnAggregations: DataSourcePropAggregationReducers = { salary: avgReducer, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function PivotByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', column: ({ column: pivotCol }) => { const lastKey = pivotCol.pivotGroupKeys[pivotCol.pivotGroupKeys.length - 1]; return { header: lastKey === 'yes' ? '💅 Designer' : '💻 Non-designer', }; }, }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={columnAggregations} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivoting-customize-column-example" columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={180} /> ); }} ); } ``` ## Total and grand-total columns In [pivot mode](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) you can configure both [total columns](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotTotalColumnPosition) and [grand-total columns](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotGrandTotalColumnPosition). By default, grand-total columns are not displayed, so you have to explicitly set the [`pivotGrandTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotGrandTotalColumnPosition) prop for them to be visible. Pivot total columns are only displayed when [pivotBy](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) has two or more fields (`pivotBy.length > 1`). With a single pivot field only grand-total columns can be shown, and naving [`pivotTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotTotalColumnPosition) has no effect — the totals would be the same as the already displayed values. The example below pivots by `stack` and `canDesign` so both total and grand-total columns are visible. **Example: Pivoting with customized position for totals and grand-total columns** ```ts import { InfiniteTable, DataSource, DataSourceGroupBy, DataSourcePivotBy, InfiniteTableColumnAggregator, DataSourcePropAggregationReducers, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, }; const defaultGroupBy: DataSourceGroupBy[] = [ { field: 'country', }, { field: 'city', }, ]; const defaultPivotBy: DataSourcePivotBy[] = [ { field: 'stack', }, { field: 'canDesign', columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer', }; }, }, ]; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => Math.round(arr.length ? sum / arr.length : 0), }; const aggregations: DataSourcePropAggregationReducers = { salary: { ...avgReducer, name: 'Salary (avg)', field: 'salary', }, age: { ...avgReducer, name: 'Age (avg)', field: 'age', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" defaultGroupBy={defaultGroupBy} defaultPivotBy={defaultPivotBy} aggregationReducers={aggregations} data={dataSource} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivot-grand-total-column-position-example" groupRenderStrategy="single-column" columns={columns} columnDefaultWidth={200} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} pivotTotalColumnPosition="end" pivotGrandTotalColumnPosition="start" /> ); }} ); } ``` **What are grand-total columns?** For each [aggregation reducer](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) specified in the `DataSource`, you can have a total column - this is what [grand-total columns](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotGrandTotalColumnPosition) basically are. ## Server-side pivoting By default, pivoting is client side. However, if you specify [DataSource.lazyLoad](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad) and provide a function that returns a promise for the [DataSource.data](https://infinite-table.com/docs/reference/datasource-props/index.md#data) prop, the table will use server-pivoted data. The [DataSource.data](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function is expected to return a promise that resolves to an object with the following shape: - `totalCount` - the total number of records in the group we're pivoting on - `data` - an array of objects that describes child groups, each object has the following properties: - `keys` - an array of the group keys (usually strings) that uniquely identifies the group, from the root to the current group - `data` - an object that describes the common properties of the group - `aggregations` - an object that describes the aggregations for the current group - `pivot` - the pivoted values and aggregations for each value. This object will have the following properties: - `totals` - an object with a key for each aggregation. The value is the aggregated value for the respective aggregation reducer. - `values` - an object keyed with the unique values for the pivot field. The values of those keys are objects with the same shape as the `pivot` top-level object, namely `totals` and `values`. In the example below, let's assume the following practical scenario, with the data-type being a `Developer{country, stack, preferredLanguage, canDesign, age, salary}`. ```tsx const groupBy = [ { field: 'country' }, // possible values: any valid country { field: 'stack' }, // possible values: "backend", "frontend", "full-stack" ]; const pivotBy = [ { field: 'preferredLanguage' }, // possible values: "TypeScript","JavaScript","Go" { field: 'canDesign' }, // possible values: "yes" or "no" ]; const aggregationReducers = { salary: { name: 'Salary (avg)', field: 'salary', reducer: 'avg' }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg' }, }; ``` ```tsx const dataSource = ({ groupBy, pivotBy, groupKeys, aggregationReducers }) => { // make sure you return a Promise that resolves to the correct structure - see details below //eg: groupBy: [{ field: 'country' }, { field: 'stack' }], // groupKeys: [], - so we're requesting top-level data //eg: groupBy: [{ field: 'country' }, { field: 'stack' }], // groupKeys: ["Canada"], - so we're requesting Canada's data //eg: groupBy: [{ field: 'country' }, { field: 'stack' }], // groupKeys: ["Canada"], - so we're requesting Canada's data } ``` ```js { data: [ { aggregations: { // for each aggregation id, have an entry salary: , age: , }, data: { // data is an object with the common group values country: "Canada" }, // the array of keys that uniquely identify this group, including all parent keys keys: ["Canada"], pivot: { totals: { // for each aggregation id, have an entry salary: , age: , }, values: { [for each unique value]: { // eg: for country totals: { // for each aggregation, have an entry salary: , age: , }, values: { [for each unique value]: { // eg: for stack totals: { salary: , age: , } } } } } } } ], // the total number of rows in the remote data set totalCount: 10, // you can map "values" and "totals" above to shorter names mappings: { values: "values", totals: "totals" } } ``` **Example: Server-side pivoting example** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns, InfiniteTablePropColumnPinning, DataSourceData, DataSourcePropAggregationReducers, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const DATA_SOURCE_SIZE = '10k'; const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, groupKeys = [], }) => { const args = [ pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers${DATA_SOURCE_SIZE}-sql?` + args, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { // the aggregation name will be used as the column header name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; // make the row labels column (id: 'labels') be pinned const defaultColumnPinning: InfiniteTablePropColumnPinning = { // make the generated group columns pinned to start 'group-by-country': 'start', 'group-by-stack': 'start', }; // make all rows collapsed by default const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function RemotePivotExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', column: { // give the group column for the country prop a custom id id: 'group-by-country', }, }, { field: 'stack', column: { // give the group column for the stack prop a custom id id: 'group-by-stack', }, }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'preferredLanguage' }, { field: 'canDesign', // customize the column group columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: `${ columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer' }`, }; }, // customize columns generated under this column group column: ({ column }) => ({ ...column, header: `🎉 ${column.header}`, }), }, ], [], ); return ( primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={aggregationReducers} defaultGroupRowsState={groupRowsState} lazyLoad={true} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="remote-pivoting-example" defaultColumnPinning={defaultColumnPinning} columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={220} /> ); }} ); } ``` The [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) prop is applicable even to pivoted tables, but `groupRenderStrategy="inline"` is not supported in this case. ### Another pivoting example with batching Pivoting builds on the same data response as server-side grouping, but adds the pivot values for each group, as we already showed. Another difference is that in pivoting, no leaf rows are rendered or loaded, since this is pivoting and it only works with aggregated data. This means the `DataSource.data` function must always return the same format for the response data. Just like server-side grouping, server-side pivoting also supports batching - make sure you specify [lazyLoad.batchSize](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad). The example below also shows you how to customize the table rows while records are still loading. **Example: Server side pivoting with lazy loding batching** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourceData, InfiniteTablePropColumns, DataSourceGroupBy, DataSourcePropAggregationReducers, DataSourcePivotBy, InfiniteTableColumn, InfiniteTablePropColumnPinning, InfiniteTablePropGroupColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const numberFormat = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', }); const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); const groupColumn: InfiniteTablePropGroupColumn = { id: 'group-col', // while loading, we can render a custom loading icon renderGroupIcon: ({ renderBag: { groupIcon }, data }) => !data ? '🤷‍' : groupIcon, // while we have no data, we can render a placeholder renderValue: ({ data, value }) => (!data ? ' Loading...' : value), }; const columnPinning: InfiniteTablePropColumnPinning = { 'group-col': 'start', }; const pivotColumnWithFormatter = ({ column, }: { column: InfiniteTableColumn; }) => { return { ...column, renderValue: ({ value }: { value: any }) => value ? numberFormat.format(value as number) : 0, }; }; export default function RemotePivotExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', }, { field: 'city' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'preferredLanguage', // for totals columns column: pivotColumnWithFormatter, }, { field: 'canDesign', columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: columnGroup.pivotGroupKey === 'yes' ? 'Designer 💅' : 'Non-Designer 💻', }; }, column: pivotColumnWithFormatter, }, ], [], ); const lazyLoad = React.useMemo(() => ({ batchSize: 10 }), []); return ( primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy.length ? pivotBy : undefined} aggregationReducers={aggregationReducers} defaultGroupRowsState={groupRowsState} lazyLoad={lazyLoad} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="server-side-pivoting-with-lazy-load-batching-example" scrollStopDelay={10} columnPinning={columnPinning} columns={columns} groupColumn={groupColumn} groupRenderStrategy="single-column" columnDefaultWidth={220} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} /> ); }} ); } const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, lazyLoadStartIndex, lazyLoadBatchSize, groupKeys = [], sortInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const startLimit: string[] = []; if (lazyLoadBatchSize && lazyLoadBatchSize > 0) { const start = lazyLoadStartIndex || 0; startLimit.push(`start=${start}`); startLimit.push(`limit=${lazyLoadBatchSize}`); } const args = [ ...startLimit, pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers30k-sql?` + args, ).then((r) => r.json()); }; ``` Here's another example, that assumes grouping by `country` and `city`, aggregations by `age` and `salary` (average values) and pivot by `preferredLanguage` and `canDesign` (a boolean property): ```tsx //request: groupKeys: [] // empty keys array, so it's a top-level group groupBy: [{"field":"country"},{"field":"city"}] reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}] lazyLoadStartIndex: 0 lazyLoadBatchSize: 10 pivotBy: [{"field":"preferredLanguage"},{"field":"canDesign"}] //response { cache: true, totalCount: 20, data: [ { data: {country: "Argentina"}, aggregations: {avgSalary: 20000, avgAge: 30}, keys: ["Argentina"], pivot: { totals: {avgSalary: 20000, avgAge: 30}, values: { Csharp: { totals: {avgSalary: 19000, avgAge: 29}, values: { no: {totals: {salary: 188897, age: 34}}, yes: {totals: {salary: 196000, age: 36}} } }, Go: { totals: {salary: 164509, age: 36}, values: { no: {totals: {salary: 189202, age: 37}}, yes: {totals: {salary: 143977, age: 35}} } }, Java: { totals: {salary: 124809, age: 32}, values: { no: {totals: {salary: 129202, age: 47}}, yes: {totals: {salary: 233977, age: 25}} } }, //... } } }, //... ] } ``` If we were to scroll down, the next batch of data would have the same structure as the previous one, but with `lazyLoadStartIndex` set to 10 (if `lazyLoad.batchSize = 10`). Now let's expand the first group and see how the request/response would look like: ```tsx //request: groupKeys: ["Argentina"] groupBy: [{"field":"country"},{"field":"city"}] reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}] lazyLoadStartIndex: 0 lazyLoadBatchSize: 10 pivotBy: [{"field":"preferredLanguage"},{"field":"canDesign"}] //response { mappings: { totals: "totals", values: "values" }, cache: true, totalCount: 20, data: [ { data: {country: "Argentina", city: "Buenos Aires"}, aggregations: {avgSalary: 20000, avgAge: 30}, keys: ["Argentina", "Buenos Aires"], pivot: { totals: {avgSalary: 20000, avgAge: 30}, values: { Csharp: { totals: {avgSalary: 39000, avgAge: 29}, values: { no: {totals: {salary: 208897, age: 34}}, yes: {totals: {salary: 296000, age: 36}} } }, Go: { totals: {salary: 164509, age: 36}, values: { no: {totals: {salary: 189202, age: 37}}, yes: {totals: {salary: 143977, age: 35}} } }, Java: { totals: {salary: 124809, age: 32}, values: { no: {totals: {salary: 129202, age: 47}}, yes: {totals: {salary: 233977, age: 25}} } }, //... } } }, //... ] } ``` The response can contain a `mappings` key with values for `totals` and `values` keys - this can be useful for making the server-side pivot response lighter. If `mappings` would be `{totals: "t", values: "v"}`, the response would look like this: ```tsx { totalCount: 20, data: {...}, pivot: { t: {avgSalary: 10000, avgAge: 30}, v: { Go: { t: {...}, v: {...} }, Java: { t: {...}, v: {...} } } } ``` More-over, you can also give aggregationReducers shorter keys to make the server response even more compact ```tsx const aggregationReducers: DataSourcePropAggregationReducers = { s: { name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, a: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; // pivot response { totalCount: 20, data: {...}, pivot: { t: {s: 10000, a: 30}, v: { Go: { t: { s: 10000, a: 30 }, v: {...} }, Java: { t: {...}, v: {...} } } } ``` Adding a `cache: true` key to the resolved object in the `DataSource.data` call will cache the value for the expanded group, so that when collaped and expanded again, the cached value will be used, and no new call is made to the `DataSource.data` function. This is applicable for both pivoted and/or grouped data. Not passing `cache: true` will make the function call each time the group is expanded. --- # Keyboard Shorcuts > Infinite React DataGrid supports user-friendly keyboard shortcuts for executing custom actions. Canonical page: https://infinite-table.com/docs/learn/keyboard-navigation/keyboard-shortcuts The React DataGrid supports defining [keyboard shorcuts](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardShortcuts) for performing custom actions. A keyboard shortcut is defined as an object of the following shape: ```ts { key: string; when?: (context) => boolean | Promise; handler: (context, event) => void | Promise; } ``` The `key` definition is what you're used to from VS Code and other applications - it can be * a single character: `t`, `x`, etc... * a combination of characters (e.g. `Ctrl+Shift+p`,`Cmd+Shift+Enter`) - key modifiers are supported, and can be added with the `+` (plus) sign. * or a special key (e.g. `Enter`, `ArrowUp`, `ArrowDown`, ` ` (space), `Escape`, `Delete`, `Insert`, `PageDown`,`PageUp`,`F1`, `F2`, etc). Examples of valid shortcuts: `Cmd+Shift+e`, `Alt+Shift+Enter`, `Shift+PageDown`, `Ctrl+x` There's a special key `*` that matches any key. This can be useful when you want to define a keyboard shortcut that should be triggered on any key press. Another important key is the `Cmd|Ctrl` key, which matches both the `Cmd` key on Mac and the `Ctrl` key on Windows/Linux. Example combinations: `Cmd|Ctrl+Shift+Enter`, `Cmd|Ctrl+e`, `Cmd|Ctrl+Shift+i`. **Example** Click on a cell and use the keyboard to navigate. Press `Shift+Enter` to show an alert with the current active cell position. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage', header: 'Language' }, country: { field: 'country', header: 'Country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardShortcuts() { return ( <> primaryKey="id" data={dataSource}> debugId="keyboard-shortcuts-initial-example" columns={columns} keyboardShortcuts={[ { key: 'Shift+Enter', when: (context) => !!context.getState().activeCellIndex, handler: (context) => { const { activeCellIndex } = context.getState(); const [rowIndex, columnIndex] = activeCellIndex!; alert( `Current active cell: row ${rowIndex}, column ${columnIndex}.`, ); }, }, { key: 'PageUp', handler: () => { console.log('PageUp key pressed.'); }, }, { key: 'PageDown', handler: () => { console.log('PageDown key pressed.'); }, }, ]} /> ); } ``` Keyboard shortcuts have a `when` optional property. If defined, it restricts when the `handler` function is called. The handler will only be called when the handler returns `true`. ## Implementing Keyboard Shortcut Handlers Both the `handler` function and the `when` function of a keyboard shorcut are called with an object that gives access to the following: - `api` - a reference to the [Infinite Table API](https://infinite-table.com/docs/reference/api/index.md) object. - `dataSourceApi` - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) object. - `getState` - a function that returns the current state of the grid. - `getDataSourceState` - a function that returns the current state of the data source. The second parameter of the `handler` function is the `event` object that triggered the keyboard shortcut. ## Predefined Keyboard Shortcuts Infinite Table DataGrid comes with some predefined keyboard shorcuts. you can import from the `keyboardShortcuts` named export. ```ts import { keyboardShortcuts } from '@infinite-table/infinite-react' ``` ### Instant Edit ```ts {4,12} import { DataSource, InfiniteTable, keyboardShortcuts } from '@infinite-table/infinite-react'; function App() { return primaryKey="id" data={dataSource}> columns={columns} keyboardShortcuts={[ keyboardShortcuts.instantEdit ]} /> } ``` For now, the only predefined keyboard shorcut is `keyboardShortcuts.instantEdit`. This keyboard shorcut starts cell editing when any key is pressed on the active cell. This is the same behavior found in Excel/Google Sheets. **Example** Click on a cell and then start typing to edit the cell. ```ts import { InfiniteTable, DataSource, DataSourceData, keyboardShortcuts, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage', header: 'Language' }, country: { field: 'country', header: 'Country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id', defaultEditable: false }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardShortcuts() { return ( <> primaryKey="id" data={dataSource}> debugId="keyboard-shortcuts-instant-edit-example" columns={columns} columnDefaultEditable keyboardShortcuts={[keyboardShortcuts.instantEdit]} /> ); } ``` --- # Keyboard Navigation for Table Cells > Documentation for Cell Keyboard Navigation for your React Infinite Table DataGrid component Canonical page: https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells By default, [keyboard navigation](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) for table cells is enabled in React Infinite Table. When a cell is clicked, it shows a highlight that indicates it is the currently active cell. From that point onwards, the user can use the keyboard to navigate the table cells. **Example** Click on a cell in the table and use the arrow keys to navigate around. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForCells() { return ( <> primaryKey="id" data={dataSource}> debugId="navigating-cells-initial-example" // keyboardNavigation="cell" is the default, so no need to specify it columns={columns} /> ); } ``` - Use `ArrowUp` and `ArrowDown` to navigate to the previous and next cells vertically. - Use `ArrowLeft` and `ArrowRight` to navigate to the previous and next cells horizontally. --- - Use `PageUp` and `PageDown` to navigate the cells vertically by pages (a page is considered equal to the visible row count). - Use `Shift+PageUp` and `Shift+PageDown` to navigate the cells horizontally by pages (a page is considered equal to the visible column count). --- - Use `Home` and `End` to navigate vertically to the cell above (that's on the first row) and the cell below (that's on the last row), - Use `Shift+Home` and `Shift+End` to navigate horizontally to the first and respectively last cell in the current row. [Watch video](https://www.youtube.com/watch?v=D4_jFYkfsUI) Keyboard navigation is controlled by the [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) prop, which can be either `"cell"`, `"row"` or `false`. Navigating table cells is the default behavior. ## Using a default active cell You can also specify an initial active cell, by using [defaultActiveCellIndex=[2,4]](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultActiveCellIndex). This tells the table that there should be a default active cell, namely the one at index 2,4 (row 2, so third row; column 4, so fifth column). The active cell should be an array of length 2, where the first number is the index of the row and the second number is the index of the column (both are zero-based). **Example** This example starts with cell `[2,0]` already active. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForCells() { return ( <> primaryKey="id" data={dataSource}> debugId="navigating-cells-uncontrolled-example" defaultActiveCellIndex={[2, 0]} columns={columns} /> ); } ``` ## Listening to active cell changes You can easily listen to changes in the cell navigation by using the [onActiveCellIndexChange](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveCellIndexChange) callback. When you use controlled [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex), make sure to use [onActiveCellIndexChange](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveCellIndexChange) to update the prop value, as otherwise the component will not update on navigation **Example** This example starts with cell `[2,0]` already active and uses [onActiveCellIndexChange](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveCellIndexChange) to update [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex). ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForCells() { const [activeCellIndex, setActiveCellIndex] = React.useState< [number, number] >([2, 0]); return ( <>
Current active cell: {activeCellIndex[0]}, {activeCellIndex[1]}.
primaryKey="id" data={dataSource}> debugId="navigating-cells-controlled-example" activeCellIndex={activeCellIndex} onActiveCellIndexChange={setActiveCellIndex} columns={columns} /> ); } ``` ## Toggling group rows When the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy), you can use the keyboard to collapse/expand group rows, by pressing the `Enter` key on the active row. Your active cell doesn't need to be in the group column in order for `Enter` key to collapse/expand the group row - being on a group row is enough. **Example** Press the `Enter` key on the active group row to toggle it. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourceProps, InfiniteTableProps, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', }, canDesign: { field: 'canDesign', }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { return ( data={dataSource} groupBy={defaultGroupBy} primaryKey="id" > debugId="keyboard-toggle-group-rows-cell-nav" columns={columns} domProps={domProps} keyboardNavigation="cell" hideColumnWhenGrouped groupColumn={groupColumn} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ## Selecting Rows with the Keyboard When [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) is enabled (read more about it in the [row selection page](../selection/row-selection)), you can use the spacebar key to select a group row (or `shift` + spacebar to do multiple selection). By default [`keyboardSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardSelection) is enabled, so you can use the **spacebar** key to select multiple rows, when [selectionMode="multi-row"](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode). Using the spacebar key is equivalent to doing a mouse click, so expect the combination of **spacebar** + `cmd`/`ctrl`/`shift` modifier keys to behave just like clicking + the same modifier keys. **Example: Multi row selection with keyboard support** Use spacebar + optional `cmd`/`ctrl`/`shift` modifier keys just like you would do clicking + the same modifier keys. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { const [keyboardSelection, setKeyboardSelection] = useState(true); return ( <>
Keyboard selection is now{' '} {keyboardSelection ? 'enabled' : 'disabled'}.
data={dataSource} selectionMode="multi-row" primaryKey="id" > debugId="default-selection-mode-multi-row-keyboard-toggle-example" keyboardSelection={keyboardSelection} columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` For selection all the rows in the table, you can use `cmd`/`ctrl` + `A` keyboard shortcut. Keyboard selection is also possible when there's a column configured with checkbox selection - [make sure you read more about it](../selection/row-selection#using-a-selection-checkbox). ## Theming There are a number of ways to customize the appearance of the element that highlights the active cell. The easiest is to override those three CSS variables: - `--infinite-active-cell-border-color--r` - the `red` component of the border color - `--infinite-active-cell-border-color--g` - the `green` component of the border color - `--infinite-active-cell-border-color--b` - the `blue` component of the border color The initial values for those are `77`, `149` and`215` respectively, so the border color is `rgb(77, 149, 215)`. In addition, the background color of the active cell highlight element is set to the same color as the border color (computed based on the above `r`, `g` and `b` variables), but with an opacity of `0.25`, configured via the `--infinite-active-cell-background-alpha` CSS variable. When the table is not focused, the opacity for the background color is set to `0.1`, which is the default value of the `--infinite-active-cell-background-alpha--table-unfocused` CSS variable. To summarize, use - `--infinite-active-cell-border-color--r` - `--infinite-active-cell-border-color--g` - `--infinite-active-cell-border-color--b` to control border and background color of the active cell highlight element. There are other CSS variables as well, that give you fined-tuned control over both the border and background color for the active cell, if you don't want to use the above three variables to propagate the same color across both border and background. - `--infinite-active-cell-background` - the background color. If you use this, you need to set opacity yourself. - `--infinite-active-cell-border` - border configuration (eg:`2px solid magenta`). If you use this, it will not be propagated to the background color. **Example: Theming active cell highlight** Use the color picker to configured the desired color for the active cell highlight ```ts import * as React from 'react'; import { useState, useMemo, HTMLProps, ChangeEvent } from 'react'; import { InfiniteTable, DataSource, DataSourceData, debounce, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const rgb = { r: 77, g: 149, b: 215, }; const defaultColor = `#${rgb.r.toString(16)}${rgb.g.toString( 16, )}${rgb.b.toString(16)}`; export default function KeyboardNavigationForCells() { const [color, setColor] = useState({ ...rgb, }); const domProps = useMemo(() => { return { style: { '--infinite-active-cell-border-color--r': color.r, '--infinite-active-cell-border-color--g': color.g, '--infinite-active-cell-border-color--b': color.b, // for the same of the example being more obvious, // make the opacity of the unfocused table same as the one used on focus '--infinite-active-cell-background-alpha--table-unfocused': '0.25', // but this defaults to 0.1 }, } as HTMLProps; }, [color]); const onChange = useMemo(() => { const onColorChange = (event: ChangeEvent) => { const color = event.target.value; const r = parseInt(color.substr(1, 2), 16); const g = parseInt(color.substr(3, 2), 16); const b = parseInt(color.substr(5, 2), 16); setColor({ r, g, b, }); }; return debounce(onColorChange, { wait: 200 }); }, []); return ( <>
Select color{' '}
primaryKey="id" data={dataSource}> debugId="navigating-cells-theming-example" defaultActiveCellIndex={[5, 0]} domProps={domProps} columns={columns} /> ); } ``` --- # Keyboard Navigation for Table Rows Canonical page: https://infinite-table.com/docs/learn/keyboard-navigation/navigating-rows To enable keyboard navigation for table rows, specify [keyboardNavigation="row"](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) in your React Infinite Table component. When row navigation is enabled, clicking a row highlights it and the user can use the arrow keys to navigate the table rows. **Example** Click on the table and use the arrow keys to navigate the rows. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForCells() { return ( <> primaryKey="id" data={dataSource}> debugId="navigating-rows-initial-example" keyboardNavigation="row" columns={columns} /> ); } ``` - Use `ArrowUp` and `ArrowDown` to navigate to the previous and next row. - Use `PageUp` and `PageDown` to navigate the rows vertically by pages (a page is considered equal to the visible row count). - Use `Home` and `End` to navigate vertically to the first and last row respectively Other possible values for the [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) prop, besides `"row"`, are `"cell"` and `false`. ## Using a default active row You can also specify an initial active row, by using [defaultActiveRowIndex=2](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultActiveRowIndex). This tells the table that there should be a default active row, namely the one at index 2 (so the third row). **Example** This example starts with row at index `2` already active. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '90vh' } }; export default function KeyboardNavigationForRows() { return ( <> primaryKey="id" data={dataSource}> debugId="navigating-rows-uncontrolled-example" domProps={domProps} columns={columns} keyboardNavigation="row" defaultActiveRowIndex={2} /> ); } ``` ## Listening to active row changes You can easily listen to changes in the row navigation by using the [`onActiveRowIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) callback. When you use controlled [`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex), make sure to use [onActiveRowIndexChange](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) to update the prop value, as otherwise the component will not update on navigation **Example** This example starts with row at index `2` already active and uses [onActiveRowIndexChange](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) to update [`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex). ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForRows() { const [activeRowIndex, setActiveRowIndex] = React.useState(2); return ( <>
Current active row: {activeRowIndex}.
primaryKey="id" data={dataSource}> debugId="navigating-rows-controlled-example" keyboardNavigation="row" activeRowIndex={activeRowIndex} onActiveRowIndexChange={setActiveRowIndex} columns={columns} /> ); } ``` ## Toggling group rows When the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy), you can use the keyboard to collapse/expand group rows, by pressing the `Enter` key on the active row. Since you're in row navigation mode, you can also use - `←` to collapse a group row - `→` to expand a group row **Example** Press the `Enter` key on the active group row to toggle it. `ArrowLeft` will collapse a group row and `ArrowRight` will expand a group row. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourceProps, InfiniteTableProps, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', }, canDesign: { field: 'canDesign', }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { return ( data={dataSource} groupBy={defaultGroupBy} primaryKey="id" > debugId="keyboard-toggle-group-rows" columns={columns} domProps={domProps} keyboardNavigation="row" hideColumnWhenGrouped groupColumn={groupColumn} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ## Selecting Rows with the Keyboard When [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) is enabled (read more about it in the [row selection page](../selection/row-selection)), you can use the spacebar key to select a group row (or `shift` + spacebar to do multiple selection). By default [`keyboardSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardSelection) is enabled, so you can use the **spacebar** key to select multiple rows, when [selectionMode="multi-row"](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode). Using the spacebar key is equivalent to doing a mouse click, so expect the combination of **spacebar** + `cmd`/`ctrl`/`shift` modifier keys to behave just like clicking + the same modifier keys. **Example: Multi row selection with keyboard support** Use spacebar + optional `cmd`/`ctrl`/`shift` modifier keys just like you would do clicking + the same modifier keys. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { const [keyboardSelection, setKeyboardSelection] = useState(true); return ( <>
Keyboard selection is now{' '} {keyboardSelection ? 'enabled' : 'disabled'}.
data={dataSource} selectionMode="multi-row" primaryKey="id" > debugId="default-selection-mode-multi-row-keyboard-toggle-example-row-navigation" keyboardNavigation="row" keyboardSelection={keyboardSelection} columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` For selection all the rows in the table, you can use `cmd`/`ctrl` + `A` keyboard shortcut. Keyboard selection is also possible when there's a column configured with checkbox selection - [make sure you read more about it](../selection/row-selection#using-a-selection-checkbox). ## Theming By default, the style of the element that highlights the active row is the same style as that of the element that highlights the active cell. The easiest is to override the style is via those three CSS variables: - `--infinite-active-cell-border-color--r` - the `red` component of the border color - `--infinite-active-cell-border-color--g` - the `green` component of the border color - `--infinite-active-cell-border-color--b` - the `blue` component of the border color The initial values for those are `77`, `149` and`215` respectively, so the border color is `rgb(77, 149, 215)`. In addition, the background color of the element that highlights the active row is set to the same color as the border color (computed based on the above `r`, `g` and `b` variables), but with an opacity of `0.25`, configured via the `--infinite-active-row-background-alpha` CSS variable. When the table is not focused, the opacity for the background color is set to `0.1`, which is the default value of the `--infinite-active-row-background-alpha--table-unfocused` CSS variable. To summarize, use - `--infinite-active-cell-border-color--r` - `--infinite-active-cell-border-color--g` - `--infinite-active-cell-border-color--b` to control border and background color of the active row highlight element. No, it's not a mistake that the element that highlights the active row is configured via the same CSS variables as the element that highlights the active cell. This is deliberate - so override CSS variables for cell, and those are propagated to the row highlight element. There are other CSS variables as well, that give you fined-tuned control over both the border and background color for the active row, if you don't want to use the above three variables to propagate the same color across both border and background. - `--infinite-active-cell-background` - the background color. If you use this, you need to set opacity yourself. Applied for both cell and row. - `--infinite-active-row-background` - the background color. If you use this, you need to set opacity yourself. If this is specified, it takes precendence over `--infinite-active-cell-background` - `--infinite-active-cell-background` - the background color. If you use this, you need to set opacity yourself. Applied for both cell and row. - `--infinite-active-row-background` - the background color. If this is specified, it takes precedence over `--infinite-active-cell-background` - `--infinite-active-row-border` - border configuration (eg:`2px solid magenta`). If you use this, it will not be propagated to the background color. For more details on the CSS variables, see the [CSS Variables documentation](../theming/css-variables##active-row-background). **Example: Theming active row highlight** Use the color picker to configured the desired color for the active row highlight ```ts import { InfiniteTable, DataSource, DataSourceData, debounce, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; import { useMemo } from 'react'; import { HTMLProps } from 'react'; import { ChangeEvent } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const rgb = { r: 77, g: 149, b: 215, }; const defaultColor = `#${rgb.r.toString(16)}${rgb.g.toString( 16, )}${rgb.b.toString(16)}`; export default function KeyboardNavigationForRows() { const [color, setColor] = useState({ ...rgb, }); const domProps = useMemo(() => { return { style: { '--infinite-active-cell-border-color--r': color.r, '--infinite-active-cell-border-color--g': color.g, '--infinite-active-cell-border-color--b': color.b, // for the same of the example being more obvious, // make the opacity of the unfocused table same as the one used on focus '--infinite-active-cell-background-alpha--table-unfocused': '0.25', // but this defaults to 0.1 }, } as HTMLProps; }, [color]); const onChange = useMemo(() => { const onColorChange = (event: ChangeEvent) => { const color = event.target.value; const r = parseInt(color.substr(1, 2), 16); const g = parseInt(color.substr(3, 2), 16); const b = parseInt(color.substr(5, 2), 16); setColor({ r, g, b, }); }; return debounce(onColorChange, { wait: 200 }); }, []); return ( <>
Select color{' '}
primaryKey="id" data={dataSource}> debugId="navigating-rows-theming-example" keyboardNavigation="row" defaultActiveRowIndex={7} domProps={domProps} columns={columns} /> ); } ``` --- # Master Detail - Caching Detail DataGrid > Learn how to use master-detail with caching for a better user-experience Canonical page: https://infinite-table.com/docs/learn/master-detail/caching-detail-datagrid By far the most common scenario will be to render another DataGrid in the detail row. For such cases we offer a caching mechanism that will keep the state of the detail DataGrid when the user collapses and then expands the row again. The most important part of the state of detail DataGrid that will be cached is the data-related. More specifically, when cached, the detail `` will get its data from the cache and will not call the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function when mounted. Other persisted parts of the state are the sorting, filtering and grouping information. To enable caching, use the [`rowDetailCache`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailCache) prop. It can be one of the following: - `false` - caching is disabled - this is the default - `true` - enables caching for all detail DataGrids - `number` - the maximum number of detail DataGrids to keep in the cache. When the limit is reached, the oldest detail DataGrid will be removed from the cache. **Example: Master detail DataGrid with caching for 5 detail DataGrids** This example will cache the last 5 detail DataGrids - meaning they won't reload when you expand them again. You can try collapsing a row and then expanding it again to see the caching in action - it won't reload the data. But when you open up a row that hasn't been opened before, it will load the data from the remote location. ```ts file=master-detail-caching-with-default-expanded-example.page.tsx ``` --- # Master Detail - Collapsing and Expanding Rows > Learn how to use master-detail and configure the state of the row details as expanded or collapsed Canonical page: https://infinite-table.com/docs/learn/master-detail/collapsing-and-expanding-rows You can control the collapsed/expanded state of rows in the master-detail configuration. By default, all row details are collapsed. You can very easily change this by using the [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) prop. ```tsx title="Specyfing the default row detail state" {8} const defaultRowDetailState = { collapsedRows: true, expandedRows: [39, 54], }; ``` **Example: Master detail DataGrid with some row details expanded by default** Some of the rows in the master DataGrid are expanded by default. Also, we have a default sort defined, by the `country` and `city` columns. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-default-expanded-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-default-expanded-example-2" domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ## Understanding and defining the collapse/expand state for row details When you want to specify a different collapse/expand state of the row details (since by default they are all collapsed, and you might want to expand some of them), you need to use the [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) prop, or its controlled counterpart - the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop. The [row detail state](https://infinite-table.com/docs/reference/type-definitions/index.md#RowDetailState) can be defined in two ways: - either specify `collapsedRows: true` (which means all rows are collapsed by default) and specify an array of `expandedRows`, which will contain the ids of the rows that should be rendered as expanded. ```tsx const defaultRowDetailState = { collapsedRows: true, expandedRows: ['id-1', 'id-2', 'id-56'], }; ``` - or specify `expandedRows: true` (which means all rows are expanded by default) and specify an array of `collapsedRows`, which will contain the ids of the rows that should be rendered as collapsed. ```tsx const rowDetailState = { expandedRows: true, collapsedRows: ['id-1', 'id-2', 'id-56'], }; ``` You can pass these objects into either the [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) (uncontrolled) or the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) (controlled). If you're using the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop, you'll need to respond to user interaction by listening to [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange) and updating the value of [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) accordingly. As an alternative to using the object literals as specified above, you can import the `RowDetailState` class from `@infinite-table/infinite-react` and use it to define the state of the row details. You can pass instances of `RowDetailState` into the [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) or [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) props. ```tsx title="Passing an instance of RowDetailState to the InfiniteTable" import { RowDetailState } from '@infinite-table/infinite-react'; const rowDetailState = new RowDetailState({ collapsedRows: true, expandedRows: [2, 3, 4], }); rowDetailState={rowDetailState} />; ``` ```tsx title="Passing an object literal to the InfiniteTable" rowDetailState={{ collapsedRows: true, expandedRows: [2, 3, 4], }} /> ``` See our type definitions for [more details on row detail state](https://infinite-table.com/docs/reference/type-definitions/index.md#RowDetailState). **Example: Master detail DataGrid with listener for row expand/collapse** Some of the rows in the master DataGrid are expanded by default. We use the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop to manage the state of the row details and update it by using [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange). ```ts file=master-detail-controlled-expanded-enhanced-example.page.tsx ``` ## Listening to row detail state changes In order to be notified when the collapse/expand state of row details changes, you can use the [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange) prop. This function is called with only one argument - the new [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState). Please note this is an instance of [`RowDetailState`](https://infinite-table.com/docs/reference/type-definitions/index.md#RowDetailState). If you want to use the object literal, make sure you call `getState()` on the instance of `RowDetailState`. ```tsx title="Using the onRowDetailStateChange listener" {11} function App() { const [rowDetailState, setRowDetailState] = React.useState({ collapsedRows: true as const, expandedRows: [39, 54], }); return {...}> rowDetailState={rowDetailState} onRowDetailStateChange={(rowDetailStateInstance) => { setRowDetailState(rowDetailStateInstance.getState()); }} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> } ``` When using the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState), you'll need to respond to the user interaction by using the [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange) listener, in order to update the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState). This allows you to manage the state of the row details yourself - making it easy to expand/collapse all rows, or to expand/collapse a specific row by simply updating the value of the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop. ```tsx const [rowDetailState, setRowDetailState] = React.useState({ collapsedRows: true, expandedRows: [39, 54], }); const expandAll = () => { setRowDetailState({ collapsedRows: [], expandedRows: true, }); }; const collapseAll = () => { setRowDetailState({ collapsedRows: true, expandedRows: [], }); }; return ( <> rowDetailState={rowDetailState} /> ); ``` If you prefer the more imperative approach, you can still use the [Row Detail API](https://infinite-table.com/docs/reference/row-detail-api/index.md) to [expand](https://infinite-table.com/docs/reference/row-detail-api/index.md#expandRowDetail) or [collapse](https://infinite-table.com/docs/reference/row-detail-api/index.md#collapseRowDetail) details for rows. ## Single row expand Using the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop is very powerful - it allows you to configure the expand state to only allow one row to be expanded at a time, if that's something you need. This means that if any other row(s) are expanded and you expand a new row, the previously expanded rows will all be collapsed. **Example: Master detail only one row expanded at a time** In this demo we allow only one row to be expanded at any given time. We use the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop to manage the state of the row details and update it by using [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange). ```ts file=master-detail-one-expanded-row-example.page.tsx ``` --- # Master Detail with Custom Row Detail Contents > Learn how to use master-detail to customise your row detail contents Canonical page: https://infinite-table.com/docs/learn/master-detail/custom-row-detail-content The Infinite Table React DataGrid allows you to render any valid JSX nodes as row details. You can render a DataGrid directly or you can nest the DataGrid at any level of nesting inside the row details. Or you can simply choose to render anything else - no DataGrid required. ## Rendering a detail DataGrid Your row detail content can include another Infinite Table DataGrid. The DataGrid you're rendering inside the row detail doesn't need to be the return value of the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) function - it can be nested inside other valid JSX nodes you return from the function. **Example: Master detail with custom content & DataGrid** In this example, the row detail contains custom content, along with another Infinite Table DataGrid. You can nest a child DataGrid inside the row details at any level of nesting. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { return (

Developers in {rowInfo.data?.name}, {rowInfo.data?.country}

data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-custom-datagrid-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} />
); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-custom-datagrid-example-2" domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailHeight={320} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` You'll probably want to configure the height of the row detail content. Use the [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) prop to do that. ## Rendering a chart component as row detail **Example: Retrieving cell selection value by mapping over them** ```ts file=master-detail-chart-detail-example.page.tsx" ``` In the above example, please note that on every render (after the detail component is mounted), we pass the same `dataSource`, `groupBy` and `aggregationReducers` props to the `` component. The references for all those objects are stable. We don't want to pass new references on every render, as that would cause the `` to reload and reprocess the data. ## Multiple levels of nesting The master-detail configuration for the DataGrid can contain any level of nesting. The example below shows 3 levels of nesting - so a master DataGrid, a detail DataGrid and another third-level detail with custom content. **Example: Master detail with 3 levels of nesting** In this example, we have 3 levels of nesting: - The master DataGrid shows cities/countries - The first level of detail shows developers in each city - The second level of detail shows custom data about each developer ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', renderRowDetailIcon: true, }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const detailStyle: React.CSSProperties = { padding: 10, color: 'var(--infinite-cell-color)', background: 'var(--infinite-background)', height: '100%', display: 'flex', flexDirection: 'column', }; function renderLastDetail(rowInfo: InfiniteTableRowInfo) { const { data } = rowInfo; if (!data) { return
No data ...
; } return (

Developer: {data.firstName} {data.lastName}

Preferred Language
{data.preferredLanguage}
Salary
{data.salary}
Currency
{data.currency}
Can Design
{data.canDesign}
); } const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { return (

Developers in {rowInfo.data?.name}, {rowInfo.data?.country}

data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-3-levels-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} rowDetailHeight={200} rowDetailRenderer={renderLastDetail} />
); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-3-levels-example-2" domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailHeight={350} rowDetailRenderer={renderDetail} />
); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ## Understanding the lifecycle of the row detail component You have to keep in mind that the content you render in the row detail can be mounted and unmounted multiple times. Whenever the user expands the row detail, it gets mounted and rendered, but then it will be unmounted when the user scrolls the content out of view. This can happen very often. Also note that the content can be recycled - meaning the same component can be reused for different rows. If you don't want recycling to happen, make sure you use a unique key for the row detail content - you can use the `masterRowInfo.id` for that. In practice this means that it's best if your row detail content is using controlled state and avoids using local state. --- # Master Detail > Learn how to use master-detail rendering with the React DataGrid Canonical page: https://infinite-table.com/docs/learn/master-detail/overview The React DataGrid that Infinite Table offers has native support for master-detail rows. The single most important property for the master-detail DataGrid configuration is the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) function prop - which makes the DataGrid be considered master-detail. In addition, make sure you have a column with the `renderRowDetailIcon: true` flag set. [`columns.renderRowDetailIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderRowDetailIcon) on a column makes the column display the row detail expand icon. The row detail in the DataGrid can contain another DataGrid or any other custom content. It's very imporant that the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) function prop you pass into `` is stable and doesn't change on every render. So make sure you pass a reference to the same function every time - except of course if you want the row detail to change based on some other state. **Example: Basic master detail DataGrid example** This example shows a master DataGrid with cities & countries. The details for each city shows a DataGrid with developers in that city. The detail DataGrid is configured with remote sorting. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} />
); } export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-example-2" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` If you want to use a component instead of the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) function, you can use the [`components.RowDetail`](https://infinite-table.com/docs/reference/infinite-table-props.md#components.RowDetail) property. This works similarly and makes the DataGrid be considered master-detail. Inside the component, you can use the [`useMasterRowInfo`](https://infinite-table.com/docs/reference/hooks/index.md#useMasterRowInfo) hook to get the master row information. ## Loading the Detail DataSource When master-detail is configured and the row detail renders a DataGrid, the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function for the detail `` will be called with the `masterRowInfo` as a property available in the object passed as argument. ```tsx title="Loading the detail DataGrid data" {2} const detailDataFn: DataSourceData = ({ masterRowInfo, sortInfo, ... }) => { return Promise.resolve([...]) } data={detailDataFn}> {...} ``` You can see the live example above for more details. ## Rendering a detail DataGrid Using the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) prop, you can render any custom content for the row details. The content doesn't need to include Infinite Table. You can, however, render an Infinite Table React DataGrid, at any level of nesting inside the row detail content. **Example: Master detail with custom content & DataGrid** In this example, the row detail contains custom content, along with another Infinite Table DataGrid. You can nest a child DataGrid inside the row details at any level of nesting. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { return (

Developers in {rowInfo.data?.name}, {rowInfo.data?.country}

data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-custom-datagrid-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} />
); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-custom-datagrid-example-2" domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailHeight={320} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ## Configuring the master-detail height In order to configure the height of the row details, you can use the [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) prop. ```tsx title="Configuring the row detail height" {3} columns={masterColumns} rowDetailHeight={500} rowDetailRenderer={renderDetail} /> ``` The default value for the [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) is `300` px. [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) can be one of the following: - `number` - the height in pixels - `string` - the name of a CSS variable that configures the height - eg: `--master-detail-height` - `(rowInfo) => number` - a function that can return a different height for each row. The sole argument is the [rowInfo object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo). **Example: Master detail DataGrid with custom height for row details** This master-detail DataGrid is configured with a custom [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) of `200px`. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-custom-detail-height-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-custom-detail-height-example-2" domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ## Conditional row details Not all rows in a DataGrid need to have details. To configure which rows have details, you can use the [`isRowDetailEnabled`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailEnabled) function prop. ```tsx title="Using conditional row details" {5} columns={masterColumns} rowDetailHeight={500} rowDetailRenderer={renderDetail} isRowDetailEnabled={(rowInfo) => rowInfo.data.cityName.contains('i')} /> ``` The [`isRowDetailEnabled`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailEnabled) function prop is called with the [rowInfo object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) and is expected to return a boolean value. **Example: Master detail DataGrid with conditional details** This example shows a master DataGrid with cities & countries. Not all rows have details - every other row is configured without details via the [`isRowDetailEnabled`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailEnabled) function prop. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-per-row-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } const isRowDetailEnabled = (rowInfo: InfiniteTableRowInfo) => { return rowInfo.indexInAll % 2 === 0; }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-per-row-example-2" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} isRowDetailEnabled={isRowDetailEnabled} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` --- # Disabled Rows Canonical page: https://infinite-table.com/docs/learn/rows/disabled-rows Disabling rows allows you to have some rows that are not selectable, not clickable, not reacheable via keyboard navigation and other interactions. The `DataSource` manages the disabled state of rows, via the [`defaultRowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowDisabledState) (uncontrolled) prop and [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) (controlled) prop. ```tsx idProperty="id" data={[]} defaultRowDisabledState={{ enabledRows: true, disabledRows: ['id1', 'id4', 'id5'] }} /> {/* ... */} /> ``` In addition to using the [`defaultRowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowDisabledState)/[`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) props, you can also specify the [`isRowDisabled`](https://infinite-table.com/docs/reference/datasource-props/index.md#isRowDisabled) function prop, which overrides those other props and ultimately determines whether a row is disabled or not. **Example: Specify some rows as initially disabled** ```tsx import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { return ( <> data={data} primaryKey="id" defaultRowDisabledState={{ enabledRows: true, disabledRows: [1, 3, 4, 5], }} > debugId="initialRowDisabledState-example" columnDefaultWidth={120} columnMinWidth={50} columns={columns} keyboardNavigation="row" /> ); }; ``` ## Using disabled rows while rendering When rendering a cell, you have access to the row disabled state - the [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) type has a `rowDisabled` property which is true if the row is disabled. **Example: Using the row disabled state while rendering** This example uses custom rendering for the `firstName` column to render an emoji for disabled rows. ```tsx import * as React from 'react'; import { DataSource, DataSourceApi, DataSourceData, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', renderValue: ({ rowInfo, value }) => { return `${value} ${rowInfo.rowDisabled ? '🚫' : ''}`; }, }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { const [dataSourceApi, setDataSourceApi] = React.useState>(); return ( <> onReady={setDataSourceApi} data={data} primaryKey="id" defaultRowDisabledState={{ enabledRows: [1, 2, 3, 5], disabledRows: true, }} > debugId="custom-rendering-for-disabled-rows-example" columnDefaultWidth={120} columnMinWidth={50} columns={columns} keyboardNavigation="row" /> ); }; ``` ## Using the API to enable/disable rows You can use the `DataSourceApi` to enable or disable rows programmatically. [`setRowEnabled`](https://infinite-table.com/docs/reference/datasource-api/index.md#setRowEnabled) ```tsx dataSourceApi.setRowEnabled(rowId, enabled); ``` [`setRowEnabledAt`](https://infinite-table.com/docs/reference/datasource-api/index.md#setRowEnabledAt) ```tsx dataSourceApi.setRowEnabledAt(rowIndex, enabled); ``` **Example: Using the API to enable/disable rows** Use the context menu on each row to toggle the disabled state of the respective row. ```tsx import * as React from 'react'; import { DataSource, DataSourceApi, DataSourceData, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', renderValue: ({ rowInfo, value }) => { return `${value} ${rowInfo.rowDisabled ? '🚫' : ''}`; }, }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { const [dataSourceApi, setDataSourceApi] = React.useState>(); return ( <> onReady={setDataSourceApi} data={data} primaryKey="id" defaultRowDisabledState={{ enabledRows: [1, 2, 3, 5], disabledRows: true, }} > debugId="using-api-to-disable-rows-example" getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { return { columns: [{ name: 'label' }], items: [ { label: 'Disable row', key: 'disable-row', disabled: rowInfo.rowDisabled, onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); hideMenu(); }, }, { label: 'Enable row', key: 'enable-row', disabled: !rowInfo.rowDisabled, onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled(rowInfo.id, true); hideMenu(); }, }, { label: 'Toggle row disable/enable', key: 'toggle-row-disable-enable', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled( rowInfo.id, rowInfo.rowDisabled, ); hideMenu(); }, }, ], }; }} columnDefaultWidth={120} columnMinWidth={50} columns={columns} keyboardNavigation="row" /> ); }; ``` --- # Styling Rows Canonical page: https://infinite-table.com/docs/learn/rows/styling-rows Rows can be styled by using the `rowStyle` and the `rowClassName` props - the [`rowStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowStyle) prop can be a style `object` or a `function` that returns a style `object` or `undefined` - the [`rowClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowClassName) prop can be a `string` (the name of a CSS class) or a `function` that returns a `string` or `undefined` ```tsx title="Defining-a-rowStyle-function" const rowStyle: InfiniteTablePropRowStyle = ({ data, rowInfo, }: { data: Employee | null; rowInfo: InfiniteTableRowInfo; }) => { const salary = data ? data.salary : 0; if (salary > 150_000) { return { background: 'tomato' }; } if (rowInfo.indexInAll % 10 === 0) { return { background: 'lightblue', color: 'black' }; } }; ``` The [`rowClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowClassName) function prop has the same signature as the [`rowStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowStyle) function prop. ## Row styling example **Example** ```ts files=["$DOCS/reference/rowStyle-example.page.tsx","$DOCS/reference/rowStyle-example-columns.ts"] ``` In the [`rowStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowStyle) function, you can access the rowInfo object, which contains information about the current row. It's especially useful when you have grouping and aggregation, as it contains the aggregation values and other extra info. --- # Using Rows at Runtime Canonical page: https://infinite-table.com/docs/learn/rows/using-row-info At runtime, the [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) function and a [lot](https://infinite-table.com/docs/reference/infinite-table-props.md#rowStyle) [of](https://infinite-table.com/docs/reference/infinite-table-props.md#rowClassName) [other](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style) functions use the [rowInfo](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) object to access the current row and use it to decide how to render the current cell or row. The `rowInfo` object has a few variations, depending on the presence or absence of grouping. See [type definition here](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo). All those variations are discriminated in the `TypeScript` typings, so you can easily use the different types of `rowInfo` objects. ## Ungrouped Scenario - normal `rowInfo` When there is no [grouping](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy), the `rowInfo` object has the following properties: - `data` - type: `DATA_TYPE` - `dataSourceHasGrouping` - type: `false` - `isGroupRow` - type: `false` - `id` - type: `any`. The id of the row, as defined by the [`idProperty`](https://infinite-table.com/docs/reference/datasource-props/index.md#idProperty) prop. - `selfLoaded` - type: `boolean`. Useful in lazy-loading scenarios, when there is batching present. If you're not in such a scenario, the value will be `false`. You can use this to show a loading indicator for the row. - `indexInAll` - type `number`. The index of the row in the full dataset. Called like this because for grouping scenarios, there's also an `indexInGroup` ### Discriminator ```ts rowInfo.dataSourceHasGrouping === false; ``` ## Grouped scenario - normal `rowInfo` When there is [grouping](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) defined, and the row is not a group row, the `rowInfo` object has the following properties: - `data` - type: `DATA_TYPE` - `dataSourceHasGrouping` - type: `true` - `isGroupRow` - type: `false` - `indexInAll` - like the above - `indexInGroup` - type: `number`. The index of the row in its parent group. - `groupKeys` - type: `any[]`, but usually it's actually `string[]`. For normal rows, the group keys will have all the keys starting from the topmost parent down to the last group row in the hierarchy (the direct parent of the current row). ```txt Example: People grouped by country and city > Italy - country - groupKeys: ['Italy'] > Rome - city - groupKeys: ['Italy', 'Rome'] - Marco - person - groupKeys: ['Italy', 'Rome'] - Luca - person - groupKeys: ['Italy', 'Rome'] - Giuseppe - person - groupKeys: ['Italy', 'Rome'] ``` - `groupBy` - type `(keyof T)[]`. Has the same structure as groupKeys, but it will contain the fields used to group the rows. - `rootGroupBy` - type `(keyof T)[]`. The groupBy value of the DataSource component, mapped to the `groupBy.field` - `parents` - a list of `rowInfo` objects that are the parents of the current row. - `indexInParentGroups[]` - type: `number[]`. See below for an example ``` > Italy - country - indexInParentGroups: [0] > Rome - city - indexInParentGroups: [0,0] - Marco - person - indexInParentGroups: [0,0,0] - Luca - person - indexInParentGroups: [0,0,1] - Giuseppe - person - indexInParentGroups: [0,0,2] > USA - country - indexInParentGroups: [1] > LA - city - indexInParentGroups: [1,0] - Bob - person - indexInParentGroups: [1,0,2] ``` - `groupCount` - type: `number`. The count of leaf rows that the current group (in this case, the parent group) contains - `groupNesting` - type `number`. The nesting of the parent group. - `collapsed` - type `boolean`. - `selfLoaded` - type: `boolean`. Useful in lazy-loading scenarios, when there is batching present. If you're not in such a scenario, the value will be `false`. ### Discriminator ```ts rowInfo.dataSourceHasGrouping === true && rowInfo.isGroupRow === false; ``` ## Grouped scenario - group `rowInfo` When there is [grouping](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) defined, and the row is a group row, the `rowInfo` object has the following properties: - `data` - type: `Partial | null`. The `data` object that might be available is the result of the [aggregation reducers](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers). If none are specified, `data` will be `null` - `dataSourceHasGrouping` - type: `true` - `isGroupRow` - type: `true` - `error` - type: `string?`. If there was an error while loading the group (when the group row is expanded), this will contain the error message. If the group row was loaded with the `cache: true` flag sent in the server response, the error will remain on the `rowInfo` object even when you collapse the group row, otherwise, if `cache: true` was not present, the `error` property will be removed on collapse. - `indexInAll` - like the above - `indexInGroup` - type: `number`. The index of the row in the its parent group. - `deepRowInfoArray` - an array of `rowInfo` objects. This array contains all the (uncollapsed, so visible) row infos under this group, at any level of nesting, in the order in which they are visible in the table. - `reducerResults` - type `Record`. The result of the [aggregation reducers](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) for each field in the [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) prop. - `groupCount` - type: `number`. The count of leaf rows that the current group (in this case, the parent group) contains - `groupData` - type: `DATA_TYPE[]`. The array of the data of all leaf nodes (normal nodes) that are inside this group. ```txt Example: People grouped by country and city > Italy - country - groupKeys: ['Italy'] > Rome - city - groupKeys: ['Italy', 'Rome'] - Marco - person - groupKeys: ['Italy', 'Rome'] - Luca - person - groupKeys: ['Italy', 'Rome'] - Giuseppe - person - groupKeys: ['Italy', 'Rome'] ``` - `collapsedChildrenCount` - type: `number`. The count of all leaf nodes (normal rows) inside the group that are not being visible due to collapsing (either the current row is collapsed or any of its children) - `directChildrenCount` - type: `number`. The count of the direct children of the current group. Direct children can be either normal rows or groups. - `directChildrenLoadedCount` - type: `number`. Like `directChildrenCount`, but only counts the rows that are loaded (when batched lazy loading is configured). - `childrenAvailable` - type: `boolean`. For lazy/batched grouping, this is true if the group has been expanded at least once. NOTE: if this is true, it doesn't mean that all the children have been loaded, it only means that at least some children have been loaded and are available. Use `directChildrenCount` and `directChildrenLoadedCount` to know if all the children have been loaded or not. - `childrenLoading` - type: `boolean`. Boolean flag that will be true while lazy loading direct children of the current row group. Use `directChildrenLoadedCount` and `directChildrenCount` to know if all the children have been loaded or not. - `childrenSelectedCount` the number of all leaf rows in the current group that are selected. - `groupKeys` - type: `any[]`, but usually it's actually `string[]`. For group rows, the group keys will have all the keys starting from the topmost parent down to the current group row (key for current group row is included). - `groupBy` - type `(keyof T)[]`. Has the same structure as groupKeys, but it will contain the fields used to group the rows. - `rootGroupBy` - type `(keyof T)[]`. The groupBy value of the DataSource component, mapped to the `groupBy.field` - `groupCount` - type: `number`. The count of leaf rows that the current group (in this case, the parent group) contains - `groupNesting` - type `number`. The nesting of the parent group. - `parents` - a list of `rowInfo` objects that are the parents of the current row. - `indexInParentGroups[]` - type: `number[]`. See below for an example ``` > Italy - country - indexInParentGroups: [0] > Rome - city - indexInParentGroups: [0,0] - Marco - person - indexInParentGroups: [0,0,0] - Luca - person - indexInParentGroups: [0,0,1] - Giuseppe - person - indexInParentGroups: [0,0,2] > USA - country - indexInParentGroups: [1] > LA - city - indexInParentGroups: [1,0] - Bob - person - indexInParentGroups: [1,0,2] ``` - `collapsed` - type `boolean`. - `selfLoaded` - type: `boolean`. Useful in lazy-loading scenarios, when there is batching present. If you're not in such a scenario, the value will be `false`. ### Discriminator ```ts rowInfo.dataSourceHasGrouping === true && rowInfo.isGroupRow === true; ``` --- # Cell Selection > InfiniteTable DataGrid component supports single and multiple cell selection. Canonical page: https://infinite-table.com/docs/learn/selection/cell-selection To use multi-cell selection, you need to configure the `` component with `selectionMode="multi-cell"` - see [`selectionMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode) for details. For selecting rows, see the [Row Selection](https://infinite-table.com/docs/learn/selection/row-selection.md) page. ```tsx title="Configuring the selection mode" // can be "single-row", "multi-row", "multi-cell" or false ``` **Example: Multiple cell selection example** Click cells in the grid to add to the selection. Use `Shift+Click` to select a range of cells and `Cmd/Ctrl+Click` to add single cells to the selection. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 60 }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { return ( primaryKey="id" data={dataSource} selectionMode="multi-cell" > debugId="cell-selection-default-example" columns={columns} columnDefaultWidth={100} /> ); } ``` ## Using default selection You can specify a default value for cell selection by using the [`defaultCellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultCellSelection) prop. ```tsx title="Using default selection" const defaultCellSelection = { defaultSelection: false, selectedCells: [ [3, "stack"], // rowId + colId [5, "stack"], // rowId + colId [0, "firstName"], // rowId + colId ] } ``` Cell selection uses `[rowId, colId]` cell descriptors to identify cells to be marked as selected or deselected - read more in the [Cell selection format](#cell-selection-format). **Example: Multiple cell selection with a default selection value** By default some cells are already selected in the grid below, by using the [`defaultCellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultCellSelection) prop on the `` component. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, DataSourcePropCellSelection_MultiCell, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 60 }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const defaultCellSelection: DataSourcePropCellSelection_MultiCell = { defaultSelection: false, selectedCells: [ [3, 'stack'], // rowId + colId [4, 'stack'], [5, 'stack'], [0, 'firstName'], ], }; return ( primaryKey="id" data={dataSource} defaultCellSelection={defaultCellSelection} selectionMode="multi-cell" > debugId="cell-selection-default-selection-example" columns={columns} columnDefaultWidth={100} /> ); } ``` Whe you're using cell selection with or without any default value (via the [`defaultCellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultCellSelection)), you're using an uncontrolled prop. This means that the selection state is managed by the `` component and not by you. If you want to control the selection state yourself, you can use the controlled [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) prop instead - see [Using controlled selection](#using-controlled-selection) for details. ## Cell selection format The [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) prop is an object with the following shape: - `defaultSelection` - `boolean` - whether or not cells are selected by default. - either: - `selectedCells`: `[rowId, colId][]` - an array of cells that should be selected (this is combined with `defaultSelection: false`) - or - `deselectedCells`: `[rowId, colId][]` - an array of cells that should be deselected (this is combined with `defaultSelection: true`) When `defaultSelection` is `true`, you will only need to specify the `deselectedCells` prop. And when `defaultSelection` is `false`, you will only need to specify the `selectedCells` prop. In this way, you can either specify which cells should be selected or which cells should be deselected - and have a default that matches the most common case. The `selectedCells`/`deselectedCells` are arrays of `[rowId, colId]` tuples. The `rowId` is the `id` of the row ([the primary key](https://infinite-table.com/docs/reference/datasource-props/index.md#primaryKey)), and the `colId` is the `id` of the column (the identifier of the column in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) prop). The following scenarios are all possible: ```tsx title="Just a few specified cells are selected" const defaultCellSelection = { defaultSelection: false, selectedCells: [ ['id2', 'stack'], ['id2', 'stack'], ['id0', 'firstName'], ], }; ``` ```tsx title="Everything is selected, except a few cells" const defaultCellSelection = { defaultSelection: true, deselectedCells: [ ['row2', 'stack'], ['row3', 'stack'], ['row5', 'firstName'], ], }; ``` ### Using wildcards for selection It's also possible to use wildcards for selecting cells. This is useful if you want to select all cells in a column, or all cells in a row. ```tsx title="Selecting all cells in a column" const defaultCellSelection = { defaultSelection: false, selectedCells: [ ['*', 'stack'], ['row2', 'firstName'], ], }; ``` ```tsx title="Selecting all cells in a row" const defaultCellSelection = { defaultSelection: false, selectedCells: [ ['row1', '*'], ['row2', 'firstName'], ], }; ``` ```tsx title="Selecting everything except a column" const defaultCellSelection = { defaultSelection: true, deselectedCells: [['*', 'stack']], }; ``` ## Using controlled selection When using the controlled [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) you have to update the value of the property yourself, by listening to the [`onCellSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onCellSelectionChange) event. **Example: Using controlled cell selection** This example shows how to use the [`onCellSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onCellSelectionChange) callback prop to listen to changes to the controlled [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) prop on the `` component. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, DataSourcePropCellSelection_MultiCell, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 60 }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [cellSelection, setCellSelection] = React.useState({ defaultSelection: false, selectedCells: [ [3, 'stack'], [0, 'firstName'], ], }); return (
Current selection:
{JSON.stringify(cellSelection, null, 2)}
primaryKey="id" data={dataSource} cellSelection={cellSelection} onCellSelectionChange={setCellSelection} selectionMode="multi-cell" > debugId="controlled-cell-selection-example" columns={columns} columnDefaultWidth={100} />
); } ``` ## Using the Cell Selection API The `` component also exposes a [Cell Selection API](https://infinite-table.com/docs/reference/cell-selection-api/index.md), which you can use to select and deselect cells programmatically. **Example: Using the CellSelectionAPI to select a column** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, DataSourcePropCellSelection_MultiCell, InfiniteTableApi, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 60 }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [cellSelection, setCellSelection] = React.useState({ defaultSelection: false, selectedCells: [ [3, 'stack'], [0, 'firstName'], ], }); const [api, setApi] = React.useState | null>(); return (
Current selection:
{JSON.stringify(cellSelection, null, 2)}
primaryKey="id" data={dataSource} cellSelection={cellSelection} onCellSelectionChange={setCellSelection} selectionMode="multi-cell" > debugId="controlled-cell-selection-with-api-example" columns={columns} columnDefaultWidth={100} onReady={({ api }) => { setApi(api); }} />
); } ``` --- # Row Selection > InfiniteTable DataGrid component supports single and multiple row selection, including checkbox column selection and lazy rows selection Canonical page: https://infinite-table.com/docs/learn/selection/row-selection `InfiniteTable` offers support for both single and multiple row selection. For selecting cells, see the [Cell Selection](https://infinite-table.com/docs/learn/selection/cell-selection.md) page. ```tsx title="Configure the selection mode on the DataSource component" // can be "single-row", "multi-row", "multi-cell" or false ``` Multiple row selection allows people to select rows just like they would in their MacOS Finder app, by clicking desired rows and using the cmd/shift keys as modifiers. The DataGrid also offers support for **checkbox selection**, which is another easy way of interacting with grid rows, especially when grouped or nested data is used. Row selection (both single and multiple) is driven by the [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) prop, which will contain **primary keys of the selected rows**. The value or values you specify for row selection are primary keys of the rows in the DataGrid. Row selection is defined on the `DataSource` component, so that's where you specify your [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) prop (or the uncontrolled version of it, namely [`defaultRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowSelection) and also the callback prop of [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange)). You can explicitly specify the [`selectionMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode) as `"single-row"` or `"multi-row"` (or `false`) but it will generally be derived from the value of your [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection)/[`defaultRowSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowSelection) prop. # Single Row Selection This is the most basic row selection - in this case the [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) prop (or the uncontrolled variant [`defaultRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowSelection)) will be the primary key of the selected row (a string or a number or `null` for no selection). ```ts {4} primaryKey="id" data={[...]} defaultRowSelection={4} > ``` **Example: Uncontrolled single row selection** Single row selection example - click a row to see selection change. You can also use your keyboard - press the spacebar to select/deselect a row. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { return ( data={dataSource} defaultRowSelection={3} primaryKey="id" > debugId="default-single-row-selection-example" columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` Row selection is changed when the user clicks a row. Clicking a row selects it and clicking it again keeps the row selected. For deselecting the row with the mouse use `cmd`/`ctrl` + click. ## Keybord support You can also use your keyboard to select a row, as by default, [`keyboardSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardSelection) is `true`. Using your keyboard, navigate to the desired row and then press the spacebar to select it. Pressing the spacebar again on the selected row will deselect it. Both `cell` and `row` [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) are available and you can use either of them to perform row selection. ## Controlled single row selection Row selection can be used as a [controlled](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) or [uncontrolled](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowSelection) property. For the controlled version, make sure you also define your [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange) callback prop to update the selection. **Example: Controlled single row selection** This example uses [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange) callback prop to update the controlled [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { const [rowSelection, setRowSelection] = useState(3); return ( <>

Current row selection:

 {JSON.stringify(rowSelection)}.

data={dataSource} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} primaryKey="id" > debugId="controlled-single-row-selection-example" columns={columns} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` # Multi Row Selection You can configure multiple selection for rows so users can interact with it through clicking around or via a checkbox selection column. ## Using your mouse and keyboard to select rows If you're using checkboxes for selection, users will be selecting rows via click or click + `cmd`/`ctrl` and `shift` keys, just like they are used to in their native Finder/Explorer applications. ### Mouse interactions For selecting with the mouse, the following gestures are supported (we tried to exactly replicate the logic/behaviour from MacOS Finder app, so most people should find it really intuitive): - clicking a row (with no modifier keys) will select that row, while clearing any existing selection - click + `cmd`/`ctrl` modifier key will toggle the selection for the clicked row while keeping any other existing selection. So if the row was not selected, it's being added to the current selection, while if the row was already selected, it's being removed from the selection - click + `shift` modifier key will perform a multi selection, starting from the last selection click where the `shift` key was not used. **Example: Multi row selection** Use your mouse to select multiple rows. Expect click and click + `cmd`/`ctrl`/`shift` modifier keys to behave just like they are in the MacOS Finder app. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { return ( data={dataSource} selectionMode="multi-row" primaryKey="id" > debugId="default-selection-mode-multi-row-example" columns={columns} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### Keyboard interactions By default [`keyboardSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardSelection) is enabled, so you can use the **spacebar** key to select multiple rows. Using the spacebar key is equivalent to doing a mouse click, so expect the combination of **spacebar** + `cmd`/`ctrl`/`shift` modifier keys to behave just like clicking + the same modifier keys. **Example: Multi row selection with keyboard support** Use spacebar + optional `cmd`/`ctrl`/`shift` modifier keys just like you would do clicking + the same modifier keys. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { const [keyboardSelection, setKeyboardSelection] = useState(true); return ( <>
Keyboard selection is now{' '} {keyboardSelection ? 'enabled' : 'disabled'}.
data={dataSource} selectionMode="multi-row" primaryKey="id" > debugId="default-selection-mode-multi-row-keyboard-toggle-example" keyboardSelection={keyboardSelection} columns={columns} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` For selecting all the rows in the table, you can use `cmd`/`ctrl` + `A` keyboard shortcut. ## Using a selection checkbox Selection multiple rows is made easier when there is a checkbox column and even-more-so when there is grouping. Configuring checkbox selection is as easy as specifying [renderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) on any of the columns in the grid. [renderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) can either be the boolean `true` or a render function that allows the customization of the selection checkbox. ```ts {8} const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, }, country: { // show the selection checkbox for this column renderSelectionCheckBox: true, field: 'country', }, firstName: { field: 'firstName', }, }; ``` Any column can show a selection checkbox if [column.renderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) is set to `true`. There is nothing prevening you from providing multiple checkbox columns. **Example: Multi row selection with checkbox support** Use the selection checkboxes to select rows. You can also use the spacebar key (+ optional shift modifier) to modify the selection ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { id: { field: 'id', renderSelectionCheckBox: true, defaultWidth: 80, }, country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age', defaultWidth: 80, type: 'number' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { return ( data={dataSource} selectionMode="multi-row" primaryKey="id" > debugId="default-checkbox-selection-multi-row-example" columns={columns} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### Mouse interactions The mouse interactions are the obvious ones you would expect from checkbox selection. Clicking a checkbox will toggle the selection for the correspondign row. Also, clicking the header checkbox will select/deselect all the rows in the table. The selection checkbox in the column header can be in an indeterminate state (when just some of the rows are selected), and when clicking it, it will become checked and select all rows. You can use [renderHeaderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderHeaderSelectionCheckBox) for a column to customize the checkbox in the column header. If no header selection checkbox is specified, [renderSelectionCheckBox](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) will be used for the column header as well, just like it's used for grid rows. ### Keyboard interactions When multi-row selection is configured to use checkboxes, you can still use your keyboard to select rows. Navigate to the desired row (you can have [keyboard navigation](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) active for either cells or rows) and press the spacebar. If the row is not selected it will select it, otherwise it will deselect it. The only supported modifier key when selecting a row by pressing **spacebar** is the `shift` key - it allows users to extend the selection over multiple rows, which is handy. ## Specify a `rowSelection` value When multiple row selection is used, the [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) prop should be an object that can have the following shape: ```ts const rowSelection = { selectedRows: [3, 6, 100, 23], // those specific rows are selected defaultSelection: false, // while all other rows are deselected by default }; // or const rowSelection = { deselectedRows: [3, 6, 100, 23], // those specific rows are deselected defaultSelection: true, // all other rows are selected }; // or, for grouped data - this example assumes groupBy=continent,country,city // for using this form of multi-row selection when you have grouping, // you have to specify DataSource.useGroupKeysForMultiRowSelection = true const rowSelection = { selectedRows: [ 45, // row with id 45 is selected, no matter the group it is nested in ['Europe', 'France'], // all rows in Europe/France are selected ['Asia'], // all rows in Asia are selected ], deselectedRows: [ ['Europe', 'France', 'Paris'], // all rows in Paris are deselected ], defaultSelection: false, // all other rows are selected }; ``` As shown above, the `rowSelection.selectedRows` and `rowSelection.deselectedRows` arrays can either contain: - primary keys of rows (which are usually strings or numbers) - any non-array value inside `rowSelection.selectedRows`/`rowSelection.deselectedRows` is considered an id/primaryKey value for a leaf row in the grouped dataset. - arrays of group keys (can be combined with primary keys as well) - those arrays describe the path of the specified selected group. Please note that `rowSelection.selectedRows` can contain certain paths while `rowSelection.deselectedRows` can contain child paths of those paths ... or any other imaginable combination. For this kind of [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection), you need to enable [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection). Row Selection only uses primary keys by default, even when you have grouped data. For grouping however, you might want to use selection with group keys - for doing that, specify [DataSource.useGroupKeysForMultiRowSelection=true](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection). Note that if you use selection with group keys, the selection will not be relevant/consistent when the [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) changes. When you have both grouping and [lazy loading](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad), [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) must be enabled - read more about it in the note below. When [`lazyLoad`](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad) is being used - this means not all available groups/rows have actually been loaded yet in the dataset - we need a way to allow you to specify that those possibly unloaded rows/groups are selected or not. In this case, the `rowSelection.selectedRows`/`rowSelection.deselectedRows` arrays should not have row primary keys as strings/numbers, but rather rows/groups specified by their full path (so [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) should be set to `true`). ```ts {6} // this example assumes groupBy=continent,country,city const rowSelection = { selectedRows: [ // row with id 45 is selected - we need this because in the lazyLoad scenario, // not all parents might have been made available yet ['Europe','Italy', 'Rome', 45], ['Europe','France'], // all rows in Europe/France are selected ['Asia'] // all rows in Asia are selected ] deselectedRows: [ ['Europe','Italy','Rome'] // all rows in Rome are deselected // but note that row with id 45 is selected, so Rome will be // rendered with an indeterminate selection state ], defaultSelection: false // all other rows are selected } ``` In the example above, we know that there are 3 groups (`continent`, `country`, `city`), so any item in the array that has a 4th element is a fully specified leaf node. While lazy loading, we need this fully specified path for specific nodes, so we know which group rows to render with indeterminate selection. ### Controlled selection with checkbox column When using the controlled [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection), make sure to specify the [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange) callback prop to update the selection accordingly as a result of user interaction. **Example: Multi row checkbox selection with grouping** This example shows how you can use multiple row selection with a predefined controlled value. Go ahead and select some groups/rows and see the selection value adjust. The example also shows how you can use the `InfiniteTableApi` to retrieve the actual ids of the selected rows. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTableProps, InfiniteTablePropColumns, DataSourceProps, DataSourcePropRowSelection_MultiRow, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', renderGroupValue: ({ value }) => `Stack: ${value || ''}`, }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', renderGroupValue: ({ value }) => `Lang: ${value || ''}`, }, canDesign: { field: 'canDesign', renderGroupValue: ({ value }) => `Can design: ${value || ''}`, }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', renderSelectionCheckBox: true, defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { const [rowSelection, setRowSelection] = useState({ selectedRows: [0, 8, 10], defaultSelection: false, }); return (
Current row selection:
 {JSON.stringify(rowSelection)}.
data={dataSource} groupBy={defaultGroupBy} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} primaryKey="id" > debugId="controlled-multi-row-selection-example" columns={columns} domProps={domProps} groupColumn={groupColumn} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ## Multi Selection with Lazy Load and Grouping Probably the most complex use-case for multi selection (with checkbox) is the combination of grouping and lazy-loading. In this scenario, not all groups and/or rows are loaded at a given point in time, but we need to be able to know how to render each checkbox for each group - either checked, unchecked or indeterminate, all this depending on whether all children, at any nesting levels are selected or not. In order to make this possible, the [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) value will only contain arrays (and not individual primary keys) in the `selectedRows` and `deselectedRows` arrays and the DataSource will be configured with [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection). **Example: Multi row checkbox selection with lazy data and grouping** The `DataSet` has lazy loading and grouping. The selection uses group keys (see [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection)), so it can specify as selected even rows/groups that have not been loaded yet. Note in the example below that some of the group rows are partly selected, even if the leaf rows which are specified as selected in the [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) are not yet loaded. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns, InfiniteTablePropGroupColumn, DataSourceData, DataSourcePropRowSelection_MultiRow, DataSourcePropGroupBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, groupKeys = [], }) => { const args = [ pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data) .then((data) => { return new Promise((resolve) => { setTimeout(() => { resolve(data); }, 100); }); }); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage', }, stack: { field: 'stack', }, }; const domProps = { style: { height: '80vh', }, }; const groupBy: DataSourcePropGroupBy = [ { field: 'stack' }, { field: 'preferredLanguage', }, { field: 'canDesign', }, ]; const groupColumn: InfiniteTablePropGroupColumn = { field: 'firstName', defaultWidth: 250, }; export default function GroupByExample() { const [rowSelection] = useState({ defaultSelection: false, selectedRows: [ ['backend', 'Java'], ['backend', 'JavaScript'], ['backend', 'CSharp', 'no', 37], ['backend', 'PHP', 'no', 66], ['frontend'], ], }); return ( primaryKey="id" data={dataSource} groupBy={groupBy} lazyLoad selectionMode="multi-row" defaultRowSelection={rowSelection} useGroupKeysForMultiRowSelection > debugId="lazy-multi-row-selection-example" domProps={domProps} columns={columns} keyboardNavigation="row" groupRenderStrategy="single-column" groupColumn={groupColumn} columnDefaultWidth={200} />
); } ``` --- # Multiple Sorting > Docs and examples on applying multiple sorting to the DataSource for Infinite Table DataGrid Canonical page: https://infinite-table.com/docs/learn/sorting/multiple-sorting By default, if you don't specify otherwise, the DataGrid is configured with single sorting. For multiple sorting, you need to specify the sorting information as an array: ```tsx primaryKey="id" data={data} // we want an array here defaultSortInfo={[]} > columns={columns} /> ``` An empty array means no sorting. However, it does specify that sorting is configured as multiple sorting, so it's useful to set it to `[]` **Example: Configuring multiple sorting with uncontrolled behavior** Try clicking the `age` column and then the `firstName` column. If the multi-sort behavior is `replace`, clicking the second column will remove the sort from the first column. In order for the sorting to be additive, even if the behavior is `replace`, use the `Ctrl`/`Cmd` key while clicking the column header. If the multi-sort behavior is `append`, clicking the second column will add it to the sort. ```ts import { InfiniteTable, DataSource, InfiniteTablePropMultiSortBehavior, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, age: { field: 'age', header: 'Age' }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function LocalUncontrolledSingleSortingExample() { const [multiSortBehavior, setMultiSortBehavior] = React.useState< 'append' | 'replace' >('replace'); return ( <>

Select the multi-sort behavior

primaryKey="id" data={dataSource} defaultSortInfo={[]} > debugId="local-multi-sorting-example-defaults-with-local-data" columns={columns} columnDefaultWidth={120} multiSortBehavior={multiSortBehavior} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', age: 24, currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', age: 46, currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', age: 24, currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', age: 24, currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', age: 24, currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', age: 23, currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', age: 23, currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', age: 22, currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', age: 23, currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', age: 23, currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` ## User interaction and multi sort behavior When `InfiniteTable` is configured with multiple sorting there are two supported behaviors: - `append` - when this behavior is used, clicking a column header adds that column to the alredy existing sort. If the column is already sorted, the sort direction is reversed. In order to remove a column from the sort, the user needs to click the column header in order to toggle sorting from ascending to descending and then to no sorting. - `replace` - the default behavior - a user clicking a column header removes any existing sorting and sets that column as sorted. In order to add a new column to the sort, the user needs to hold the `Ctrl/Cmd` key while clicking the column header. The behavior of multiple sorting is configured via the [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior) - the default value for this prop is `"replace"`. ❗️❗️❗️ The [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior) prop is defined on the `InfiniteTable` component, not on the `DataSource` component - as it's the `InfiniteTable` that handles user interaction, even though the `DataSource` does the actual sorting. ### Multi sort behavior - `append` #### Scenario 1 - user clicks a column header to sort by that column - an ascending sort is added, and the column header will contain the sort index - `1` - if user clicks the same column, the sort direction is reversed - sort index is preserved as `1`, but descending order is set. - user clicks the same column again - the column is removed from the sort. #### Scenario 2 - user clicks a column header to sort by that column - an ascending sort is added, and the column header will contain the sort index - `1` - user clicks another column - the new column is added to the sort, with ascending order and sort index `2`. The initial clicked column is still the sorted, and that sort is applied first. For equal values on column `1`, the sort by column `2` is applied. - user clicks column `2` again - the sort direction is reversed for the second column. So now the sort order is `1` ascending, `2` descending. - user clicks column `2` again - the column is removed from the sort. The sorting now only contains the first column, in ascending order. ### Multi sort behavior - `replace` This is the [default behavior](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior) for multiple sorting. In the `replace` behavior, clicking a column header will remove any existing sorting and set that specific column as sorted. In order to add a new column to the sort, the user needs to hold the `Ctrl`/`Cmd` key while clicking a column header. Holding the `Ctrl`/`Cmd` key while clicking a column header results in the same behavior as the `append`. ## Controlled and uncontrolled sorting As noted above, for multiple sorting, you need to specify an array of objects - see [`DataSourceSingleSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceSingleSortInfo) for more on the shape of those objects: ```ts // sort by age in descending order, then by `firstName` in ascending order sortInfo = [ { field: 'age', type: 'number', dir: -1 }, { field: 'firstName', dir: 1 }, ]; // no sorting sortInfo = []; ``` The simplest way to use multiple sorting is via the uncontrolled [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop. Specify an empty array as the default value, and multiple sorting will be enabled. This allows sorting by multiple fields (to which columns are bound) - you can specify however many you want - so when sorting two objects in the `DataSource`, the first `sortInfo` is used to compare the two, and then, on equal values, the next `sortInfo` is used and so on. If you want to change the sorting from code, after the component is mounted, you need to use the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop. In this case, make sure you update the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop as a result of user interaction, by using the [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) callback. **Example: Local + uncontrolled multi-sorting example** This table allows sorting multiple columns - initially the `country` column is sorted in descending order and the `salary` column is sorted in ascending order. `Ctrl`/`Cmd` + click the `salary` column to toggle the column sort to descending. `Ctrl`/`Cmd` clicking it a second time will remove it from the sort altogether. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, id: { field: 'id' }, canDesign: { field: 'canDesign' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '90vh' } }; const shouldReloadData = { sortInfo: false, }; export default function LocalUncontrolledMultiSortingExampleWithRemoteData() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={[ { field: 'country', dir: -1 }, { field: 'salary', dir: 1 }, ]} shouldReloadData={shouldReloadData} > debugId="local-uncontrolled-multi-sorting-example-with-remote-data" domProps={domProps} columns={columns} columnDefaultWidth={120} /> ); } ``` **Example: Remote + uncontrolled multi-sorting example** ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = ({ sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, preferredLanguage: { field: 'preferredLanguage' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: true, }; export default function RemoteUncontrolledMultiSortingExample() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={[ { field: 'salary', dir: -1, }, ]} shouldReloadData={shouldReloadData} > debugId="remote-uncontrolled-multi-sorting-example" columns={columns} columnDefaultWidth={120} /> ); } ``` If you use uncontrolled sorting via [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) there's no way to switch between single and multiple sorting after the component is mounted. If you have this use-case, you need to use the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop. ## Remote Sorting Sorting remotely makes a lot of sense when using a function as your [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) source. Whenever the sort information is changed, the function will be called with all the information needed to retrieve the data from the remote endpoint. For remote sorting, make sure you specify [shouldReloadData.sortInfo=true](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) - if you don't, the data will also be sorted locally in the browser (which most of the times will be harmless, but it means wasted CPU cycles). **Example: Remote + controlled multi-sorting example** ```ts import { InfiniteTable, DataSource, DataSourceData, DataSourcePropSortInfo, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = ({ sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function RemoteControlledMultiSortingExample() { const [sortInfo, setSortInfo] = React.useState< DataSourcePropSortInfo >([ { field: 'salary', dir: -1, }, ]); const shouldReloadData = { sortInfo: true, }; return ( <> primaryKey="id" data={dataSource} sortInfo={sortInfo} shouldReloadData={shouldReloadData} onSortInfoChange={setSortInfo} > debugId="remote-controlled-multi-sorting-example" columns={columns} columnDefaultWidth={220} /> ); } ``` In the example above, remote and controlled sorting are combined - because `shouldReloadData.sortInfo=true` is specified, the `` will call the `data` function whenever sorting changes, and will pass in the `dataParams` object that contains the sort information. --- # Sorting > Docs and examples on sorting the DataSource for Infinite Table DataGrid Canonical page: https://infinite-table.com/docs/learn/sorting/overview `InfiniteTable` comes with multiple sorting behaviours, which are described below. Both [single sorting](https://infinite-table.com/docs/learn/sorting/single-sorting.md) and [multiple sorting](https://infinite-table.com/docs/learn/sorting/multiple-sorting.md) are supported via the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) and [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) props. ### Single Sorting For [single sorting](https://infinite-table.com/docs/learn/sorting/single-sorting.md), [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) (or the uncontrolled [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo)) should an object like ```ts // sort by `firstName`, in ascending order sortInfo = { field: 'firstName', dir: 1 }; ``` or you can use ```ts // no sorting sortInfo = null; ``` for explicit no sorting. When you use controlled sorting via [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo), make sure you also listen to [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) for changes, to get notifications when sorting is changed by the user. Also, for controlled sorting, it's your responsibility to sort the data - read bellow in the [controlled and uncontrolled section](#controlled-and-uncontrolled-sorting). The sort information object has the following shape (see [`DataSourceSingleSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceSingleSortInfo) for details): - `dir` - `1 | -1` - the direction of the sorting - `field?` - `keyof DATA_TYPE` - the field to sort by - optional. - `id?` - `string` - if you don't sort by a field, you can specify an id of the column this sorting is bound to. Note that columns have a [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter), which will be used when doing local sorting and the column is not bound to an exact field. - `type?` - the sort type - one of the keys in [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) - eg `"string"`, `"number"`, `"date"` - will be used for local sorting, to provide the proper comparison function. **Example: Local + uncontrolled single-sorting example** This example shows initial sorting by `salary` in ascending order. Click the header of the `salary` column to sort in descending order and then click it again to unsort. ```ts import { InfiniteTable, DataSource, DataSourceSingleSortInfo, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName' }, age: { field: 'age' }, country: { field: 'country' }, preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const defaultSortInfo: DataSourceSingleSortInfo = { field: 'age', dir: 1, }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={defaultSortInfo} > debugId="local-uncontrolled-single-sorting-example-with-local-data" columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', lastName: 'Klein', country: 'India', city: 'Unnao', age: 24, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', lastName: 'Runolfsson', country: 'Mexico', city: 'Cuitlahuac', age: 46, currency: 'USD', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', lastName: 'McGlynn', country: 'United Arab Emirates', city: 'Fujairah', age: 54, currency: 'JPY', preferredLanguage: 'Go', stack: 'frontend', canDesign: 'yes', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', lastName: 'McLaughlin', country: 'Mexico', city: 'Tlacolula de Matamoros', age: 43, currency: 'CHF', preferredLanguage: 'Rust', stack: 'backend', canDesign: 'no', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', lastName: 'Harber', country: 'France', city: 'Persan', age: 23, currency: 'EUR', preferredLanguage: 'Go', stack: 'backend', canDesign: 'yes', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', lastName: 'Schroeder', country: 'United States', city: 'Hays', age: 34, currency: 'EUR', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', lastName: 'Mills', country: 'Mexico', city: 'Nuevo Casas Grandes', age: 33, currency: 'AUD', preferredLanguage: 'JavaScript', stack: 'frontend', canDesign: 'no', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', lastName: 'Hayes', country: 'Sweden', city: 'Delsbo', age: 22, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'full-stack', canDesign: 'yes', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', lastName: 'Boyle', country: 'Germany', city: 'Bad Camberg', age: 11, currency: 'GBP', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', lastName: 'Deckow', country: 'Canada', city: 'Raymore', age: 31, currency: 'EUR', preferredLanguage: 'Rust', stack: 'frontend', canDesign: 'yes', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` By default, columns in the InfiniteTable DataGrid are sortable. If you want to disable column sorting for all columns, use [columnDefaultSortable=false](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultSortable) and then you can turn it back on per-column, by setting [column.defaultSortable=true](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortable). ### Multiple Sorting If you want to use [multiple sorting](https://infinite-table.com/docs/learn/sorting/multiple-sorting.md), specify an array of objects like ```ts // sort by age in descending order, then by `firstName` in ascending order sortInfo = [ { field: 'age', type: 'number', dir: -1 }, { field: 'firstName', dir: 1 }, ]; // no sorting sortInfo = []; ``` This allows sorting by multiple fields (to which columns are bound) - you can specify however many you want - so when sorting two objects in the `DataSource`, the first `sortInfo` is used to compare the two, and then, on equal values, the next `sortInfo` is used and so on. **Example: Local + uncontrolled multi-sorting example** This table allows sorting multiple columns - initially the `country` column is sorted in descending order and the `salary` column is sorted in ascending order. Click the `salary` column to toggle the column sort to descending. Clicking it a second time will remove it from the sort altogether. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, id: { field: 'id' }, canDesign: { field: 'canDesign' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '90vh' } }; const shouldReloadData = { sortInfo: false, }; export default function LocalUncontrolledMultiSortingExampleWithRemoteData() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={[ { field: 'country', dir: -1 }, { field: 'salary', dir: 1 }, ]} shouldReloadData={shouldReloadData} > debugId="local-uncontrolled-multi-sorting-example-with-remote-data" domProps={domProps} columns={columns} columnDefaultWidth={120} /> ); } ``` **Example: Remote + uncontrolled multi-sorting example** ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = ({ sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, preferredLanguage: { field: 'preferredLanguage' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: true, }; export default function RemoteUncontrolledMultiSortingExample() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={[ { field: 'salary', dir: -1, }, ]} shouldReloadData={shouldReloadData} > debugId="remote-uncontrolled-multi-sorting-example" columns={columns} columnDefaultWidth={120} /> ); } ``` If you use uncontrolled sorting via [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) there's no way to switch between single and multiple sorting after the component is mounted. If you have this use-case, you need to use the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop. ## Understanding local and remote sorting Sorting can be done both locally in the browser and remotely on the server. When you want sorting to be performed remotely on the server, a change on the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) should trigger a reload of the datasource. In order to achieve this, you need to specify [shouldReloadData.sortInfo=true](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo). Possible values for [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) are `false` (sorting will be performed locally and won't trigger a reload of the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) source) and `true` (sorting will be performed remotely and will trigger a reload of the data). This allows you fine-grained control on how sorting is done, either in the client or on the server. ### Uncontrolled sorting If you use uncontrolled sorting (namely you don't care about updating the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) yourself as a result of user interaction - via [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange)) - then by default, the [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) is `false` unless you specify otherwise. You can initially render the component with no sort state or you can specify a default sorting state, via the uncontrolled prop [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo). ```tsx // initially render the component with ascending sorting on `firstName` field // also, note this is an array, so multiple sorting will be enabled const defaultSortInfo = [{ field: 'firstName', dir: 1 }]; primaryKey="id" data={data} defaultSortInfo={defaultSortInfo} > ; ``` If your data is remote and you want the sorting to happen on the backend, you can still use uncontrolled sorting, but you need to specify [shouldReloadData.sortInfo=true](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo). Using remote sort mode will trigger a call to the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function whenever sorting changes, so you can re-fetch the data from the backend, according to the new `sortInfo`. Whe `local` uncontrolled sorting is used, the `` sorts the data internally, based on the existing sorting information. To start with a specific `sortInfo`, use the [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop. As the user interacts with the table, [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) is being called with updated sort info and the `` continues to sort the data accordingly. The [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop is an uncontrolled prop, so it's all managed inside the `` component and you can't change it from the outside. If you need to control it from outside the component, use the [controlled sortInfo](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop - read the next section for more details ### Controlled Sorting When you use the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop, by default the [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) is set to `true`, unless you specify otherwise. Also, be aware that when the user interacts with the DataGrid when controlled sorting is configured, the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop will not update automatically - you need to listen to [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) and update the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) yourself. Just like with uncontrolled sorting, updating the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) when `shouldReloadData.sortInfo` is `true`, will trigger a call to the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function, so new sorted data can be re-fetched. When the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) is combined with [shouldReloadData.sortInfo=false](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo), the `` will sort the data internally, on any changes of the sorting information. But remember it's your responsibility to update the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop when the user interacts with the DataGrid. Both controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) and uncontrolled [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) work in combination with [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) - use it to be notified when sorting changes, so you can react and update your app accordingly if needed. ### Local Sorting When you use uncontrolled sorting locally, the `` will sort the data internally, based on the [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop. Local sorting is available for any configured [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) source - be it an array or a function that returns a promise. You can use [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange), which is called whenever any of the sorting, filtering, grouping or pivoting information changes. **Example: Local uncontrolled sorting + local data** ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: false, }; export default function LocalUncontrolledSingleSortingExampleWithRemoteData() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={{ field: 'salary', dir: -1 }} shouldReloadData={shouldReloadData} > debugId="local-uncontrolled-single-sorting-example-with-remote-data" columns={columns} columnDefaultWidth={220} /> ); } ``` **Example: Local uncontrolled sorting + remote data** ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: false, }; export default function LocalUncontrolledSingleSortingExampleWithRemoteData() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={{ field: 'salary', dir: -1 }} shouldReloadData={shouldReloadData} > debugId="local-uncontrolled-single-sorting-example-with-remote-data" columns={columns} columnDefaultWidth={220} /> ); } ``` ### Remote Sorting Sorting remotely makes a lot of sense when using a function as your [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) source. Whenever the sort information is changed, the function will be called with all the information needed to retrieve the data from the remote endpoint. For remote sorting, make sure you specify [shouldReloadData.sortInfo=true](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) - if you don't, the data will also be sorted locally in the browser (which most of the times will be harmless, but it means wasted CPU cycles). **Example: Remote + controlled multi-sorting example** ```ts import { InfiniteTable, DataSource, DataSourceData, DataSourcePropSortInfo, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = ({ sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function RemoteControlledMultiSortingExample() { const [sortInfo, setSortInfo] = React.useState< DataSourcePropSortInfo >([ { field: 'salary', dir: -1, }, ]); const shouldReloadData = { sortInfo: true, }; return ( <> primaryKey="id" data={dataSource} sortInfo={sortInfo} shouldReloadData={shouldReloadData} onSortInfoChange={setSortInfo} > debugId="remote-controlled-multi-sorting-example" columns={columns} columnDefaultWidth={220} /> ); } ``` In the example above, remote and controlled sorting are combined - because `shouldReloadData.sortInfo=true` is specified, the `` will call the `data` function whenever sorting changes, and will pass in the `dataParams` object that contains the sort information. ## Custom Sort Functions with `sortTypes` By default, all columns are sorted as strings, even if they contain numeric values. To make numeric columns sort as numbers, you need to specify [a `dataType` for the column](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType), or, [a column `sortType`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType). There are 3 `dataType` values that can be used: - `"string"` - `"number"` - `"date"` Each dataType has its own sorting function and its own filtering operators & functions. Sorting works in combination with the [`sortTypes`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortTypes) property, which is an object with keys being sort types and values being functions that compare two values of the same type. ```ts const sortTypes = { string: (a, b) => a.localeCompare(b), number: (a, b) => a - b, date: (a, b) => a - b, }; ``` Those are the three sort types supported by default. The functions specified in the [`sortTypes`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortTypes) object need to always sort data in ascending order. A column can choose to use a specific [`columns.sortType`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType), in which case, for local sorting, the corresponding sort function will be used, or, it can simply specify a [dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) and the `sortType` with the same name will be used (when no explicit [sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) is defined). To conclude, the [dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) of a column will be used as the [sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) and [filterType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.filterType), when those are not explicitly specified. **Example: Custom sort by color - magenta will come first** ```ts import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export type CarSale = { id: number; make: string; model: string; year: number; sales: number; color: string; }; const carsales: CarSale[] = [ { make: 'Volkswagen', model: 'GTI', year: 2009, sales: 6, color: 'red', id: 0, }, { make: 'Honda', model: 'Element 2WD', year: 2009, sales: 739, color: 'red', id: 1, }, { make: 'Acura', model: 'RDX 4WD', year: 2008, sales: 2, color: 'magenta', id: 2, }, { make: 'Honda', model: 'Fit', year: 2009, sales: 211, color: 'blue', id: 3, }, { make: 'Mazda', model: '6', year: 2009, sales: 31, color: 'blue', id: 4, }, { make: 'Acura', model: 'TSX', year: 2009, sales: 14, color: 'yellow', id: 5, }, { make: 'Acura', model: 'TSX', year: 2010, sales: 14, color: 'red', id: 6, }, { make: 'Audi', model: 'A3', year: 2009, sales: 2, color: 'magenta', id: 7, }, ]; const columns: Record> = { color: { field: 'color', sortType: 'color' }, make: { field: 'make' }, model: { field: 'model' }, sales: { field: 'sales', sortType: 'number', }, year: { field: 'year', sortType: 'number', }, }; const newSortTypes = { color: (one: string, two: string) => { if (one === 'magenta') { // magenta comes first return -1; } if (two === 'magenta') { // magenta comes first return 1; } return one.localeCompare(two); }, }; export default function DataTestPage() { return ( <> data={carsales} primaryKey="id" defaultSortInfo={{ field: 'color', dir: 1, type: 'color', }} sortTypes={newSortTypes} > debugId="sortTypes-example" columns={columns} /> ); } ``` In this example, for the `"color"` column, we specified [column.sortType="color"](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) - we could have passed that as `column.dataType` instead, but if the grid had filtering, it wouldn't know what filters to use for "color" - so we used [column.sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) to only change how the data is sorted. When you provide a [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop and the sorting information uses a custom [sortType](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes), make sure you specify that as the `type` property of the sorting info object. ```tsx defaultSortInfo={{ field: 'color', dir: 1, // note this custom sort type type: 'color', }} ``` You will need to have a property for that type in your [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) object as well. ```tsx sortTypes={{ color: (a, b) => //... }} ``` ## Replacing the sort function While there are many ways to customise sorting, including the [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) mentioned above, you might want to completely replace the sorting function used by the `` component. You can do this by configuring the [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) prop. ```tsx const sortFunction = (sortInfo, dataArray) => { // sort the dataArray according to the sortInfo // and return the sorted array // return sortedDataArray; }; sortFunction={sortFunction} />; ``` The function specified in the [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) prop is called with the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) as the first argument and the data array as the second. It should return a sorted array, as per the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) it was called with. When [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) is specified, [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) will be forced to `false`, as the sorting is done in the browser. **Example: Using a custom sortFunction** ```ts import { InfiniteTable, DataSource, DataSourceSingleSortInfo, multisort, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const defaultSortInfo: DataSourceSingleSortInfo = { field: 'stack', dir: 1, }; const sortFunction = ( sortInfo: DataSourceSingleSortInfo[], arr: Developer[], ) => { // you call the default sorting const result = multisort(sortInfo, arr); // and also apply your custom sorting // result.sort((a, b) => { // }) return result; }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={defaultSortInfo} sortFunction={sortFunction} > debugId="local-sortFunction-single-sorting-example-with-local-data-example" columns={columns} columnDefaultWidth={220} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', lastName: 'Klein', country: 'India', city: 'Unnao', age: 24, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', lastName: 'Runolfsson', country: 'Mexico', city: 'Cuitlahuac', age: 46, currency: 'USD', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', lastName: 'McGlynn', country: 'United Arab Emirates', city: 'Fujairah', age: 54, currency: 'JPY', preferredLanguage: 'Go', stack: 'frontend', canDesign: 'yes', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', lastName: 'McLaughlin', country: 'Mexico', city: 'Tlacolula de Matamoros', age: 43, currency: 'CHF', preferredLanguage: 'Rust', stack: 'backend', canDesign: 'no', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', lastName: 'Harber', country: 'France', city: 'Persan', age: 23, currency: 'EUR', preferredLanguage: 'Go', stack: 'backend', canDesign: 'yes', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', lastName: 'Schroeder', country: 'United States', city: 'Hays', age: 34, currency: 'EUR', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', lastName: 'Mills', country: 'Mexico', city: 'Nuevo Casas Grandes', age: 33, currency: 'AUD', preferredLanguage: 'JavaScript', stack: 'frontend', canDesign: 'no', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', lastName: 'Hayes', country: 'Sweden', city: 'Delsbo', age: 22, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'full-stack', canDesign: 'yes', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', lastName: 'Boyle', country: 'Germany', city: 'Bad Camberg', age: 11, currency: 'GBP', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', lastName: 'Deckow', country: 'Canada', city: 'Raymore', age: 31, currency: 'EUR', preferredLanguage: 'Rust', stack: 'frontend', canDesign: 'yes', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` --- # Single Sorting > Docs and examples on single-column sorting for Infinite Table DataGrid Canonical page: https://infinite-table.com/docs/learn/sorting/single-sorting By default, the Infinite Table is sortable - clicking a column will sort the grid by that column. Clicking again will reverse the sort and a third click on the column removes the sort altogether. At any point, clicking another column header removes any existing column sort and performs a new sort by the clicked column. This is called single sorting - only one column can be sorted at a time. Technically, it's the `` that's being sorted, not the `` component. **Example: Default behavior is single sorting.** By default, clicking a column header sorts the column. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, age: { field: 'age', header: 'Age' }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="local-single-sorting-example-defaults-with-local-data" columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', age: 24, currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', age: 46, currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', age: 54, currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', age: 43, currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', age: 23, currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', age: 34, currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', age: 33, currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', age: 22, currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', age: 11, currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', age: 31, currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` ## Apply a default sort order You can specify a default sort order by using the [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop - specify an object like ```ts // sort by `firstName`, in ascending order defaultSortInfo = { field: 'firstName', dir: 1 }; ``` [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) is an uncontrolled property, so updating the sorting by clicking a column header does not require you to respond to user actions via the [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange). Uncontrolled sorting is managed internally by the `` component, so you don't need to worry about it. For controlled sorting, make sure you use the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop and the [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) callback. **Example: Local + uncontrolled single-sorting example** The `age` column is sorted in ascending order. ```ts import { InfiniteTable, DataSource, DataSourceSingleSortInfo, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName' }, age: { field: 'age' }, country: { field: 'country' }, preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const defaultSortInfo: DataSourceSingleSortInfo = { field: 'age', dir: 1, }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={defaultSortInfo} > debugId="local-uncontrolled-single-sorting-example-with-local-data" columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', lastName: 'Klein', country: 'India', city: 'Unnao', age: 24, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', lastName: 'Runolfsson', country: 'Mexico', city: 'Cuitlahuac', age: 46, currency: 'USD', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', lastName: 'McGlynn', country: 'United Arab Emirates', city: 'Fujairah', age: 54, currency: 'JPY', preferredLanguage: 'Go', stack: 'frontend', canDesign: 'yes', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', lastName: 'McLaughlin', country: 'Mexico', city: 'Tlacolula de Matamoros', age: 43, currency: 'CHF', preferredLanguage: 'Rust', stack: 'backend', canDesign: 'no', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', lastName: 'Harber', country: 'France', city: 'Persan', age: 23, currency: 'EUR', preferredLanguage: 'Go', stack: 'backend', canDesign: 'yes', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', lastName: 'Schroeder', country: 'United States', city: 'Hays', age: 34, currency: 'EUR', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', lastName: 'Mills', country: 'Mexico', city: 'Nuevo Casas Grandes', age: 33, currency: 'AUD', preferredLanguage: 'JavaScript', stack: 'frontend', canDesign: 'no', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', lastName: 'Hayes', country: 'Sweden', city: 'Delsbo', age: 22, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'full-stack', canDesign: 'yes', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', lastName: 'Boyle', country: 'Germany', city: 'Bad Camberg', age: 11, currency: 'GBP', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', lastName: 'Deckow', country: 'Canada', city: 'Raymore', age: 31, currency: 'EUR', preferredLanguage: 'Rust', stack: 'frontend', canDesign: 'yes', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` ## Controlled sorting For controlled, single sorting, use the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) as an object like this: ```ts // sort by `firstName`, in ascending order sortInfo = { field: 'firstName', dir: 1 }; ``` or you can specify `null` for explicit no sorting ```ts // no sorting sortInfo = null; ``` When you use controlled sorting via [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo), make sure you also listen to [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) for changes, to get notifications when sorting is changed by the user. Also, for controlled sorting, it's your responsibility to sort the data - read bellow in the [controlled and uncontrolled section](#controlled-and-uncontrolled-sorting). ## Describing the sort order To describe the sorting order, you have to use an object that has the following shape: - `dir` - `1 | -1` - the direction of the sorting - `field?` - `keyof DATA_TYPE` - the field to sort by - optional. - `id?` - `string` - if you don't sort by a field, you can specify an id of the column this sorting is bound to. Note that columns have a [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter), which will be used when doing local sorting and the column is not bound to an exact field. - `type?` - the sort type - one of the keys in [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) - eg `"string"`, `"number"`, `"date"` - will be used for local sorting, to provide the proper comparison function. ### Multiple Sorting If you want to use multiple sorting, specify an array of objects like ```ts // sort by age in descending order, then by `firstName` in ascending order sortInfo = [ { field: 'age', type: 'number', dir: -1 }, { field: 'firstName', dir: 1 }, ]; // no sorting sortInfo = []; ``` This allows sorting by multiple fields (to which columns are bound) - you can specify however many you want - so when sorting two objects in the `DataSource`, the first `sortInfo` is used to compare the two, and then, on equal values, the next `sortInfo` is used and so on. **Example: Local + uncontrolled multi-sorting example** This table allows sorting multiple columns - initially the `country` column is sorted in descending order and the `salary` column is sorted in ascending order. Click the `salary` column to toggle the column sort to descending. Clicking it a second time will remove it from the sort altogether. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, id: { field: 'id' }, canDesign: { field: 'canDesign' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '90vh' } }; const shouldReloadData = { sortInfo: false, }; export default function LocalUncontrolledMultiSortingExampleWithRemoteData() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={[ { field: 'country', dir: -1 }, { field: 'salary', dir: 1 }, ]} shouldReloadData={shouldReloadData} > debugId="local-uncontrolled-multi-sorting-example-with-remote-data" domProps={domProps} columns={columns} columnDefaultWidth={120} /> ); } ``` **Example: Remote + uncontrolled multi-sorting example** ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = ({ sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, preferredLanguage: { field: 'preferredLanguage' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: true, }; export default function RemoteUncontrolledMultiSortingExample() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={[ { field: 'salary', dir: -1, }, ]} shouldReloadData={shouldReloadData} > debugId="remote-uncontrolled-multi-sorting-example" columns={columns} columnDefaultWidth={120} /> ); } ``` If you use uncontrolled sorting via [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) there's no way to switch between single and multiple sorting after the component is mounted. If you have this use-case, you need to use the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop. ## Understanding sort mode Sorting can be done both locally in the browser and remotely on the server. For configuring where sorting is performed you need to specify the [shouldReloadData.sortInfo](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo). Possible values for [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) are `false` (for local sorting) and `true` (for remote sorting). This allows you fine-grained control on how sorting is done, either in the client or on the server. ### Uncontrolled sorting If you use uncontrolled sorting (namely you don't care about updating the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) yourself as a result of user interaction - via [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange)) - then by default, the [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) is `false` (so local sorting) unless you specify otherwise. You can initially render the component with no sort state or you can specify a default sorting state, via the uncontrolled prop [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo). ```tsx // initially render the component with ascending sorting on `firstName` field // also, note this is an array, so multiple sorting will be enabled const defaultSortInfo = [{ field: 'firstName', dir: 1 }]; primaryKey="id" data={data} defaultSortInfo={defaultSortInfo} > ; ``` If your data is remote and you want the sorting to happen on the backend, you can still use uncontrolled sorting, but you need to specify [shouldReloadData.sortInfo=true](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo). Using remote sort mode will trigger a call to the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function whenever sorting changes, so you can re-fetch the data from the backend, according to the new `sortInfo`. Whe `local` uncontrolled sorting is used, the `` sorts the data internally, based on the existing sorting information. To start with a specific `sortInfo`, use the [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop. As the user interacts with the table, [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) is being called with updated sort info and the `` continues to sort the data accordingly. The [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop is an uncontrolled prop, so it's all managed inside the `` component and you can't change it from the outside. If you need to control it from outside the component, use the [controlled sortInfo](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop - read the next section for more details ### Controlled Sorting When you use the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop, by default the [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) is `true` (remote sorting), unless you specify otherwise. Also, be aware that when the user interacts with the DataGrid when controlled sorting is configured, the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop will not update automatically - you need to listen to [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) and update the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) yourself. Just like with uncontrolled sorting, updating the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) when `shouldReloadData.sortInfo=true`, will trigger a call to the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function, so new sorted data can be re-fetched. When the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) is combined with [shouldReloadData.sortInfo=false](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo), the `` will sort the data internally, on any changes of the sorting information. But remember it's your responsibility to update the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop when the user interacts with the DataGrid. Both controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) and uncontrolled [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) work in combination with [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) - use it to be notified when sorting changes, so you can react and update your app accordingly if needed. ### Local Sorting When you use uncontrolled sorting locally, the `` will sort the data internally, based on the [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop. Local sorting is available for any configured [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) source - be it an array or a function that returns a promise. You can use [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange), which is called whenever any of the sorting, filtering, grouping or pivoting information changes. **Example: Local uncontrolled sorting + local data** ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: false, }; export default function LocalUncontrolledSingleSortingExampleWithRemoteData() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={{ field: 'salary', dir: -1 }} shouldReloadData={shouldReloadData} > debugId="local-uncontrolled-single-sorting-example-with-remote-data" columns={columns} columnDefaultWidth={220} /> ); } ``` **Example: Local uncontrolled sorting + remote data** ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: false, }; export default function LocalUncontrolledSingleSortingExampleWithRemoteData() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={{ field: 'salary', dir: -1 }} shouldReloadData={shouldReloadData} > debugId="local-uncontrolled-single-sorting-example-with-remote-data" columns={columns} columnDefaultWidth={220} /> ); } ``` ### Remote Sorting Sorting remotely makes a lot of sense when using a function as your [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) source. Whenever the sort information is changed, the function will be called with all the information needed to retrieve the data from the remote endpoint. For remote sorting, make sure you specify [shouldReloadData.sortInfo=true](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) - if you don't, the data will also be sorted locally in the browser (which most of the times will be harmless, but it means wasted CPU cycles). **Example: Remote + controlled multi-sorting example** ```ts import { InfiniteTable, DataSource, DataSourceData, DataSourcePropSortInfo, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = ({ sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function RemoteControlledMultiSortingExample() { const [sortInfo, setSortInfo] = React.useState< DataSourcePropSortInfo >([ { field: 'salary', dir: -1, }, ]); const shouldReloadData = { sortInfo: true, }; return ( <> primaryKey="id" data={dataSource} sortInfo={sortInfo} shouldReloadData={shouldReloadData} onSortInfoChange={setSortInfo} > debugId="remote-controlled-multi-sorting-example" columns={columns} columnDefaultWidth={220} /> ); } ``` In the example above, remote and controlled sorting are combined - because `shouldReloadData.sortInfo=true` is specified, the `` will call the `data` function whenever sorting changes, and will pass in the `dataParams` object that contains the sort information. ## Custom Sort Functions with `sortTypes` By default, all columns are sorted as strings, even if they contain numeric values. To make numeric columns sort as numbers, you need to specify [a `dataType` for the column](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType), or, [a column `sortType`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType). There are two `dataType` values that can be used: - `"string"` - `"number"` Each dataType has its own sorting function and its own filtering operators & functions. Sorting works in combination with the [`sortTypes`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortTypes) property, which is an object with keys being sort types and values being functions that compare two values of the same type. ```ts const sortTypes = { string: (a, b) => a.localeCompare(b), number: (a, b) => a - b, }; ``` Those are the two sort types supported by default. The functions specified in the [`sortTypes`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortTypes) object need to always sort data in ascending order. A column can choose to use a specific [`columns.sortType`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType), in which case, for local sorting, the corresponding sort function will be used, or, it can simply specify a [dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) and the `sortType` with the same name will be used (when no explicit [sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) is defined). To conclude, the [dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) of a column will be used as the [sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) and [filterType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.filterType), when those are not explicitly specified. **Example: Custom sort by color - magenta will come first** ```ts import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export type CarSale = { id: number; make: string; model: string; year: number; sales: number; color: string; }; const carsales: CarSale[] = [ { make: 'Volkswagen', model: 'GTI', year: 2009, sales: 6, color: 'red', id: 0, }, { make: 'Honda', model: 'Element 2WD', year: 2009, sales: 739, color: 'red', id: 1, }, { make: 'Acura', model: 'RDX 4WD', year: 2008, sales: 2, color: 'magenta', id: 2, }, { make: 'Honda', model: 'Fit', year: 2009, sales: 211, color: 'blue', id: 3, }, { make: 'Mazda', model: '6', year: 2009, sales: 31, color: 'blue', id: 4, }, { make: 'Acura', model: 'TSX', year: 2009, sales: 14, color: 'yellow', id: 5, }, { make: 'Acura', model: 'TSX', year: 2010, sales: 14, color: 'red', id: 6, }, { make: 'Audi', model: 'A3', year: 2009, sales: 2, color: 'magenta', id: 7, }, ]; const columns: Record> = { color: { field: 'color', sortType: 'color' }, make: { field: 'make' }, model: { field: 'model' }, sales: { field: 'sales', sortType: 'number', }, year: { field: 'year', sortType: 'number', }, }; const newSortTypes = { color: (one: string, two: string) => { if (one === 'magenta') { // magenta comes first return -1; } if (two === 'magenta') { // magenta comes first return 1; } return one.localeCompare(two); }, }; export default function DataTestPage() { return ( <> data={carsales} primaryKey="id" defaultSortInfo={{ field: 'color', dir: 1, type: 'color', }} sortTypes={newSortTypes} > debugId="sortTypes-example" columns={columns} /> ); } ``` In this example, for the `"color"` column, we specified [column.sortType="color"](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) - we could have passed that as `column.dataType` instead, but if the grid had filtering, it wouldn't know what filters to use for "color" - so we used [column.sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) to only change how the data is sorted. When you provide a [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) prop and the sorting information uses a custom [sortType](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes), make sure you specify that as the `type` property of the sorting info object. ```tsx defaultSortInfo={{ field: 'color', dir: 1, // note this custom sort type type: 'color', }} ``` You will need to have a property for that type in your [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) object as well. ```tsx sortTypes={{ color: (a, b) => //... }} ``` ## Replacing the sort function While there are many ways to customise sorting, including the [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) mentioned above, you might want to completely replace the sorting function used by the `` component. You can do this by configuring the [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) prop. ```tsx const sortFunction = (sortInfo, dataArray) => { // sort the dataArray according to the sortInfo // and return the sorted array // return sortedDataArray; }; sortFunction={sortFunction} />; ``` The function specified in the [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) prop is called with the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) as the first argument and the data array as the second. It should return a sorted array, as per the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) it was called with. When [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) is specified, [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) will be forced to `false`, as the sorting is done in the browser. **Example: Using a custom sortFunction** ```ts import { InfiniteTable, DataSource, DataSourceSingleSortInfo, multisort, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const defaultSortInfo: DataSourceSingleSortInfo = { field: 'stack', dir: 1, }; const sortFunction = ( sortInfo: DataSourceSingleSortInfo[], arr: Developer[], ) => { // you call the default sorting const result = multisort(sortInfo, arr); // and also apply your custom sorting // result.sort((a, b) => { // }) return result; }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={defaultSortInfo} sortFunction={sortFunction} > debugId="local-sortFunction-single-sorting-example-with-local-data-example" columns={columns} columnDefaultWidth={220} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', lastName: 'Klein', country: 'India', city: 'Unnao', age: 24, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', lastName: 'Runolfsson', country: 'Mexico', city: 'Cuitlahuac', age: 46, currency: 'USD', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', lastName: 'McGlynn', country: 'United Arab Emirates', city: 'Fujairah', age: 54, currency: 'JPY', preferredLanguage: 'Go', stack: 'frontend', canDesign: 'yes', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', lastName: 'McLaughlin', country: 'Mexico', city: 'Tlacolula de Matamoros', age: 43, currency: 'CHF', preferredLanguage: 'Rust', stack: 'backend', canDesign: 'no', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', lastName: 'Harber', country: 'France', city: 'Persan', age: 23, currency: 'EUR', preferredLanguage: 'Go', stack: 'backend', canDesign: 'yes', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', lastName: 'Schroeder', country: 'United States', city: 'Hays', age: 34, currency: 'EUR', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', lastName: 'Mills', country: 'Mexico', city: 'Nuevo Casas Grandes', age: 33, currency: 'AUD', preferredLanguage: 'JavaScript', stack: 'frontend', canDesign: 'no', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', lastName: 'Hayes', country: 'Sweden', city: 'Delsbo', age: 22, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'full-stack', canDesign: 'yes', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', lastName: 'Boyle', country: 'Germany', city: 'Bad Camberg', age: 11, currency: 'GBP', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', lastName: 'Deckow', country: 'Canada', city: 'Raymore', age: 31, currency: 'EUR', preferredLanguage: 'Rust', stack: 'frontend', canDesign: 'yes', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` --- # Theming > Read our docs on the available themes and how you can customize the look and feel of InfiniteTable for React. Canonical page: https://infinite-table.com/docs/learn/theming/ `` ships with a CSS file that you need to import in your codebase to make the component look as intended. ```ts import '@infinite-table/infinite-react/index.css'; ``` This root CSS file includes the `"default"` theme. The other available themes are - `shadcn` - `minimalist` - `ocean` - `balsam` and if you want to use any of them, you have to import their respective CSS file explicitly: ```ts import '@infinite-table/infinite-react/theme/shadcn.css' import '@infinite-table/infinite-react/theme/balsam.css' import '@infinite-table/infinite-react/theme/minimalist.css' import '@infinite-table/infinite-react/theme/ocean.css' ``` Each theme CSS file includes both the **`light`** and the **`dark`** modes. Version `6.2.0` is the first version where the root CSS file (`@infinite-table/infinite-react/index.css`) doesn't include all the themes. Previous to this version, simply importing the root CSS file gave you access to all available themes. Splitting each theme into a dedicated CSS file helps reduce the bundle size for our users, as most people will only use one theme for `` in their apps. ## Applying a theme The following themes are currently available: - `default` - applied by default, no special configuration needed. It's included in the root CSS you need to import from `@infinite-table/infinite-react/index.css` - `balsam` - `minimalist` - `ocean` - `shadcn` - for this theme to correctly show up, make sure the shadcn CSS vars are available on page - see [shadcn theming](https://ui.shadcn.com/docs/theming) for details To apply a theme (except the default one), you have to set the className `"infinite-theme-name--THEME_NAME"` to any parent element of the `` component (or even on the component itself). You will want to apply the theme name and theme mode classNames to the same element, so you'll end up with a className like `"infinite-theme-name--minimalist infinite-theme-mode--dark"`. ```tsx title="Applying the minimalist theme with dark mode explicitly" ``` Example configured with `minimalist` theme and `dark` mode by default. ```tsx live title="Theme switching demo - defaults to minimalist theme in dark mode" size="md" viewMode="preview" files="theme-switching-minimalist-theme-default-example.page.tsx,columns.ts" ``` ## Theme mode - light or dark At runtime, the `light` or `dark` mode is applied based on the user OS settings for the [preferred color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme). To explicitly apply the light mode, apply the className `"infinite-theme-mode--light"` to any parent element of the `` component. To explicitly apply the dark mode, apply the className `"infinite-theme-mode--dark"` to any parent element of the `` component. ```tsx title="Explicitly applying light mode via container className"
``` If instead you specify a `infinite-theme-mode--dark` CSS className, the dark mode will be applied ```tsx title="Explicitly applying dark theme via container className"
``` Example configured with `default` theme and `light` mode by default. ```tsx live title="Theme switching demo - defaults to light theme" size="md" viewMode="preview" files="theme-switching-example.page.tsx,columns.ts" ``` If you don't explicitly have a `infinite-theme-mode--light` or `infinite-theme-mode--dark` ancestor, `InfiniteTable` will use the browser/OS preference (via `@media (prefers-color-scheme: ...)`) to apply the dark or light theme. ## Available themes ### Default theme The `default` theme is applied when you don't specify any explicit theme by default. ### Minimalist theme The `minimalist` theme is inspired from minimalistic designs and is a good choice if you want to keep the UI simple and clean. --- # CSS Variables > Reference list of CSS variables that can be used to style the Infinite Table for React Canonical page: https://infinite-table.com/docs/learn/theming/css-variables Below you can find the complete list of CSS variables that can be used to style the component. {/* START VARS */} ### Accent color Brand-specific accent color. This probably needs override to match your app. ```css --infinite-accent-color ``` ### Success color ```css --infinite-success-color ``` ### Error color ```css --infinite-error-color ``` ### Color The text color inside the component ```css --infinite-color ``` ### Space 0 ```css --infinite-space-0 ``` ### Space 1 ```css --infinite-space-1 ``` ### Space 2 ```css --infinite-space-2 ``` ### Space 3 ```css --infinite-space-3 ``` ### Space 4 ```css --infinite-space-4 ``` ### Space 5 ```css --infinite-space-5 ``` ### Space 6 ```css --infinite-space-6 ``` ### Space 7 ```css --infinite-space-7 ``` ### Space 8 ```css --infinite-space-8 ``` ### Space 9 ```css --infinite-space-9 ``` ### Space 10 ```css --infinite-space-10 ``` ### Font size 0 ```css --infinite-font-size-0 ``` ### Font size 1 ```css --infinite-font-size-1 ``` ### Font size 2 ```css --infinite-font-size-2 ``` ### Font size 3 ```css --infinite-font-size-3 ``` ### Font size 4 ```css --infinite-font-size-4 ``` ### Font size 5 ```css --infinite-font-size-5 ``` ### Font size 6 ```css --infinite-font-size-6 ``` ### Font size 7 ```css --infinite-font-size-7 ``` ### Font family ```css --infinite-font-family ``` ### Min height ```css --infinite-min-height ``` ### Border radius ```css --infinite-border-radius ``` ### Focus outline ```css --infinite-focus-outline ``` ### Background The background color for the whole component. Overriden in the `dark` theme. ```css --infinite-background ``` ### Icon size ```css --infinite-icon-size ``` ### Grouping toolbar color ```css --infinite-grouping-toolbar-color ``` ### Grouping toolbar background ```css --infinite-grouping-toolbar-background ``` ### Grouping toolbar reject background ```css --infinite-grouping-toolbar-reject-background ``` ### Grouping toolbar active background ```css --infinite-grouping-toolbar-active-background ``` ### Grouping toolbar active background alpha ```css --infinite-grouping-toolbar-active-background-alpha ``` ### Grouping toolbar padding ```css --infinite-grouping-toolbar-padding ``` ### Grouping toolbar border ```css --infinite-grouping-toolbar-border ``` ### Grouping toolbar reject border ```css --infinite-grouping-toolbar-reject-border ``` ### Grouping toolbar gap ```css --infinite-grouping-toolbar-gap ``` ### Grouping toolbar item border ```css --infinite-grouping-toolbar-item-border ``` ### Grouping toolbar item border radius ```css --infinite-grouping-toolbar-item-border-radius ``` ### Grouping toolbar item active border ```css --infinite-grouping-toolbar-item-active-border ``` ### Grouping toolbar item background ```css --infinite-grouping-toolbar-item-background ``` ### Grouping toolbar item active background ```css --infinite-grouping-toolbar-item-active-background ``` ### Grouping toolbar item active background alpha ```css --infinite-grouping-toolbar-item-active-background-alpha ``` ### Load mask padding The padding used for the content inside the LoadMask. ```css --infinite-load-mask-padding ``` ### Load mask color ```css --infinite-load-mask-color ``` ### Load mask text background ```css --infinite-load-mask-text-background ``` ### Load mask overlay background ```css --infinite-load-mask-overlay-background ``` ### Load mask overlay opacity ```css --infinite-load-mask-overlay-opacity ``` ### Load mask border radius ```css --infinite-load-mask-border-radius ``` ### Header background Background color for the header. Defaults to [`--infinie-header-cell-background`](#header-cell-background). Overriden in the `dark` theme. ```css --infinite-header-background ``` ### Header color The text color inside the header. Overriden in the `dark` theme. ```css --infinite-header-color ``` ### Column header height The height of the column header. ```css --infinite-column-header-height ``` ### Header cell background Background for header cells. Overriden in the `dark` theme. ```css --infinite-header-cell-background ``` ### Header cell hover background ```css --infinite-header-cell-hover-background ``` ### Header cell border ```css --infinite-header-cell-border ``` ### Header cell padding ```css --infinite-header-cell-padding ``` ### Header cell padding x ```css --infinite-header-cell-padding-x ``` ### Header cell padding y ```css --infinite-header-cell-padding-y ``` ### Header cell icon size ```css --infinite-header-cell-icon-size ``` ### Header cell menu icon line width ```css --infinite-header-cell-menu-icon-line-width ``` ### Header cell sort icon margin ```css --infinite-header-cell-sort-icon-margin ``` ### Header cell border right ```css --infinite-header-cell-border-right ``` ### Resize handle active area width The width of the area you can hover over in order to grab the column resize handle. Defaults to `20px`. The purpose of this active area is to make it easier to grab the resize handle. ```css --infinite-resize-handle-active-area-width ``` ### Resize handle width The width of the colored column resize handle that is displayed on hover and on drag. Defaults to `2px` ```css --infinite-resize-handle-width ``` ### Resize handle hover background The color of the column resize handle - the resize handle is the visible indicator that you see when hovering over the right-edge of a resizable column. Also visible on drag while doing a column resize. ```css --infinite-resize-handle-hover-background ``` ### Resize handle constrained hover background The color of the column resize handle when it has reached a min/max constraint. ```css --infinite-resize-handle-constrained-hover-background ``` ### Filter operator padding x ```css --infinite-filter-operator-padding-x ``` ### Filter editor padding x ```css --infinite-filter-editor-padding-x ``` ### Filter editor margin x ```css --infinite-filter-editor-margin-x ``` ### Filter operator padding y ```css --infinite-filter-operator-padding-y ``` ### Filter editor padding y ```css --infinite-filter-editor-padding-y ``` ### Filter editor margin y ```css --infinite-filter-editor-margin-y ``` ### Filter editor background ```css --infinite-filter-editor-background ``` ### Filter editor border ```css --infinite-filter-editor-border ``` ### Filter editor focus border color ```css --infinite-filter-editor-focus-border-color ``` ### Filter editor border radius ```css --infinite-filter-editor-border-radius ``` ### Filter editor color ```css --infinite-filter-editor-color ``` ### Active cell indicator inset ```css --infinite-active-cell-indicator-inset ``` ### Flashing duration ```css --infinite-flashing-duration ``` ### Flashing animation name ```css --infinite-flashing-animation-name ``` ### Flashing overlay z index ```css --infinite-flashing-overlay-z-index ``` ### Flashing background ```css --infinite-flashing-background ``` ### Flashing up background ```css --infinite-flashing-up-background ``` ### Flashing down background ```css --infinite-flashing-down-background ``` ### Cell padding ```css --infinite-cell-padding ``` ### Cell border width ```css --infinite-cell-border-width ``` ### Cell border color ```css --infinite-cell-border-color ``` ### Cell border Specifies the border for cells. Overriden in the `dark` theme - eg: `1px solid #2a323d` ```css --infinite-cell-border ``` ### Cell border left ```css --infinite-cell-border-left ``` ### Cell border right ```css --infinite-cell-border-right ``` ### Cell border top ```css --infinite-cell-border-top ``` ### Cell border invisible ```css --infinite-cell-border-invisible ``` ### Cell border radius ```css --infinite-cell-border-radius ``` ### Column reorder effect duration ```css --infinite-column-reorder-effect-duration ``` ### Pinned cell border ```css --infinite-pinned-cell-border ``` ### Horizontal layout column reorder disabled page opacity ```css --infinite-horizontal-layout-column-reorder-disabled-page-opacity ``` ### Cell color Text color inside rows. Defaults to `currentColor` Overriden in `dark` theme. ```css --infinite-cell-color ``` ### Selected cell background The background for selected cells, when cell selection is enabled. If not specified, it will default to `var(--infinite-active-cell-background)`. ```css --infinite-selected-cell-background ``` ### Selected cell background default ```css --infinite-selected-cell-background-default ``` ### Selected cell background alpha The opacity of the background color for the selected cell. If not specified, it will default to the value for `var(--infinite-active-cell-background-alpha)` ```css --infinite-selected-cell-background-alpha ``` ### Selected cell background alpha table unfocused The opacity of the background color for the selected cell, when the table is unfocused. If not specified, it will default to `var(--infinite-active-cell-background-alpha--table-unfocused)`. ```css --infinite-selected-cell-background-alpha--table-unfocused ``` ### Selected cell border color The color for border of the selected cell (when cell selection is enabled). Defaults to `var(--infinite-active-cell-border-color)`. ```css --infinite-selected-cell-border-color ``` ### Selected cell border width The width of the border for the selected cell. Defaults to `var(--infinite-active-cell-border-width)`. ```css --infinite-selected-cell-border-width ``` ### Selected cell border style The style of the border for the selected cell (eg: 'solid', 'dashed', 'dotted') - defaults to 'dashed'. Defaults to `var(--infinite-active-cell-border-style)`. ```css --infinite-selected-cell-border-style ``` ### Selected cell border Specifies the border for the selected cell. Defaults to `var(--infinite-selected-cell-border-width) var(--infinite-selected-cell-border-style) var(--infinite-selected-cell-border-color)`. ```css --infinite-selected-cell-border ``` ### Active cell background alpha The opacity of the background color for the active cell (when cell keyboard navigation is enabled). Eg: 0.25 If `activeBackground` is not explicitly defined (this is the default), the background color of the active cell is the same as the border color (`activeBorderColor`), but with this modified opacity. If `activeBorderColor` is also not defined, the accent color will be used. This is applied when the component has focus. ```css --infinite-active-cell-background-alpha ``` ### Active cell background alpha table unfocused Same as the above, but applied when the component does not have focus. ```css --infinite-active-cell-background-alpha--table-unfocused ``` ### Active cell background The background color of the active cell. If not specified, it will default to `activeBorderColor` with the opacity of `activeBackgroundAlpha`. If `activeBorderColor` is not specified, it will default to the accent color, with the same opacity as mentioned. However, specify this to explicitly override the default. ```css --infinite-active-cell-background ``` ### Active cell background default ```css --infinite-active-cell-background-default ``` ### Active cell border color The color for border of the active cell (when cell keyboard navigation is enabled). ```css --infinite-active-cell-border-color ``` ### Active cell border width The width of the border for the active cell. ```css --infinite-active-cell-border-width ``` ### Active cell border style The style of the border for the active cell (eg: 'solid', 'dashed', 'dotted') - defaults to 'dashed'. ```css --infinite-active-cell-border-style ``` ### Active cell border Specifies the border for the active cell. Defaults to `var(--infinite-active-cell-border-width) var(--infinite-active-cell-border-style) var(--infinite-active-cell-border-color)`. ```css --infinite-active-cell-border ``` ### Selection checkbox margin inline ```css --infinite-selection-checkbox-margin-inline ``` ### Expand collapse icon color ```css --infinite-expand-collapse-icon-color ``` ### Menu background ```css --infinite-menu-background ``` ### Menu color ```css --infinite-menu-color ``` ### Menu separator color ```css --infinite-menu-separator-color ``` ### Menu padding ```css --infinite-menu-padding ``` ### Menu cell padding vertical ```css --infinite-menu-cell-padding-vertical ``` ### Menu cell padding horizontal ```css --infinite-menu-cell-padding-horizontal ``` ### Menu cell margin vertical ```css --infinite-menu-cell-margin-vertical ``` ### Menu item disabled background ```css --infinite-menu-item-disabled-background ``` ### Menu item active background ```css --infinite-menu-item-active-background ``` ### Menu item active opacity ```css --infinite-menu-item-active-opacity ``` ### Menu item pressed opacity ```css --infinite-menu-item-pressed-opacity ``` ### Menu item pressed background ```css --infinite-menu-item-pressed-background ``` ### Menu item disabled opacity ```css --infinite-menu-item-disabled-opacity ``` ### Menu border radius ```css --infinite-menu-border-radius ``` ### Menu shadow color ```css --infinite-menu-shadow-color ``` ### Rowdetail background ```css --infinite-rowdetail-background ``` ### Rowdetail padding ```css --infinite-rowdetail-padding ``` ### Rowdetail grid height ```css --infinite-rowdetail-grid-height ``` ### Row background Background color for rows. Defaults to [`--infinite-background`](#background). Overriden in `dark` theme. ```css --infinite-row-background ``` ### Row odd background Background color for odd rows. Even rows will use [`--infinite-row-background`](#row-background). Overriden in `dark` theme. ```css --infinite-row-odd-background ``` ### Row disabled background ```css --infinite-row-disabled-background ``` ### Row odd disabled background Background color for disabled rows. For setting the background of disabled even rows, use [`--infinite-row-disabled-background`](#row-disabled-background). ```css --infinite-row-odd-disabled-background ``` ### Row selected background ```css --infinite-row-selected-background ``` ### Row disabled opacity Opacity for disabled rows. Defaults to 0.5 ```css --infinite-row-disabled-opacity ``` ### Active row background The background color of the active row. Defaults to the value of `var(--infinite-active-cell-background)`. However, specify this to explicitly override the default. ```css --infinite-active-row-background ``` ### Active row border color The border color for the active row. Defaults to the value of `var(--infinite-active-cell-border-color)`. ```css --infinite-active-row-border-color ``` ### Active row border width The width of the border for the active row. Defaults to the value of `var(--infinite-active-cell-border-width)`. ```css --infinite-active-row-border-width ``` ### Active row border style The style of the border for the active row (eg: 'solid', 'dashed', 'dotted') - defaults to the value of `var(--infinite-active-cell-border-style)`, which is `dashed` by default. ```css --infinite-active-row-border-style ``` ### Active row border Specifies the border for the active row. Defaults to `var(--infinite-active-row-border-width) var(--infinite-active-row-border-style) var(--infinite-active-row-border-color)`. ```css --infinite-active-row-border ``` ### Active row background alpha The opacity of the background color for the active row (when row keyboard navigation is enabled). When you explicitly specify `--infinite-active-row-background`, this variable will not be used. Instead, this variable is used when the active row background uses the color of the active cell (border). This is applied when the component has focus. Defaults to the value of `var(--infinite-active-cell-background-alpha)`. ```css --infinite-active-row-background-alpha ``` ### Active row background alpha table unfocused Same as the above, but applied when the component does not have focus. When you explicitly specify `--infinite-active-row-background`, this variable will not be used. Instead, this variable is used when the active row background uses the color of the active cell (border). Defaults to the value of `var(--infinite-active-cell-background-alpha--table-unfocused)`. ```css --infinite-active-row-background-alpha--table-unfocused ``` ### Row hover background Background color for rows, on hover. Overriden in the `dark` theme. ```css --infinite-row-hover-background ``` ### Row selected hover background ```css --infinite-row-selected-hover-background ``` ### Row selected disabled background ```css --infinite-row-selected-disabled-background ``` ### Group row background ```css --infinite-group-row-background ``` ### Group row column nesting ```css --infinite-group-row-column-nesting ``` ### Row pointer events while scrolling ```css --infinite-row-pointer-events-while-scrolling ``` {/* END VARS */} --- # Using Tree Data > Learn how to use the Tree DataGrid to display tree data Canonical page: https://infinite-table.com/docs/learn/tree-grid/overview Starting with version `6.0.0`, Infinite Table has support for displaying tree data. To show tree data, you have to use: - the `` instead of `` component - the `` instead of `` component. Under the hood, those specialized components have better typing support for tree data, which will make it easier to work with them. To specify which column will have the expand/collapse icon, set the [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) prop to `true` for that column. **Example: Basic TreeGrid example** ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', renderTreeIcon: true, header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', mimeType: 'text/plain', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` Throughout the docs for the TreeGrid, we will use an example data source that illustrates file system data, as that will be familiar to most people. ## Terminology When referring to rows in the TreeGrid, we'll prefer to use the term `"node"` instead of "row". So whenever you see `"node"` in the docs, you should know that it refers to a TreeGrid configuration of Infinite Table. Also in the context of the TreeGrid, we'll use the term `"node path"` instead of row id. The `"node path"` is the array with the ids of all the parent nodes leading down to the current node. The node path includes the id of the current node. ```tsx {2} title="Node path vs row id" const data = [ { id: '1', name: 'Documents', // path: ['1'] children: [ { id: '10', name: 'Private', // path: ['1', '10'] children: [ { id: '100', name: 'Report.docx' }, // path: ['1', '10', '100'] { id: '101', name: 'Vacation.docx' },// path: ['1', '10', '101'] ], }, ] }, { id: '2', name: 'Downloads', // path: ['2'] children: [ { id: '20', name: 'cat.jpg', // path: ['2', '20'] }, ], }, ]; ``` It's important to understand node paths, as that will be the primary way you'll interact with the TreeGrid/TreeDataSource. For the initial version of the TreeGrid, it's safer if your node ids are unique globally, but as we refine the TreeGrid, it will be safe to use ids unique only within a node children (so unique relative to siblings). ### Parent vs leaf nodes Nodes with an array for their `nodesKey` property (defaults to `"children"`) are considered parent nodes. All other nodes are leaf nodes. When using the [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) type, you can check for `isTreeNode` to determine if you're in a tree scenario. Also use the `isParentNode` property to check if a node is a parent node or not. ## Data format for the TreeDataSource When using the `` component, the data you specify in your `[`dataSource`](https://infinite-table.com/docs/reference/datasource-props/index.md#dataSource)` should resolve to a nested array - with the `nodesKey` containing the child items for each tree node. ```tsx {2} title="Using the nodesKey prop to specify where the node children are" ``` With the `nodesKey` set to `"children"`, the `` will look for the `children` property on each item in the data array, and use that to determine the child nodes for each tree node. Nodes without a `"children"` property are assumed to be leaf nodes. ```tsx {2} title="Nested data structure for the TreeDataSource component" const dataSource = [ { id: '1', name: 'Documents', children: [ { id: '10', name: 'Private', children: [ { id: '100', name: 'Report.docx', }, { id: '101', name: 'Vacation.docx', }, ], }, ], }, { id: '2', name: 'Downloads', children: [] // will be a parent node, with no children }, ]; ``` ## Tree collapse and expand state The `` component allows you to fully configure & control the collapse and expand state of the tree nodes, via the [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState)/[`defaultTreeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeExpandState) props. By default, if no expand state is specified, the tree will be rendered as fully expanded. However, you can choose to specify the expand state with a default value and then with specific values for node paths (or node ids) **Example: Using controlled tree expand state** ```ts import { InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeExpandState, setTreeExpandState] = useState({ defaultExpanded: true, collapsedPaths: [['1', '10'], ['3']], }); return ( <>
Current tree expand state:
{JSON.stringify(treeExpandState, null, 2)}
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` When using node paths for [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState), the object should have the following properties: - `defaultExpanded`: `boolean` - whether the tree nodes are expanded by default or not. - `collapsedPaths`: `string[]` - when `defaultExpanded` is `true`, this is a mandatory prop. - `expandedPaths`: `string[]` - when `defaultExpanded` is `false`, this is a mandatory prop. ```tsx title="Example of treeExpandState with node paths" const treeExpandState = { defaultExpanded: true, collapsedPaths: [ ['1', '10'], ['2', '20'], ['5'] ], expandedPaths: [ ['1', '4'], ['5','nested node in 5'], ], }; ``` As seen above, you can have a node specifically collapsed while other child nodes specifically expanded. So you can combine the expanded/collapsed paths to achieve very complex tree layouts, which can be restored later. ## Working with horizontal layout The [`wrapRowsHorizontally`](https://infinite-table.com/docs/reference/infinite-table-props.md#wrapRowsHorizontally) prop can be used to enable horizontal layout, just like non-tree DataGrids. **Example: TreeGrid with horizontal layout** ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', renderTreeIcon: true, header: 'Name' }, type: { field: 'type', header: 'Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size' }, }; export default function App() { const [wrapRowsHorizontally, setWrapRowsHorizontally] = useState(false); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', mimeType: 'text/plain', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, { id: '311', name: 'WinterVacation.mp4', sizeInKB: 245, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, { id: '312', name: 'SummerVacation.mp4', sizeInKB: 1259, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` --- # Managing the tree column and expand/collapse icon > Learn how to render the tree expand/collapse icon and manage the tree column Canonical page: https://infinite-table.com/docs/learn/tree-grid/tree-column When rendering a tree, you have to use the `` component instead of ``. The `` component is simply an `` component with some props removed - those don't make sense for tree scenarios. By default no tree column is rendered. To specify the tree column, you have to to set the [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) prop to `true` for your column of choice. ```tsx {3} title="Specifying the tree column" const columns: Record> = { name: { renderTreeIcon: true, field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; ``` This is very similar to how you specify the [selection column for multi-select configurations](https://infinite-table.com/docs/learn/selection/row-selection.md#using-a-selection-checkbox). **Example: Specifying the tree column** ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useMemo, useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const allColumns: Record> = { name: { field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeIcon, setTreeIcon] = useState('name'); const columns = useMemo(() => { const cols = { ...allColumns }; cols[treeIcon] = { ...cols[treeIcon], renderTreeIcon: true, }; return cols; }, [treeIcon]); return ( <>

Select the tree column

); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ## Customizing the expand/collapse icon Using the [column.renderTreeIcon=true](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) is obviously not enough to customize the expand/collapse icon. This prop can also be a function that returns a React node. With the default value of `true` for [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon), an icon will be rendered only for parent nodes. If you want to render an icon for all nodes, specify a function (and differentiate between parent and leaf nodes), and it will be called regardless of whether the node is a parent or a leaf. **Example: Customizing the expand/collapse icon** ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useMemo, useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const allColumns: Record> = { name: { field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeIcon, setTreeIcon] = useState('name'); const columns = useMemo(() => { const cols = { ...allColumns }; cols[treeIcon] = { ...cols[treeIcon], renderTreeIcon: ({ rowInfo, toggleCurrentTreeNode }) => (
{rowInfo.isParentNode ? (rowInfo.nodeExpanded ? '👇' : '👉') : '🔴'}
), }; return cols; }, [treeIcon]); return ( <>

Select the tree column

); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 108, type: 'file', extension: 'txt', mimeType: 'text/plain', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` --- # Managing the tree expand/collapse state > Learn how to control which tree nodes are expanded or collapsed Canonical page: https://infinite-table.com/docs/learn/tree-grid/tree-expand-and-collapse-state By default, the tree will be rendered with all nodes expanded. This is fine for basic use cases, but as soon as you go into more complex scenarios, you will want to control which nodes are expanded or collapsed. This is easy to achieve via the [`defaultTreeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeExpandState) prop. This is an uncontrolled prop and allows you to initially specify the expand/collapse state of the tree - all subsequent user updates will result in the tree state being updated to match the UI actions. **Example: Specifying an initial tree expand state** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; const defaultTreeExpandState: TreeExpandStateValue = { defaultExpanded: true, collapsedPaths: [ ['1', '10'], ['3', '31'], ], expandedPaths: [['3']], }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ## Understanding the tree expand state You can specify the expand/collapse state of the tree in two ways: 1. With node paths (recommended) When using node paths, the object should have the following properties: - `defaultExpanded`: `boolean` - whether the tree nodes are expanded by default or not. - `collapsedPaths`: `string[]` - when `defaultExpanded` is `true`, this is a mandatory prop. - `expandedPaths`: `string[]` - when `defaultExpanded` is `false`, this is a mandatory prop. ```tsx title="Example of treeExpandState with node paths" const treeExpandState = { defaultExpanded: true, collapsedPaths: [ ['1', '10'], ['2', '20'], ['5'] ], expandedPaths: [ ['1', '4'], ['5','nested node in 5'], ], }; ``` 2. With node ids When using node ids, the object should have the following properties: - `defaultExpanded`: `boolean` - whether the tree nodes are expanded by default or not. - `collapsedIds`: `string[]` - when `defaultExpanded` is `true`, this is a mandatory prop. - `expandedIds`: `string[]` - when `defaultExpanded` is `false`, this is a mandatory prop. ```tsx title="Example of treeExpandState with node ids" const treeExpandState = { defaultExpanded: true, collapsedIds: ['1', '2', '5'], expandedIds: ['10', '20', 'nested node in 5'], }; ``` ## Reacting to user actions You can listen to the user interactions with the tree by using the [{`onTreeExpandStateChange(treeExpandState, {dataSourceApi, nodePath, nodeState})`}](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange) callback. This callback is called with the new tree state whenever the user expands or collapses a node. In addition to this callback, you can also use the following: - [{`onNodeExpand(nodePath, {dataSourceApi})`}](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeExpand) - [{`onNodeCollapse(nodePath, {dataSourceApi})`}](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeCollapse) The [`onNodeExpand`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeExpand) and [`onNodeCollapse`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeCollapse) callbacks are called when a node is expanded or collapsed, respectively - either via user interaction or by an API call. However, they will not be called when the [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) or [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) methods are called. ## Using controlled expand/collapse state If you want maximum control over the collapse/expand state, you should use the controlled [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState) prop. This will allow you to own the collapse/expand state entirely - but make sure you use the [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange) callback to react to user actions or API calls being made to update the tree state. **Example: Using controlled expand/collapse state** ```tsx import { InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeExpandState, setTreeExpandState] = useState({ defaultExpanded: true, collapsedPaths: [['1', '10'], ['3']], }); return ( <>
Current tree expand state:
{JSON.stringify(treeExpandState, null, 2)}
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` When using controlled [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState), you no longer need to use API calls. When you need to expand all nodes, simply set the [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState) to `{defaultExpanded: true, collapsedPaths: []}`. When you need to collapse all nodes, simply set the [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState) to `{defaultExpanded: false, expandedPaths: []}`. --- # Using & rendering tree icons > Learn how to customize the tree icons Canonical page: https://infinite-table.com/docs/learn/tree-grid/tree-icon-rendering To make a column render the tree icon, you have to set [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) to `true`. This will cause the default tree icon to be rendered for non-leaf nodes. ```tsx {4} title="Specifying the tree icon for a column" const columns: Record> = { name: { field: 'name', renderTreeIcon: true, }, type: { field: 'type' }, extension: { field: 'extension' }, size: { field: 'sizeInKB', type: 'number' }, }; ``` If you don't have [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) set, there will be no tree column to render the tree icon. **Example: Tree icon rendering** ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { renderTreeIcon: true, field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ## Customizing the tree icon There are multiple ways to customize the tree icon. First, you can very easily change the color of the icon. The color of the icon is controlled by the `--infinite-expand-collapse-icon-color` CSS variable, and defaults to `--infinite-accent-color`, but you can also set it to any other color you want. ```css title="Changing the color of the tree icon" .Infinite { --infinite-expand-collapse-icon-color: #6f6f6f; } ``` **Example: Customizing the tree icon color** ```tsx import { InfiniteTableColumn, InfiniteTableProps, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { CSSProperties } from 'react'; const domProps: InfiniteTableProps['domProps'] = { style: { // specify it here or in your CSS file '--infinite-expand-collapse-icon-color': '#6f6f6f', } as CSSProperties, }; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { renderTreeIcon: true, field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` If you want to go further, use a function for the `column.renderTreeIcon` property - the next section will go into more detail on this. ## Rendering a custom tree icon for both parent and leaf nodes When [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) is `true`, the tree icon will be rendered only for parent nodes. In your implementation of the `renderTreeIcon` function, you'll use the `rowInfo.nodeExpanded` property. Note that the property is only available for parent nodes, so you'll first have to use the `rowInfo.isParentNode` property as a TS discriminator to check if the node is a parent node. ```tsx title="Checking if the node is a parent node" const renderTreeIcon = ({ rowInfo }) => { if (!rowInfo.isParentNode) { // rowInfo.nodeExpanded not available here return ; } // it's now OK for TS to use rowInfo.nodeExpanded return }; ``` However when you specify a function, it will be called for both parent and leaf nodes (if you don't want an icon for leaf nodes, simply return `null`). This gives you maximum flexibility to icons. A common example is a file explorer, where you might want to render icons not only for folders, but also for files. **Example: Rendering a custom tree icon for both parent and leaf nodes** This example renders a custom tree icon and uses the `toggleCurrentTreeNode` function to toggle the node state when Clicked. `toggleCurrentTreeNode` is a property of the argument passed to the `renderTreeIcon` function. ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { CSSProperties } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; sizeInKB: number; children?: FileSystemNode[]; }; const renderTreeIcon: InfiniteTableColumn['renderTreeIcon'] = ({ rowInfo, toggleCurrentTreeNode, }) => { return rowInfo.isParentNode ? ( ) : ( ); }; const svgStyle: CSSProperties = { verticalAlign: 'middle', position: 'relative', top: '-1px', marginInline: '5px', }; const FileIcon = () => ( ); const FolderIcon = ({ onClick, open, }: { onClick: () => void; open: boolean; }) => { return ( {open ? ( ) : ( )} ); }; const columns: Record> = { name: { renderTreeIcon, field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` If you implement a custom [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) function for your column, you can still use the default tree icon. Use `renderBag.treeIcon` property in the JSX you return (the `renderBag` is available as a property of the `cellContext` argument of the `renderTreeIcon` function). --- # Using tree selection > Learn how to leverage the tree and specify the tree selection state Canonical page: https://infinite-table.com/docs/learn/tree-grid/tree-selection When using a tree grid, a common use-case is to allow users to select nodes, both parent and child nodes. The `` component allows you to specify an initial tree selection, via the `defaultTreeSelection` prop. When using [`defaultTreeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeSelection) or its controlled counterpart [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection), if no [`selectionMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode) is specified, the selection mode will default to `"multi-row"`. If you enable selection, don't forget to specify which column should render a selection checkbox, by using [renderSelectionCheckBox=true](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox). ```tsx title="Example of tree selection value with default selection set to false" import type { TreeSelectionValue } from '@infinite-table/infinite-react'; // Default selection is false, with some selected node paths: ['1'] and ['2', '20'] // however, node ['1', '10'] is deselected const treeSelectioDefaultDeselected: TreeSelectionValue = { defaultSelection: false, selectedPaths: [['1'], ['2', '20']], deselectedPaths: [['1', '10']], }; ``` ```tsx title="Example of tree selection value with default selection set to true" // Default selection is true, with some deselected node paths: ['2'] and ['3'] // however, inside ['3'], we have a selected node ['3','30','301'] const treeSelectionDefaultSelected: TreeSelectionValue = { defaultSelection: true, deselectedPaths: [['2'], ['3']], selectedPaths: [['3','30','301']], }; ``` **Example: Using default tree selection** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, TreeSelectionValue, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; const defaultTreeSelection: TreeSelectionValue = { defaultSelection: true, deselectedPaths: [ ['1', '10'], ['3', '31'], ], selectedPaths: [['3']], }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` When using [uncontrolled tree selection](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeSelection), the `` will manage the selection state internally, and will update it as a result of user actions. If you want to change the selection, you can use [the Tree API](https://infinite-table.com/docs/reference/tree-api/index.md) to do so: [`selectNode`](https://infinite-table.com/docs/reference/tree-api/index.md#selectNode), [`deselectNode`](https://infinite-table.com/docs/reference/tree-api/index.md#deselectNode), [`selectAll`](https://infinite-table.com/docs/reference/tree-api/index.md#selectAll), [`deselectAll`](https://infinite-table.com/docs/reference/tree-api/index.md#deselectAll), etc. ## Reacting to user actions To listen to selection changes, you can use the [`onTreeSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeSelectionChange) callback. This callback is called both when the user interacts with the grid, and when you use the [Tree API](https://infinite-table.com/docs/reference/tree-api/index.md) to change the selection. ## Using controlled tree selection When using the controlled [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection) prop, you have to make sure you update the tree selection via [`onTreeSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeSelectionChange). Controlled tree selection also gives you a more declarative way to manage the selection state. You no longer have to call [Tree API](https://infinite-table.com/docs/reference/tree-api/index.md) methods to change the selection. Simply pass a new tree selection state object to the [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection) prop and the tree grid will be updated accordingly. For example, if you want to select all nodes, set the [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection) prop to: ```tsx title="Tree selection value to show all nodes as selected" { defaultSelection: true, deselectedPaths: [], } ``` For deselecting all nodes, the value should be: ```tsx title="All nodes as deselected" { defaultSelection: false, selectedPaths: [], } ``` Using controlled tree selection also gives you an easy way to restore a previously saved tree selection at any point in time. **Example: Using controlled tree selection** ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, TreeSelectionValue, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeSelection, setTreeSelection] = useState({ defaultSelection: false, selectedPaths: [['1', '10'], ['3']], }); return ( <>
Current tree selection:
{JSON.stringify(treeSelection, null, 2)}
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` --- # Working with Data > Learn how to visualise and manage your data in new ways with Infinite Table Canonical page: https://infinite-table.com/docs/learn/working-with-data/ When working with data, you will mostly interact with the `` component, which is responsible for handling and managing the data and passing it down to the `` component, which is the rendering engine for the DataGrid. So we provide those two components (as named exports) inside `@infinite-table/infinite-react` package: - `` - our data-handling component - `` - our virtualized component The `` component is responsible for the data the management layer. Probably the most important prop for the `` component is the [`idProperty`](https://infinite-table.com/docs/reference/datasource-props/index.md#idProperty) prop. It specifies the property of the data object that is used as a unique identifier for data rows/items. ```tsx idProperty="id" data={[]} // or a Promise or function returning a Promise. /> ``` The `` is a generic React TypeScript component that can be bound to an array of items of the generic type. In this documentation, we'll use `DATA_TYPE` when referring to the generic type. Rarely, we'll use `T`. ```tsx > /> ``` Most of our examples in these docs have a `Developer` or `Employee` TypeScript data type used as the generic type for the `` component. ```tsx import { DataSource } from '@infinite-table/infinite-react'; type Employee = { id: string | number; name: string; salary: number; department: string; company: string; }; const employees: Employee[] = [ { id: 1, name: 'Bob', salary: 10_000, department: 'IT', company: 'Bobsons' }, { id: 2, name: 'Alice', salary: 20_000, department: 'IT', company: 'Bobsons', }, { id: 3, name: 'John', salary: 30_000, department: 'IT', company: 'Bobsons' }, ]; primaryKey={'id'} data={employees} />; ``` In the snippet above, we see 3 important details: 1. the component is bound to the `Employee` type 2. we use a `primaryKey` property - here it is `id`, but since the bound type is `Employee`, `primaryKey` is `keyof Employee` 3. we pass the `employees` array as the `data` property. The [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) prop can be either: - an array of the bound generic type - here `Employee[]` - a Promise tha resolves to an array like the above - a function that returns any of the above ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Employee = { id: string | number; name: string; salary: number; department: string; company: string; }; const employees: Employee[] = [ { id: 1, name: 'Bob', salary: 10_000, department: 'IT', company: 'Bobsons', }, { id: 2, name: 'Alice', salary: 20_000, department: 'IT', company: 'Bobsons', }, { id: 3, name: 'John', salary: 30_000, department: 'IT', company: 'Bobsons', }, { id: 4, name: 'Jane', salary: 35_000, department: 'Marketing', company: 'Janies', }, { id: 5, name: 'Mary', salary: 40_000, department: 'Marketing', company: 'Janies', }, ]; // simulate data-loading with a 1500ms delay const data = new Promise((resolve) => { setTimeout(() => { resolve(employees); }, 1500); }); export default function App() { return ( data={data} primaryKey="id"> debugId="basic-example" columnDefaultWidth={130} columns={columns} /> ); } const columns: Record> = { id: { field: 'id', type: 'number', defaultWidth: 80, }, name: { field: 'name', }, salary: { field: 'salary', type: 'number' }, department: { field: 'department', header: 'Dep.' }, company: { field: 'company' }, }; ``` ## Data Loading Strategies We're aware there are countless strategies for loading data - each with its own strengths. We decided we should focus on building what we do best, namely building virtualized components, so we encourage you to use your preferred data-fetching library/solution. This being said, we still provide you with the flexibility you need when using the ``, so here's what you can use for the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) prop of the component: - an array of the bound type - a Promise that resolves to an array of the bound type - a function that returns any of the above While you're loading the data, you can always render a loading indicator - pass the [`loading`](https://infinite-table.com/docs/reference/datasource-props/index.md#loading) prop into the component (along with [`loadingText`](https://infinite-table.com/docs/reference/infinite-table-props.md#loadingText) prop in the `` component if you want to customize the message). ### Using fetch For basic datasets, which have simple data requirements, using `fetch` is probably sufficient, so here is an example: **Example: Using fetch for remote data** ```ts files=["using-fetch-example.page.tsx","columns.ts"] ``` #### Re-fetching on change It's important to note you can re-fetch data by changing the reference you pass as the `data` prop to the `` component. Passing another [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function, will cause the component to re-execute the function and thus load new data. Alternatively, you can use the [`refetchKey`](https://infinite-table.com/docs/reference/datasource-props/index.md#refetchKey) prop to trigger a re-fetch - give it a new value (eg: use it as a counter, and increment it) and the component will re-fetch the data. **Example: Re-fetching data** ```ts files=["refetch-example.page.tsx","columns.ts"] ``` ## Live Updates You can update your data in real-time by using our [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md). Read more about how to use our API to update your data in real-time --- # Handling Date Objects > Learn how to display, manipulate and render dates with Infinite Table Canonical page: https://infinite-table.com/docs/learn/working-with-data/handling-dates InfiniteTable can handle dates just like any other data type - make sure you specify [type="date"](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) for date columns. If your date column does not specify a custom [formatter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) or [renderer](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue), by default the date will be formatted using the [`toLocaleDateString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString) method of the date object. For date columns, make sure you specify [column.type="date"](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type). This will ensure that the column is sorted correctly (as per the available [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes)) and that the default date formatting is applied. **Example: Using date objects** In this example, the `birthDate` column contains dates and we customized the way they are displayed. ```tsx const renderValue = ({ value }: { value: Date }) => { return {value.toISOString().split('T')[0]}; }; ``` If no custom `renderValue` was specified, the dates would have been formatted using the `Date.toLocaleDateString()` ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, type InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { birthDate: Date; id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, birthDate: { field: 'birthDate', header: 'Birth Date', // we need to specify the type of the column as "date" type: 'date', renderValue: ({ value }: { value: Date }) => { return {value.toISOString().split('T')[0]}; }, }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="dates-with-local-data-example" columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', birthDate: new Date(1997, 0, 1), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', birthDate: new Date(1993, 3, 10), currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', birthDate: new Date(1997, 10, 30), currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', birthDate: new Date(1990, 5, 20), currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', birthDate: new Date(1990, 3, 20), currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', birthDate: new Date(2002, 3, 20), currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', birthDate: new Date(1992, 11, 12), currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', birthDate: new Date(1990, 9, 5), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', birthDate: new Date(1990, 9, 15), currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', birthDate: new Date(1990, 4, 18), currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` ## Using date strings If your dates are not `instanceof Date` but strings or numbers (timestamps) then it's better not to use the [column.type="date"](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) but rather to specify a custom [column.type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) along with [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes). For the case when your dates are not actually dates, but date strings (the same applies to timestamps), you have to define your sorting function. ```tsx const sortTypes = { mydatestring: (a: string, b: string) => { // use your preferred date parsing library // to turn a string into date and then compare the two values return new Date(a).getTime() - new Date(b).getTime(); }, }; ``` When then pass the [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) to the `` component and configure our date column to be of type `"mydatestring"` (it should match the key you specified in your `sortTypes` definition). **Example: Using date strings** In this example, the `birthDate` column contains dates as strings, so we have to define a custom column.type and sort type. ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, DataSourceSortInfo, type InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { birthDate: string; id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, birthDate: { field: 'birthDate', header: 'Birth Date', type: 'datestring', defaultWidth: 150, }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const sortTypes = { datestring: (a: string, b: string) => { return new Date(a).getTime() - new Date(b).getTime(); }, }; const defaultSortInfo: DataSourceSortInfo = [ { field: 'birthDate', dir: -1, }, ]; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource} sortTypes={sortTypes} defaultSortInfo={defaultSortInfo} > debugId="date-strings-with-local-data-example" columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', birthDate: '1997-01-01', currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', birthDate: '1993-04-10', currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', birthDate: '1997-11-30', currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', birthDate: '1990-06-20', currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', // birthDate: new Date(1990, 3, 20), birthDate: '1990-04-20', currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', birthDate: '2002-04-20', currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', birthDate: '1992-12-12', currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', birthDate: '1990-10-05', currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', birthDate: '1990-10-15', currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', birthDate: '1990-05-18', currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` --- # Lazy Loading Canonical page: https://infinite-table.com/docs/learn/working-with-data/lazy-loading With `InfiniteTable` you can lazily load data on demand - loading data is triggered by the user scrolling to a certain visible row range. So when the user stopped scrolling (after [`scrollStopDelay`](https://infinite-table.com/docs/reference/infinite-table-props.md#scrollStopDelay) ms passed), the `DataSource` is loading the records that are in the viewport. Also, the table will render as if all the remote data is loaded into viewport - so the scroll height is correspondingly set. We call this `"lazy loading"`, and it needs to be enabled by specifying the [DataSource.lazyLoad](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad) prop. **Example: Lazy loading ungrouped and unpivoted data** ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, DataSourceData, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useMemo } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 100 }, salary: { field: 'salary', header: 'Salary' }, age: { field: 'age', header: 'Age' }, firstName: { field: 'firstName', header: 'First Name' }, preferredLanguage: { field: 'preferredLanguage', header: 'Preferred Language', }, lastName: { field: 'lastName', header: 'Last Name' }, country: { field: 'country', header: 'Country' }, city: { field: 'city', header: 'City' }, currency: { field: 'currency', header: 'Currency' }, stack: { field: 'stack', header: 'Stack' }, canDesign: { field: 'canDesign', header: 'Can Design' }, hobby: { field: 'hobby', header: 'Hobby' }, }; export default function App() { const lazyLoad = useMemo(() => ({ batchSize: 40 }), []); return ( data={dataSource} primaryKey="id" lazyLoad={lazyLoad} > debugId="simple-lazy-load-example" columns={columns} columnDefaultWidth={130} /> ); } const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, lazyLoadStartIndex, lazyLoadBatchSize, groupKeys = [], sortInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const startLimit: string[] = []; if (lazyLoadBatchSize && lazyLoadBatchSize > 0) { const start = lazyLoadStartIndex || 0; startLimit.push(`start=${start}`); startLimit.push(`limit=${lazyLoadBatchSize}`); } const args = [ ...startLimit, pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers10k-sql?` + args, ).then((r) => r.json()); }; ``` The [DataSource.lazyLoad](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad) prop can be either a boolean or an object with a `batchSize: number` property. If `batchSize` is not specified, it will load all records from the current row group (makes sense for grouped and/or pivoted data). For ungrouped and unpivoted data, make sure you set `batchSize` to a conveninent number. Simply specifying `lazyLoad=true` makes more sense for grouped (or/and pivoted) data, where you want to load all records from the current level at once. If you want configure it this way, new data will only be requested when a group row is expanded. For lazy loading to work, the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function in the `` component must return a Promise that resolves to an an object with `data` and `totalCount` properties. ```tsx { data: [ ... ], totalCount: 10000 } ``` The [DataSource.data](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function will be called with an object with the following properties: - `sortInfo` - details about current sorting state - `pivotBy` - an array that describes the current pivot state - `aggregationReducers` - an object with the aggregation to apply to the data - `groupBy` - array that specifies the current grouping information - `groupKeys` - an array of the current group keys (if grouping is enabled). This uniquely identifies the current group. - `lazyLoadStartIndex` - the index (in the total remote datasource) of the first record to be loaded - `lazyLoadBatchSize` - the number of records to be loaded in this batch Find out about server-side grouping Find out about server-side pivoting ## How lazy loading fetches data When lazy loading is enabled, and the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) changes (eg: user clicks on a column header), the DataGrid will discard current data and call the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function prop again, to fetch the new data. The same happens when the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) or [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) changes. This is done automatically by the component, and you don't need to do anything. **Example: Lazy loading grouped data** This demo lazily loads grouped data as the user scrolls down. Expand some groups to see the lazy loading in action. When the user stops scrolling, after [`scrollStopDelay`](https://infinite-table.com/docs/reference/infinite-table-props.md#scrollStopDelay) milliseconds, the DataGrid will fetch the next batch of data from the server. ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, DataSourceData, InfiniteTablePropColumns, DataSourceProps, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useMemo } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { country: { field: 'country', header: 'Country' }, id: { field: 'id', header: 'ID', defaultWidth: 100 }, salary: { field: 'salary', header: 'Salary', renderValue: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return ( <> Avg: {value} ); } return value; }, }, age: { field: 'age', header: 'Age' }, firstName: { field: 'firstName', header: 'First Name' }, preferredLanguage: { field: 'preferredLanguage', header: 'Preferred Language', }, lastName: { field: 'lastName', header: 'Last Name' }, city: { field: 'city', header: 'City' }, currency: { field: 'currency', header: 'Currency' }, stack: { field: 'stack', header: 'Stack' }, canDesign: { field: 'canDesign', header: 'Can Design' }, hobby: { field: 'hobby', header: 'Hobby' }, }; const groupBy: DataSourceProps['groupBy'] = [ { field: 'country', }, { field: 'stack', }, ]; const aggregationReducers: DataSourceProps['aggregationReducers'] = { salary: { field: 'salary', reducer: 'avg', }, }; export default function App() { const lazyLoad = useMemo(() => ({ batchSize: 40 }), []); return ( data={dataSource} primaryKey="id" groupBy={groupBy} lazyLoad={lazyLoad} aggregationReducers={aggregationReducers} > debugId="grouped-lazy-load-example" columns={columns} columnDefaultWidth={130} groupColumn={{ id: 'group-col', defaultSortable: false, }} groupRenderStrategy="single-column" /> ); } const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, lazyLoadStartIndex, lazyLoadBatchSize, groupRowsState, groupKeys = [], sortInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const startLimit: string[] = []; if (lazyLoadBatchSize && lazyLoadBatchSize > 0) { const start = lazyLoadStartIndex || 0; startLimit.push(`start=${start}`); startLimit.push(`limit=${lazyLoadBatchSize}`); } const args = [ ...startLimit, pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, `prefetchGroupKeys=${JSON.stringify(groupRowsState?.expandedRows || [])}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers10k-sql?` + args, ).then((r) => r.json()); }; ``` Batching also happens for groups - when a group is expanded, the DataGrid will fetch the first batch of data in the expanded group and then fetch additional batches as the user scrolls down. When scrolling goes beyound the group, the DataGrid is smart enough to request a batch of data from sibling groups. Lazy loading when grouping is enabled needs data for non-leaf rows to be in another format (as opposed to the format used for non-grouped data or for the non-grouped scenario). See example above for details. For more docs on this, read [Server side grouping with lazy loading](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md#server-side-grouping-with-lazy-loading). --- # Live Pagination > Live Pagination DataSource documentation and examples for Infinite Table DataGrid Canonical page: https://infinite-table.com/docs/learn/working-with-data/live-pagination `InfiniteTable` supports live pagination in its `DataSource` via the [`livePagination`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePagination) prop together with [`livePaginationCursor`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePaginationCursor) Specify `DataSource.livePagination=true` and provide a pagination cursor (a good cursor would be the id of the last item in the `DataSource`). In addition, you have to listen to [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange) which will be triggered with an object that contains the following properties: - `sortInfo` - information about the current sort state - `groupBy` - current grouping info - `livePaginationCursor` - the current pagination cursor When `dataParams` change (you will be notified via [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange)), you have to fetch new data using the cursor from `dataParams` object. Basically [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange) is triggered whenever props (and state) that affect the `DataSource` change - be it via sorting, filtering, live pagination, pivoting, etc. Below you can see a live pagination demo implemented in combination with [react-query](https://react-query.tanstack.com/). **Example: Live pagination - with react-query** ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, InfiniteTableColumn, DataSource, DataSourceSingleSortInfo, DataSourceDataParams, DataSourceLivePaginationCursorFn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useCallback } from 'react'; import { QueryClient, QueryClientProvider, useInfiniteQuery, keepPreviousData, } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, }); const emptyArray: Employee[] = []; export const columns: Record> = { id: { field: 'id' }, country: { field: 'country', }, city: { field: 'city' }, team: { field: 'team' }, department: { field: 'department' }, firstName: { field: 'firstName' }, lastName: { field: 'lastName' }, salary: { field: 'salary' }, age: { field: 'age' }, }; type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: number; department: string; team: string; salary: number; age: number; email: string; }; const PAGE_SIZE = 10; const dataSource = ({ sortInfo, livePaginationCursor = 0, }: { sortInfo: DataSourceSingleSortInfo | null; livePaginationCursor: number; }) => { return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${ sortInfo?.dir === 1 ? 'asc' : 'desc' }&_start=${livePaginationCursor}`, ) .then(async (r) => { const data = await r.json(); // we need the remote count, so we take it from headers const total = Number(r.headers.get('X-Total-Count')!); return { data, total }; }) .then(({ data, total }: { data: Employee[]; total: number }) => { const page = livePaginationCursor / PAGE_SIZE + 1; const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0); return { data, hasMore: total > PAGE_SIZE * page, page, prevPageCursor, nextPageCursor: prevPageCursor + data.length, }; }) .then( ( response, ): Promise<{ data: Employee[]; hasMore: boolean; page: number; nextPageCursor: number; prevPageCursor: number; }> => { return new Promise((resolve) => { setTimeout(() => { resolve(response); }, 150); }); }, ); }; const Example = () => { const [dataParams, setDataParams] = React.useState< Partial> >({ groupBy: [], sortInfo: undefined, livePaginationCursor: null, }); const { data, fetchNextPage: fetchNext, isFetchingNextPage, } = useInfiniteQuery({ initialPageParam: 0, queryKey: ['employees', dataParams.sortInfo, dataParams.groupBy], queryFn: ({ pageParam = 0 }) => { const params = { livePaginationCursor: pageParam, sortInfo: dataParams.sortInfo as DataSourceSingleSortInfo | null, }; return dataSource(params); }, placeholderData: keepPreviousData, getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0, getNextPageParam: (lastPage) => { const nextPageCursor = lastPage.hasMore ? lastPage.nextPageCursor : undefined; return nextPageCursor; }, select: (data) => { const flatData = data.pages.flatMap((x) => x.data); const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor; const result = { pages: flatData, pageParams: [nextPageCursor], }; return result; }, }); const onDataParamsChange = useCallback( (dataParams: DataSourceDataParams) => { const params = { groupBy: dataParams.groupBy, sortInfo: dataParams.sortInfo, livePaginationCursor: dataParams.livePaginationCursor, }; setDataParams(params); }, [], ); const [scrollTopId, setScrollTop] = React.useState(0); React.useEffect(() => { // when sorting changes, scroll to the top setScrollTop(Date.now()); }, [dataParams.sortInfo]); const fetchNextPage = () => { if (isFetchingNextPage) { return; } fetchNext(); }; React.useEffect(() => { fetchNextPage(); }, [dataParams.livePaginationCursor]); const livePaginationCursorFn: DataSourceLivePaginationCursorFn = useCallback(({ length }) => { return length; }, []); return ( primaryKey="id" // take the data from `data.pages`, // as returned from our react-query select function data={data?.pages || emptyArray} loading={isFetchingNextPage} onDataParamsChange={onDataParamsChange} livePagination livePaginationCursor={livePaginationCursorFn} > debugId="live-pagination-example" scrollTopKey={scrollTopId} columnDefaultWidth={200} columns={columns} /> ); }; function App() { return ( ); } export default App; ``` In the example above, play around and scroll the table and also make sure to try sorting (eg: sort by country or city). For demo purposes, the page size in the example above is small - it shows that `InfiniteTable` handles infinite pagination correctly by immediately triggering [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange) when there are not enough rows to fill the viewport. On the other hand, when there are many rows and there is a horizontal scrollbar, it triggers [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange) only when the user scrolls to the end of the table. It also handles the case when there is a vertical scrollbar and then the user resizes the viewport to make it bigger and no more vertical scrollbar is needed - again [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange) is triggered to request more rows. --- # Updating Data in Real-Time Canonical page: https://infinite-table.com/docs/learn/working-with-data/updating-data-in-realtime Real-Time updates of data are possible via the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md). In this page we explain some of the complexities and features involved. ## Getting a reference to the DataSource API Data Updates are related to the `DataSource` component, therefore make sure you use the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) for this. You can get a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) - either by using [the DataSource onReady](https://infinite-table.com/docs/reference/datasource-props/index.md#onReady) prop ```tsx const onReady = (dataSourceApi) => { // do something with the dataSourceApi }; ; ``` - or by using the [InfiniteTable onReady](https://infinite-table.com/docs/reference/infinite-table-props.md#onReady) prop. ```tsx const onReady = ({ api, dataSourceApi }) => { // note for InfiniteTable.onReady, you get back an object // with both the InfiniteTable API (the `api` property) // and the DataSource API (the `dataSourceApi` property) } ``` ## Updating Rows To update the data of a row, you need to know the `primaryKey` for that row and use the [`updateData`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateData) method of the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md). ```tsx {1,3} title="Updating_a_single_row_using_dataSourceApi.updateData" dataSourceApi.updateData({ // if the primaryKey is the "id" field, make sure to include it id: 1, // and then include any properties you want to update - in this case, the name and age name: 'Bob Blue', age: 35, }); ``` To update multiple rows, you need to pass the array of data items to the [`updateDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataArray) method. ```tsx {1,3,8} title="Updating_multiple_rows" dataSourceApi.updateDataArray([ { id: 1, // if the primaryKey is the "id" field, make sure to include it name: 'Bob Blue', age: 35, }, { id: 2, // primaryKey for this row name: 'Alice Green', age: 25, }, ]); ``` **Example: Live data updates with DataSourceApi.updateData** The DataSource has 10k items - use the **Start/Stop** button to see updates in real-time. In this example, we're updating 5 rows (in the visible viewport) every 30ms. The update rate could be much higher, but we're keeping it at current levels to make it easier to see the changes. ```ts import * as React from 'react'; import '@infinite-table/infinite-react/index.css'; import { DataSourceApi, InfiniteTable, InfiniteTableApi, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; reposCount: number; }; const dataSource = () => { return fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/developers10k-sql`) .then((r) => r.json()) .then((data: Developer[]) => { return data; }); }; export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } const CURRENCIES = ['USD', 'CAD', 'EUR']; const stacks = ['frontend', 'backend', 'fullstack']; const updateRow = (api: DataSourceApi, data: Developer) => { const getDelta = (num: number): number => Math.ceil(0.2 * num); const initialData = data; if (!initialData) { return; } const salaryDelta = getDelta(initialData?.salary); const reposCountDelta = getDelta(initialData?.reposCount); const newSalary = initialData.salary + getRandomInt(-salaryDelta, salaryDelta); const newReposCount = initialData.reposCount + getRandomInt(-reposCountDelta, reposCountDelta); const newData: Partial = { id: initialData.id, salary: newSalary, reposCount: newReposCount, currency: CURRENCIES[getRandomInt(0, CURRENCIES.length - 1)] || CURRENCIES[0], stack: stacks[getRandomInt(0, stacks.length - 1)] || stacks[0], age: getRandomInt(0, 100), }; api.updateData(newData); }; const ROWS_TO_UPDATE_PER_FRAME = 5; const UPDATE_INTERVAL_MS = 30; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', style: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return {}; } return { color: 'black', background: value > 80 ? 'tomato' : value > 60 ? 'orange' : value > 40 ? 'yellow' : value > 20 ? 'lightgreen' : 'green', }; }, }, salary: { field: 'salary', type: 'number', }, reposCount: { field: 'reposCount', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default function App() { const [running, setRunning] = React.useState(false); const [apis, onReady] = React.useState<{ api: InfiniteTableApi; dataSourceApi: DataSourceApi; }>(); const intervalIdRef = React.useRef(null); React.useEffect(() => { const { current: intervalId } = intervalIdRef; if (!running || !apis) { return clearInterval(intervalId); } intervalIdRef.current = setInterval(() => { const { dataSourceApi, api } = apis!; const { renderStartIndex, renderEndIndex } = api.getVerticalRenderRange(); const dataArray = dataSourceApi.getRowInfoArray(); const data = dataArray .slice(renderStartIndex, renderEndIndex) .map((x) => x.data as Developer); for (let i = 0; i < ROWS_TO_UPDATE_PER_FRAME; i++) { const row = data[getRandomInt(0, data.length - 1)]; if (row) { updateRow(dataSourceApi, row); } } return () => { clearInterval(intervalIdRef.current); intervalIdRef.current = null; }; }, UPDATE_INTERVAL_MS); }, [running, apis]); return ( data={dataSource} primaryKey="id"> debugId="realtime-updates-example" domProps={domProps} onReady={onReady} columnDefaultWidth={130} columnMinWidth={50} columns={columns} /> ); } ``` For updating multiple rows, use the [`updateDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataArray) method. When updating a row, the data object you pass to the `updateData` method needs to at least include the [`primaryKey`](https://infinite-table.com/docs/reference/datasource-props/index.md#primaryKey) field. Besides that field, it can include any number of properties you want to update for the specific row. ## Batching updates All the methods for updating/inserting/deleting rows exposed via the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) are batched by default. So you can call multiple methods on the same raf (requestAnimationFrame), and they will trigger a single render. All the function calls made in the same raf return the same promise, which is resolved when the data is persisted to the `DataSource` ```tsx title="Updates_made_on_the_same_raf_are_batched_together" const promise1 = dataSourceApi.updateData({ id: 1, name: 'Bob Blue', }); const promise2 = dataSourceApi.updateDataArray([ { id: 2, name: 'Alice Green' }, { id: 3, name: 'John Red' }, ]); promise1 === promise2; // true ``` ## Inserting Rows To insert a new row into the `DataSource`, you need to use the [`insertData`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertData) method. For inserting multiple rows at once, use the [`insertDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertDataArray) method. ```tsx title="Inserting_a_single_row" dataSourceApi.insertData( { id: 10, name: 'Bob Blue', age: 35, salary: 12_000, stack: 'frontend', //... }, { position: 'before', primaryKey: 2, }, ); ``` When you insert new data, as a second parameter, you have to provide an object that specifies the insert `position`. Valid values for the insert `position` are: - `start` | `end` - inserts the data at the beginning or end of the data source. In this case, no `primaryKey` is needed. ```tsx dataSourceApi.insertData({ ... }, { position: 'start'}) // or insert multiple items via dataSourceApi.insertDataArray([{ ... }, { ... }], { position: 'start'}) ``` - `before` | `after` - inserts the data before or after the data item that has the specified primary key. **In thise case, the `primaryKey` is required.** ```tsx {5,10} dataSourceApi.insertData( { /* ... all data properties here */ }, { position: 'before', primaryKey: 2 } ) // or insert multiple items via dataSourceApi.insertDataArray([{ ... }, { ... }], { position: 'after', primaryKey: 10 }) ``` **Example: Using dataSourceApi.insertData** Click any row in the table to make it the current active row, and then use the second button to add a new row after the active row. ```ts import * as React from 'react'; import '@infinite-table/infinite-react/index.css'; import { DataSourceApi, InfiniteTable, InfiniteTableApi, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; reposCount: number; }; export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } const CURRENCIES = ['USD', 'CAD', 'EUR']; const stacks = ['frontend', 'backend', 'fullstack']; let ID = 0; const firstNames = ['John', 'Jane', 'Bob', 'Alice', 'Mike', 'Molly']; const lastNames = ['Smith', 'Doe', 'Johnson', 'Williams', 'Brown', 'Jones']; const getRow = (count?: number): Developer => { return { id: ID++, firstName: ID === 1 ? 'ROCKY' : firstNames[getRandomInt(0, firstNames.length - 1)] + (count ? ` ${count}` : ''), lastName: lastNames[getRandomInt(0, firstNames.length - 1)], currency: CURRENCIES[getRandomInt(0, 2)], salary: getRandomInt(1000, 10000), preferredLanguage: 'JavaScript', stack: stacks[getRandomInt(0, 2)], canDesign: getRandomInt(0, 1) === 0 ? 'yes' : 'no', age: getRandomInt(20, 100), reposCount: getRandomInt(0, 100), }; }; const dataSource: Developer[] = [...Array(10)].map(getRow); const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', style: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return {}; } return { color: 'black', background: value > 80 ? 'tomato' : value > 60 ? 'orange' : value > 40 ? 'yellow' : value > 20 ? 'lightgreen' : 'green', }; }, }, salary: { field: 'salary', type: 'number', }, reposCount: { field: 'reposCount', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; const buttonStyle = { border: '2px solid magenta', color: 'var(--infinite-cell-color)', background: 'var(--infinite-background)', }; export default () => { const [apis, onReady] = React.useState<{ api: InfiniteTableApi; dataSourceApi: DataSourceApi; }>(); const [currentActivePrimaryKey, setCurrentActivePrimaryKey] = React.useState(''); return ( data={dataSource} primaryKey="id"> debugId="insert-example" domProps={domProps} onReady={onReady} columnDefaultWidth={130} columnMinWidth={50} columns={columns} keyboardNavigation="row" onActiveRowIndexChange={(rowIndex) => { if (apis) { const id = apis.dataSourceApi.getRowInfoArray()[rowIndex].id; setCurrentActivePrimaryKey(id); } }} /> ); }; ``` ### Adding rows In addition to the [`insertData`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertData) and [`insertDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertDataArray) methods, the `DataSource` also exposes the [`addData`](https://infinite-table.com/docs/reference/datasource-api/index.md#addData) and [`addDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#addDataArray) methods (same as insert with `position=end`). ## Deleting Rows To delete rows from the `DataSource` you either need to know the `primaryKey` for the row you want to delete, or you can pass the data object (or at least a partial that contains the `primaryKey`) for the row you want to delete. All the following methods are available via the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md): - [`removeData`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeData) - [`removeDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArray) - [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByPrimaryKey) - [`removeDataArrayByPrimaryKeys`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArrayByPrimaryKeys) --- # Reference Overview > Infinite Table Reference Overview Canonical page: https://infinite-table.com/docs/reference/ The Reference pages contain API documentation on every prop exposed in the `InfiniteTable` component and the related `DataSource` component. Go through the extensive list of props available on the `InfiniteTable` component. See the full list of props available on the `DataSource` component. ## API objects In addition, both those components expose an `API` object that can be used to imperatively interact with them. This is useful for very advanced use cases - as most of the time interacting with the component via its props declaratively will be enough. Explore the `InfiniteTable` API which allows advanced interactions with the component, like scrolling to a specific cell, selecting rows and more. Jump into the `DataSource` API, which allows advanced interactions with the data source, like editing & inserting data, fetching data, refreshing and more. Read more about the Selection API and how you can use it to change row and group selection. Find out more about the Column API and how you can use it to change column state. Jump into the `Tree` API, which allows advanced interactions with the tree data source - collapse/expand nodes, node updates and more. ## Hooks Infinite Table exposes a few custom hooks that can be used to customize the component and its behavior. Most of the hooks will be useful when you want to implement custom components for `InfiniteTable` - like custom cells, headers, cell editors, etc. Custom hooks allow you to use the `InfiniteTable` at full potential and customize it to your needs. ## Type Definitions Infinite Table exports quite a lot of TS type definitions. See the dedicated page for guides and explanations to help you use them effectively. We export our type definitions in case you ever need them. In many cases, you won't need to import them explicitly, but you might find them useful in more advaced scenarios. --- # Infinite Table API Canonical page: https://infinite-table.com/docs/reference/api/ When rendering the `InfiniteTable` component, you can get access to the API by getting it from the [`onReady`](https://infinite-table.com/docs/reference/infinite-table-props.md#onReady) callback prop. ```tsx {2} const onReady = ( {api, dataSourceApi}: { api: InfiniteTableApi, dataSourceApi: DataSourceApi }) => { // api is accessible here // you may want to store a reference to it in a ref or somewhere in your app state } columns={[...]} onReady={onReady} /> ``` For API on row/group selection, see the [Selection API page](https://infinite-table.com/docs/reference/selection-api). See the [Infinite Table Cell Selection API page](https://infinite-table.com/docs/reference/cell-selection-api/index.md) for the cell selection API. See the [Infinite Table Column API page](https://infinite-table.com/docs/reference/column-api/index.md) for the column API. See the [Infinite Table Keyboard Navigation API page](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md) for the keyboard navigation API. See the [Infinite Table Row Details API page](https://infinite-table.com/docs/reference/row-detail-api/index.md) for the row detail API (when master-detail is configured). See the [Tree API page](https://infinite-table.com/docs/reference/tree-api/index.md) for the tree API (when using the `` component). ### confirmEdit (`(value?: any) => void`) > Confirms the current edit operation and closes the editor. If the `value` parameter is provided, it will be used as the value the cell will be updated with. If the `value` parameter is not provided, the current value of the cell will be used. See related [`cancelEdit`](https://infinite-table.com/docs/reference/api/index.md#cancelEdit) and [`rejectEdit`](https://infinite-table.com/docs/reference/api/index.md#rejectEdit). ### cancelEdit (`() => void`) > Cancels the current edit operation and closes the editor. See related [`confirmEdit`](https://infinite-table.com/docs/reference/api/index.md#confirmEdit) and [`rejectEdit`](https://infinite-table.com/docs/reference/api/index.md#rejectEdit). ### hideContextMenu (`() => void`) > Hides the context menu that's currently displayed (if there's one). ### rejectEdit (`(error: Error) => void`) > Rejects the current edit operation with the specified error and closes the editor. The error will later be available to the [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) callback prop, via the parameter of the function (also applicable for related functions that are called with same the same parameter). See related [`confirmEdit`](https://infinite-table.com/docs/reference/api/index.md#confirmEdit) and [`cancelEdit`](https://infinite-table.com/docs/reference/api/index.md#cancelEdit). ### clearColumnFilter (`(columnId: string) => void`) > Clears any filter for the specified column ### toggleSortingForColumn (`(columnId: string, options?) => void`) > Toggles the sorting for the specified column. This is the same method the component uses internally when the user clicks a column header. If the column is not sorted, it gets sorted in ascending order. If the column is sorted in ascending order, it gets sorted in descending order. If the column is sorted in descending order, the sorting is cleared. The `options` is optional and can have the `multiSortBehavior` property, which can be either `append` or `replace`. See related [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior) prop. If not provided, the default behavior is used. See related [`setSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#setSortingForColumn) and [`getSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#getSortingForColumn). ### setSortingForColumn (`(columnId: string, dir: 1|-1|null) => void`) > Sets the sorting for the specified column. The sort direction is specified by the `dir` parameter, which can be: - `1` for ascending - `-1` for descending - `null` for clearing the sorting. See related [`toggleSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#toggleSortingForColumn) and [`getSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#getSortingForColumn). ### getSortingForColumn (`(columnId: string)=> 1|-1|null`) > Returns the sorting currently applied to the specified column. The return value is: - `1` for ascending - `-1` for descending - `null` for no sorting. See related [`toggleSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#toggleSortingForColumn) and [`setSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#setSortingForColumn). ### collapseGroupRow (`(groupKeys: any[]) => boolean`) > Collapses the specified group row. Returns true if the group was expanded and is now being collapsed. ```tsx api.collapseGroupRow(['USA', 'New York']); // collapses the group with these keys ``` ### expandGroupRow (`(groupKeys: any[]) => boolean`) > Expands the specified group row. Returns true if the group was collapsed and is now being expanded. ```tsx api.expandGroupRow(['USA', 'New York']); // expands the group with these keys ``` ### getCellValue (`({columnId, rowIndex?, primaryKey? }) => any`) > Returns the value for the specified cell. The value is either the raw value (as retrieved via the `field` property of the column or by calling the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter)) or the formatted value - if the column has a [valueFormatter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter). Call this function with an object that has a `columnId` and either a `rowIndex` or a `primaryKey` property. See related [`getCellValues`](https://infinite-table.com/docs/reference/api/index.md#getCellValues). This function should not be called during a cell render (eg: in [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render)/[`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) or other functions called during render). ### getCellValues (`({columnId, rowIndex?, primaryKey? }) => ({value, rawValue, formattedValue })`) > Returns an object with raw and formatted values for the specified cell. Call this function with an object that has a `columnId` and either a `rowIndex` or a `primaryKey` property. The returned object has the following properties: - `rawValue` - the raw value of the cell - as retrieved from the [`columns.field`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) property of the column or by calling the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) - `formattedValue` - the formatted value of the cell - if the column has a [valueFormatter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter), it's the value returned by the formatter, otherwise it's the same as the `rawValue` - `value` - it's either `formattedValue` or `rawValue`. If the column has a [valueFormatter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter), it's the value returned by the formatter, otherwise it's the `rawValue` See related [`getCellValue`](https://infinite-table.com/docs/reference/api/index.md#getCellValue). This function should not be called during a cell render (eg: in [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render)/[`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) or other functions called during render). ### getColumnApi (`(colIdOrIndex: string|number) => InfiniteTableColumnAPI`) > Returns [a column API object](https://infinite-table.com/docs/reference/column-api/index.md) bound to the specified column The parameter can be either a column id or a column index (note this is not the index in all columns, but rather the index in current visible columns). ### getVerticalRenderRange (`() => { renderStartIndex, renderEndIndex }`) > Returns the vertical render range of the table The vertical render range is the range of rows that are currently rendered in the table viewport. ### onReady (`({ api, dataSourceApi }) => void`) > Called when the table has been layed out and sized and is ready to be used. This callback prop will be called with an object containing the `api` (which is an instance of `InfiniteTableApi`) and [`dataSourceApi`](https://infinite-table.com/docs/reference/datasource-api/index.md) objects. ### startEdit (`({ rowIndex, columnId }) => Promise`) > Tries to start editing the cell specified by the given row index and column id. Returns a promise that resolves to `true` if editing was started, or `false` if editing was not started because the cell is not editable. See [`columns.defaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) for more details on how to configure a cell as editable. **Example: Starting an Edit via the API** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, InfiniteTableApi, } from '@infinite-table/infinite-react'; import { useCallback, useRef, useState } from 'react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { const [activeRowIndex, setActiveRowIndex] = useState(2); const apiRef = useRef | null>(null); const onReady = useCallback( ({ api }: { api: InfiniteTableApi }) => { apiRef.current = api; }, [], ); return ( <> primaryKey="id" data={dataSource}> debugId="api-start-edit-example" onReady={onReady} columns={columns} columnDefaultEditable activeRowIndex={activeRowIndex} onActiveRowIndexChange={setActiveRowIndex} /> ); } ``` ### scrollCellIntoView (`(rowIndex: number; colIdOrIndex: string | number) => boolean`) > Can be used to scroll a cell into the visible viewport If scrolling was successful and the row and column combination was found, it returns `true`, otherwise `false`. The first arg of the function is the row index, while the second one is the column id or the column index (note this is not the index in all columns, but rather the index in current visible columns). ### scrollColumnIntoView (`(colId: string) => boolean`) > Can be used to scroll a column into the visible viewport If scrolling was successful and the column was found, it returns `true`, otherwise `false`. The only parameter of this method is the column id. ### scrollLeft (`getter|setter`) > Gets or sets the `scrollLeft` value in the grid viewport Can be used as either a setter, to set the scroll left position or a getter to read the scroll left position. ```ts // use as setter - will scroll the table viewport api.scrollLeft = 200; // use as getter to read the current scroll left value const scrollLeft = api.scrollLeft; ``` ### scrollRowIntoView (`(rowIndex: number) => boolean`) > Can be used to scroll a row into the visible viewport If scrolling was successful and the row was found, it returns `true`, otherwise `false` ### scrollTop (`getter|setter`) > Gets or sets the `scrollTop` value in the grid viewport Can be used as either a setter, to set the scroll top position or a getter to read the scroll top position. ```ts // use as setter - will scroll the table viewport api.scrollTop = 1200; // use as getter to read the current scroll top value const scrollTop = api.scrollTop; ``` ### rowSelectionApi (`InfiniteTableRowSelectionApi`) > Getter for the [Row Selection API](https://infinite-table.com/docs/reference/row-selection-api/index.md) ### rowDetailApi (`InfiniteTableRowDetailApi`) > Getter for the [Row Detail API](https://infinite-table.com/docs/reference/row-detail-api/index.md) ### cellSelectionApi (`InfiniteTableCellSelectionApi`) > Getter for the [Cell Selection API](https://infinite-table.com/docs/reference/cell-selection-api/index.md) ### setColumnFilter (`(columnId: string, value: any) =>void`) > Sets a filter value for the specified column ### setColumnOrder (`(columnIds: string[] | true) => void`) > Set the column order. If `true` is specified, it resets the column order to the order the columns are specified in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) prop (the iteration order of that object). ```ts api.setColumnOrder(['id', 'firstName', 'age']); // restore default order api.setColumnOrder(true); ``` ### toggleGroupRow (`(groupKeys: any[]) => void`) > Toggles the collapse/expand state of the specified group row ```tsx api.toggleGroupRow(['USA', 'New York']); // toggle the group with these keys ``` --- # Infinite Table Cell Selection API Canonical page: https://infinite-table.com/docs/reference/cell-selection-api/ ```tsx title="Configuring the selection mode to be 'multi-cell'" // can be "single-row", "multi-row", "multi-cell" or false ``` To enable cell selection, you need to specify [selectionMode="multi-cell"](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode) on the `` component. You can retrieve the cell selection api by reading it from the `api.cellSelectionApi` property. ```tsx {4} const onReady = ({api}: {api:InfiniteTableApi}) => { // do something with it api.cellSelectionApi.selectGroupRow(['USA']) } columns={[...]} onReady={onReady} /> ``` See the [Infinite Table API page](https://infinite-table.com/docs/reference/api/index.md) for the main API. See the [Infinite Table Keyboard Navigation API page](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md) for the keyboard navigation API. See the [Infinite Table Row Selection API page](https://infinite-table.com/docs/reference/row-selection-api/index.md) for the row selection API. See the [Infinite Table Column API page](https://infinite-table.com/docs/reference/column-api/index.md) for the column API. ### isCellSelected (`({rowIndex/rowId, colIndex/colId}) => boolean`) > Boolean getter to report if a cell is selected. The accepted argument is an object with the following properties: - `rowIndex` (the index of the row) or `rowId` (the id of the row) - `colIndex` (the index of the column) or `colId` (the id of the column) You can identify the cell by any of the valid combinations of `rowIndex`/`rowId` and `colIndex`/`colId`. Using row and column indexes for selection is supported to make it easier to use the API, but in fact cells are selected by the `rowId/colId` combination. This is important to keep in mind, as when columns are reordered or rows are sorted/filtered - the selection will be bound to the `rowId/colId` - so cells that were selected as siblings before a column reorder might not be siblings after the reorder, but they will still be rendered as selected. ### mapCellSelectionPositions (`(fn: (rowInfo, colId) => any, emptyValue)`) > Maps the selected cells using the passed fn. This allows you to retrieve the values from the selected cells, by using a mapping function, so for each value (cell) in the selection, the passed `fn` is called, so you can return your own object with the values you need. **Example: Retrieving cell selection value by mapping over them** ```ts file=cell-selection-mapping-example.page.tsx" ``` ### selectColumn (`(colId: string)=> void`) > Selects all cells in the specified column. **Example: Using `selectColumn` with controlled selection** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, DataSourcePropCellSelection_MultiCell, InfiniteTableApi, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 60 }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [cellSelection, setCellSelection] = React.useState({ defaultSelection: false, selectedCells: [ [3, 'stack'], [0, 'firstName'], ], }); const [api, setApi] = React.useState | null>(); return (
Current selection:
{JSON.stringify(cellSelection, null, 2)}
primaryKey="id" data={dataSource} cellSelection={cellSelection} onCellSelectionChange={setCellSelection} selectionMode="multi-cell" > debugId="controlled-cell-selection-with-api-example" columns={columns} columnDefaultWidth={100} onReady={({ api }) => { setApi(api); }} />
); } ``` ### selectCell (`({ rowIndex/rowId, colIndex/colId, clear?: boolean}) => void`) > Selects the specified cell. For the shape of the argument see related [isCellSelected](#isCellSelected). Additionally, you can pass a `clear` property to clear the selection before selecting the cell. Also see related [deselectCell](#deselectCell). In order to select a cell via mouse interaction, simply click the desired cell. Clicking a cell without any modifier keys will clear the selection and select the clicked cell. You can use `Cmd/Ctrl+Click` to add cells to the selection, or `Shift+Click` to select a range of cells. **Example: Selecting a cell via the Cell Selection API** ```ts import { InfiniteTable, DataSource, InfiniteTableApi, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [api, setApi] = React.useState | null>( null, ); return ( <>
primaryKey="id" data={dataSource} selectionMode="multi-cell" > debugId="select-cell-example" onReady={({ api }) => { setApi(api); }} columns={columns} columnDefaultWidth={100} />
); } ``` ### deselectCell (`({ rowIndex/rowId, colIndex/colId}) => void`) > Deselects the specified cell. For the shape of the argument see related [isCellSelected](#isCellSelected). Also see related [selectCell](#selectCell). ### selectAll (`() => void`) > Selects all cells in the DataGrid. See related [deselectAll](#deselectAll). ### deselectAll (`() => void`) > Deselects all cells in the DataGrid. See related [selectAll](#selectAll). ### clear (`() => void`) > An alias for [deselectAll](#deselectAll). ### selectRange (`(start, end) => void`) > Selects the specified cell range. The `start` and `end` arguments are objects of the same shape as the argument for [isCellSelected](#isCellSelected). In order to select a range via mouse interaction, use `Cmd/Ctrl+Click` and `Shift+Click` as you would in a spreadsheet application. Clicking a cell without holding the modifier keys will clear the selection and select the clicked cell. **Example: Selecting a range via the Cell Selection API** ```ts import { InfiniteTable, DataSource, InfiniteTableApi, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [api, setApi] = React.useState | null>( null, ); return ( <>
primaryKey="id" data={dataSource} selectionMode="multi-cell" > debugId="select-range-example" onReady={({ api }) => { setApi(api); }} columns={columns} columnDefaultWidth={100} />
); } ``` Don't worry if the `start` or `end` are not passed in the correct order - Infinite Table will figure it out. For deselecting a range see [deselectRange](#deselectRange). ### deselectRange (`(start, end) => void`) > Deselects the specified cell range. The `start` and `end` arguments are objects of the same shape as the argument for [isCellSelected](#isCellSelected). Don't worry if the `start` or `end` are not passed in the correct order - Infinite Table will figure it out. For selecting a range see [selectRange](#selectRange). **Example: Deselecting a range via the Cell Selection API** ```ts import { InfiniteTable, DataSource, InfiniteTableApi, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [api, setApi] = React.useState | null>( null, ); return ( <>
primaryKey="id" data={dataSource} selectionMode="multi-cell" defaultCellSelection={{ defaultSelection: true, deselectedCells: [], }} > debugId="deselect-range-example" onReady={({ api }) => { setApi(api); }} columns={columns} columnDefaultWidth={100} />
); } ``` --- # Infinite Table Column API Canonical page: https://infinite-table.com/docs/reference/column-api/ When rendering the `InfiniteTable` component, you can get access to the Column API through various column render props (for example, the [`columns.header`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) render prop). For the root API see the [API page](https://infinite-table.com/docs/reference/api/index.md). See the [Infinite Table Keyboard Navigation API page](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md) for the keyboard navigation API. For API on row/group selection, see the [Selection API page](https://infinite-table.com/docs/reference/selection-api). See the [Infinite Table Row Selection API page](https://infinite-table.com/docs/reference/row-selection-api/index.md) for the row selection API. See the [Infinite Table Row Detail API page](https://infinite-table.com/docs/reference/row-detail-api/index.md) for the row detail API (when master-detail is configured). ### clearSort > Clears the sorting for the current column. See related [`setSort`](https://infinite-table.com/docs/reference/infinite-table-props.md#setSort) prop. Calling this will trigger [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) ### hideContextMenu (`() => void`) > Hides the column context menu, if visible. For showing the menu, see [`showContextMenu`](https://infinite-table.com/docs/reference/infinite-table-props.md#showContextMenu). To toggle the menu, see [`toggleContextMenu`](https://infinite-table.com/docs/reference/infinite-table-props.md#toggleContextMenu). ### setSort (`(sortDir: 1|-1|null) => void`) > Sets the sort direction for the current column. To clear the sort, pass `null` as the argument. See related [`clearSort`](https://infinite-table.com/docs/reference/infinite-table-props.md#clearSort) Calling this will trigger [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange). ### toggleSort (`(options?) => void`) > Toggles the sorting for the current column. Aliased to [`toggleSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#toggleSortingForColumn). This is the same method the component uses internally when the user clicks a column header. If the column is not sorted, it gets sorted in ascending order. If the column is sorted in ascending order, it gets sorted in descending order. If the column is sorted in descending order, the sorting is cleared. The `options` is optional and can have the `multiSortBehavior` property, which can be either `append` or `replace`. See related [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior) prop. If not provided, the default behavior is used. See related [`setSort`](https://infinite-table.com/docs/reference/column-api/index.md#setSort) and [`getSortingForColumn`](https://infinite-table.com/docs/reference/column-api/index.md#getSortingForColumn). ### setSort (`(dir: 1|-1|null) => void`) > Sets the sorting for the current column. Aliased to [`setSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#setSortingForColumn). The sort direction is specified by the `dir` parameter, which can be: - `1` for ascending - `-1` for descending - `null` for clearing the sorting. See related [`toggleSort`](https://infinite-table.com/docs/reference/column-api/index.md#toggleSort) and [`getSortDir`](https://infinite-table.com/docs/reference/column-api/index.md#getSortDir). ### getSortDir (`()=> 1|-1|null`) > Returns the sorting currently applied to the current column. Aliased to [`getSortingForColumn`](https://infinite-table.com/docs/reference/api/index.md#getSortingForColumn). The return value is: - `1` for ascending - `-1` for descending - `null` for no sorting. See related [`toggleSortingForColumn`](https://infinite-table.com/docs/reference/column-api/index.md#toggleSortingForColumn) and [`setSortingForColumn`](https://infinite-table.com/docs/reference/column-api/index.md#setSortingForColumn). ### clearSort (`() => void`) > Clears the sorting for the current column. It is the same as calling [`setSort`](https://infinite-table.com/docs/reference/column-api/index.md#setSort) with `null` as the argument. ### isSortable (`()=> boolean`) > Returns whether the current column is sortable. See related [`columnDefaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultSortable), [`columns.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultSortable), [`columnTypes.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultSortable) and [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) ### showContextMenu (`() => void`) > Shows the column context menu, if not already visible. For hiding the menu, see [`hideContextMenu`](https://infinite-table.com/docs/reference/infinite-table-props.md#hideContextMenu). To toggle the menu, see [`toggleContextMenu`](https://infinite-table.com/docs/reference/infinite-table-props.md#toggleContextMenu). ### toggleContextMenu (`() => void`) > Toggles the column context menu. For showing the menu, see [`showContextMenu`](https://infinite-table.com/docs/reference/infinite-table-props.md#showContextMenu). For hiding the menu, see [`hideContextMenu`](https://infinite-table.com/docs/reference/infinite-table-props.md#hideContextMenu). **Example: Custom header with button to trigger the column context menu using the Column API** The `preferredLanguage` column has a custom header that shows a button for triggering the column context menu using the Column API. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { currency: { field: 'currency', // custom menu icon renderMenuIcon: () =>
🌎
, }, preferredLanguage: { field: 'preferredLanguage', defaultWidth: 350, header: ({ columnApi, renderLocation }) => { // if we're inside the column menu with all columns, return only the col name if (renderLocation === 'column-menu') { return 'Preferred Language'; } // but for the real column header // return this custom content return ( <> Preferred Language{' '} ); }, // custom menu icon renderMenuIcon: () =>
🌎
, }, salary: { field: 'salary', // hide the menu icon renderMenuIcon: false, }, country: { field: 'country', }, id: { field: 'id', defaultWidth: 80, renderMenuIcon: false }, firstName: { field: 'firstName', }, }; export default function ColumnContextMenuItems() { return ( <> primaryKey="id" data={dataSource}> debugId="getColumnMenuItems-example" columnHeaderHeight={70} columns={columns} getColumnMenuItems={(items, { column }) => { if (column.id === 'firstName') { // you can adjust the default items for a specific column items.splice(0, 0, { key: 'firstName', label: 'First name menu item', onAction: () => { console.log('Hey there!'); }, }); } items.push( { key: 'hello', label: 'Hello World', onAction: () => { alert('Hello World from column ' + column.id); }, }, { key: 'translate', label: 'Translate', menu: { items: [ { key: 'translateToEnglish', label: 'English', onAction: () => { console.log('Translate to English'); }, }, { key: 'translateToFrench', label: 'French', onAction: () => { console.log('Translate to French'); }, }, ], }, }, ); return items; }} />
); } ``` --- # DataSource API Canonical page: https://infinite-table.com/docs/reference/datasource-api/ When rendering the `DataSource` component, you can get access to the API by getting it from the [`onReady`](https://infinite-table.com/docs/reference/datasource-props/index.md#onReady) callback prop. ```tsx {3} onReady={(api: DataSourceApi) => { // api is accessible here // you may want to store a reference to it in a ref or somewhere in your app state }} /> ``` You can also get it from the `InfiniteTable` [`onReady`](https://infinite-table.com/docs/reference/infinite-table-props.md#onReady) callback prop: ```tsx {4} columns={[...]} onReady={( {api, dataSourceApi}: { api: InfiniteTableApi, dataSourceApi: DataSourceApi }) => { // both api and dataSourceApi are accessible here }} /> ``` For API on row/group selection, see the [Selection API page](https://infinite-table.com/docs/reference/selection-api). ### isRowDisabledAt (`(rowIndex: number) => boolean`) > Returns `true` if the row at the specified index is disabled, `false` otherwise. See the [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) prop for more information. For checking if a row is disabled by its primary key, see the [`isRowDisabled`](https://infinite-table.com/docs/reference/datasource-api/index.md#isRowDisabled) method. For changing the enable/disable state for the row, see the [`setRowEnabledAt`](https://infinite-table.com/docs/reference/datasource-api/index.md#setRowEnabledAt). **Example: Changing the enable/disable state for a row** ```ts import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, RowDisabledStateObject, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { const [rowDisabledState, setRowDisabledState] = React.useState< RowDisabledStateObject >({ enabledRows: true, disabledRows: [1, 3, 4, 5], }); return ( <> data={data} primaryKey="id" rowDisabledState={rowDisabledState} onRowDisabledStateChange={(rowState) => { setRowDisabledState(rowState.getState()); }} > debugId="rowDisabledState-example" getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { const rowDisabled = dataSourceApi.isRowDisabledAt( rowInfo.indexInAll, ); return { columns: [{ name: 'label' }], items: [ { label: 'Disable row', disabled: rowDisabled, key: 'disable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); hideMenu(); }, }, { label: 'Enable row', disabled: !rowDisabled, key: 'enable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled(rowInfo.id, true); hideMenu(); }, }, { label: 'Toggle row enable/disable', key: 'toggle-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled( rowInfo.id, dataSourceApi.isRowDisabled(rowInfo.id), ); hideMenu(); }, }, ], }; }} keyboardNavigation="row" columnDefaultWidth={120} columnMinWidth={50} columns={columns} />
); }; ``` ### isRowDisabled (`(primaryKey: any) => boolean`) > Returns `true` if the row with the specified primary key is disabled, `false` otherwise. See the [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) prop for more information. For checking if a row is disabled by its index, see the [`isRowDisabledAt`](https://infinite-table.com/docs/reference/datasource-api/index.md#isRowDisabledAt) method. For changing the enable/disable state for the row, see the [`setRowEnabled`](https://infinite-table.com/docs/reference/datasource-api/index.md#setRowEnabled). **Example: Changing the enable/disable state for a row** ```ts import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, RowDisabledStateObject, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { const [rowDisabledState, setRowDisabledState] = React.useState< RowDisabledStateObject >({ enabledRows: true, disabledRows: [1, 3, 4, 5], }); return ( <> data={data} primaryKey="id" rowDisabledState={rowDisabledState} onRowDisabledStateChange={(rowState) => { setRowDisabledState(rowState.getState()); }} > debugId="rowDisabledState-example" getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { const rowDisabled = dataSourceApi.isRowDisabledAt( rowInfo.indexInAll, ); return { columns: [{ name: 'label' }], items: [ { label: 'Disable row', disabled: rowDisabled, key: 'disable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); hideMenu(); }, }, { label: 'Enable row', disabled: !rowDisabled, key: 'enable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled(rowInfo.id, true); hideMenu(); }, }, { label: 'Toggle row enable/disable', key: 'toggle-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled( rowInfo.id, dataSourceApi.isRowDisabled(rowInfo.id), ); hideMenu(); }, }, ], }; }} keyboardNavigation="row" columnDefaultWidth={120} columnMinWidth={50} columns={columns} />
); }; ``` ### setRowEnabled (`(primaryKey: any, enabled: boolean) => void`) > Sets the enable/disable state for the row with the specified primary key. See the [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) prop for more information. For setting the enable/disable state for a row by its index, see the [`setRowEnabledAt`](https://infinite-table.com/docs/reference/datasource-api/index.md#setRowEnabledAt) method. **Example: Changing the enable/disable state for a row** ```ts import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, RowDisabledStateObject, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { const [rowDisabledState, setRowDisabledState] = React.useState< RowDisabledStateObject >({ enabledRows: true, disabledRows: [1, 3, 4, 5], }); return ( <> data={data} primaryKey="id" rowDisabledState={rowDisabledState} onRowDisabledStateChange={(rowState) => { setRowDisabledState(rowState.getState()); }} > debugId="rowDisabledState-example" getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { const rowDisabled = dataSourceApi.isRowDisabledAt( rowInfo.indexInAll, ); return { columns: [{ name: 'label' }], items: [ { label: 'Disable row', disabled: rowDisabled, key: 'disable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); hideMenu(); }, }, { label: 'Enable row', disabled: !rowDisabled, key: 'enable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled(rowInfo.id, true); hideMenu(); }, }, { label: 'Toggle row enable/disable', key: 'toggle-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled( rowInfo.id, dataSourceApi.isRowDisabled(rowInfo.id), ); hideMenu(); }, }, ], }; }} keyboardNavigation="row" columnDefaultWidth={120} columnMinWidth={50} columns={columns} /> ); }; ``` ### treeApi (`TreeApi`) > A reference to the [Tree API](https://infinite-table.com/docs/reference/tree-api/index.md). When using the `` component, this property will be available on the `DataSourceApi` instance. ### setRowEnabledAt (`(rowIndex: number, enabled: boolean) => void`) > Sets the enable/disable state for the row at the specified index. See the [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) prop for more information. For setting the enable/disable state for a row by its primary key, see the [`setRowEnabled`](https://infinite-table.com/docs/reference/datasource-api/index.md#setRowEnabled) method. **Example: Changing the enable/disable state for a row** ```ts import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, RowDisabledStateObject, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { const [rowDisabledState, setRowDisabledState] = React.useState< RowDisabledStateObject >({ enabledRows: true, disabledRows: [1, 3, 4, 5], }); return ( <> data={data} primaryKey="id" rowDisabledState={rowDisabledState} onRowDisabledStateChange={(rowState) => { setRowDisabledState(rowState.getState()); }} > debugId="rowDisabledState-example" getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { const rowDisabled = dataSourceApi.isRowDisabledAt( rowInfo.indexInAll, ); return { columns: [{ name: 'label' }], items: [ { label: 'Disable row', disabled: rowDisabled, key: 'disable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); hideMenu(); }, }, { label: 'Enable row', disabled: !rowDisabled, key: 'enable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled(rowInfo.id, true); hideMenu(); }, }, { label: 'Toggle row enable/disable', key: 'toggle-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled( rowInfo.id, dataSourceApi.isRowDisabled(rowInfo.id), ); hideMenu(); }, }, ], }; }} keyboardNavigation="row" columnDefaultWidth={120} columnMinWidth={50} columns={columns} /> ); }; ``` ### replaceAllData (`(data: DATA_TYPE[], options?: DataSourceCRUDParam) => Promise`) > Replaces all data in the DataSource with the provided data. Clears the current data array in the `` and replaces it with the provided data. When calling this, if there are pending data mutations, they will be discarded. See related [`clearAllData`](https://infinite-table.com/docs/reference/datasource-api/index.md#clearAllData) method. ### clearAllData (`() => Promise`) > Clears all data in the DataSource. See related [`replaceAllData`](https://infinite-table.com/docs/reference/datasource-api/index.md#replaceAllData) method. ### addData (`(data: DATA_TYPE) => Promise`) > Adds the specified data at the end of the data source. The given data param should be of type `DATA_TYPE` (the TypeScript generic data type that the `DataSource` was bound to). For adding an array of data, see the [`addDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#addDataArray) method. If the component has [sorting](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo), the added data might not be displayed at the end of the data source. This method batches data updates and waits for a request animation frame until it persists the data to the `DataSource`. This means you can execute multiple calls to `addData` (or [`updateData`](https://infinite-table.com/docs/reference/datasource-props/index.md#updateData), [`removeData`](https://infinite-table.com/docs/reference/datasource-props/index.md#removeData), [`insertData`](https://infinite-table.com/docs/reference/datasource-props/index.md#insertData)) in the same frame and they will be batched and persisted together. The return value is a `Promise` that resolves when the data has been added. When multiple `addData` (and friends) calls are executed in the same frame, the result of those calls is a reference to the same promise. ```ts const promise1 = dataSourceApi.add({ ... }) const promise2 = dataSourceApi.add({ ... }) const promise3 = dataSourceApi.insertData({ ... }, { position: 'before', primaryKey: 4 }) // promise1, promise2 and promise3 are the same promise // as the calls are run in the same raf and batched together // promise1 === promise2 // promise1 === promise3 ``` For adding an array of data, see the [`addDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#addDataArray) method. For inserting data at a specific position, see the [`insertData`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertData) method. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. **Example: Using DataSourceApi.addData to update the DataSource** ```ts import * as React from 'react'; import { DataSourceApi, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import { DataSource } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; }; const data: Developer[] = [ { id: 1, firstName: 'John', lastName: 'Bob', age: 20, canDesign: 'yes', currency: 'USD', preferredLanguage: 'JavaScript', stack: 'frontend', }, { id: 2, firstName: 'Marry', lastName: 'Bob', age: 25, canDesign: 'yes', currency: 'USD', preferredLanguage: 'JavaScript', stack: 'frontend', }, { id: 3, firstName: 'Bill', lastName: 'Bobson', age: 30, canDesign: 'no', currency: 'CAD', preferredLanguage: 'TypeScript', stack: 'frontend', }, { id: 4, firstName: 'Mark', lastName: 'Twain', age: 31, canDesign: 'yes', currency: 'CAD', preferredLanguage: 'Rust', stack: 'backend', }, { id: 5, firstName: 'Matthew', lastName: 'Hilson', age: 29, canDesign: 'yes', currency: 'CAD', preferredLanguage: 'Go', stack: 'backend', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', }, firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; let ID = data.length; const currencies = ['USD', 'CAD', 'EUR', 'GBP', 'RUB', 'UAH', 'CNY']; const getMark = (): Developer => ({ id: ++ID, firstName: 'Mark', lastName: 'Berg', // random int from 20 to 60 age: Math.floor(Math.random() * (60 - 20 + 1)) + 20, canDesign: 'no', //random currency currency: currencies[Math.floor(Math.random() * currencies.length)], preferredLanguage: 'Go', stack: 'frontend', }); export default () => { const [dataSourceApi, setDataSourceApi] = React.useState>(); return ( <> data={data} primaryKey="id" onReady={setDataSourceApi} > debugId="addData-example" columnDefaultWidth={100} columnMinWidth={50} columns={columns} /> ); }; ``` ### addDataArray (`(data: DATA_TYPE[]) => Promise`) > Adds an array of data at the end of the data source See related [`addData`](https://infinite-table.com/docs/reference/datasource-api/index.md#addData) method. For adding at the beginning of the data source, see the [`insertDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertDataArray) method. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. ### getDataByNodePath (`(nodePath: NodePath) => DATA_TYPE | null`) > Retrieves the data object for the specified node path. **Example: Retrieving data by node path** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeIndex, setActiveIndex] = useState(0); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### getDataByPrimaryKey (`(primaryKey: string | number) => DATA_TYPE | null`) > Retrieves the data object for the specified primary key. You can call this method to retrieve objects from the data source even when they have been filtered out via [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) or [`filterFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterFunction), as long as they are present in the initial data. The alternative API method [`getRowInfoByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#getRowInfoByPrimaryKey) can only be used to retrieve [row info objects](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) of rows that are not filtered out - so only rows that match the filtering, if one is present. ### getIndexByPrimaryKey (`(id: any) => number`) > Retrieves the index of a row by its primary key. If the row is not found, returns `-1`. See related [`getPrimaryKeyByIndex`](https://infinite-table.com/docs/reference/datasource-api/index.md#getPrimaryKeyByIndex) The primary key you pass in needs to exist in the current data set. If you pass in a primary key that has been filtered out or that's not in the data set, the method will return `-1`. ### getPrimaryKeyByIndex (`(index: number) => any | undefined `) > Retrieves the primary key of a row by its current index. If the row is not found, returns `undefined`. See related [`getIndexByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#getIndexByPrimaryKey) The index needs to be of an existing row, after all filtering is applied. If you pass in an non-existent index, the method will return `undefined`. ### getDataByNodePath (`(nodePath: any[]) => DATA_TYPE | null`) > Retrieves the data object for the node with the specified path. If the node is not found, returns `null`. See related [`getDataByIndex`](https://infinite-table.com/docs/reference/datasource-api/index.md#getDataByIndex). See related [`getRowInfoByNodePath`](https://infinite-table.com/docs/reference/datasource-api/index.md#getRowInfoByNodePath). The node path needs to be of an existing node. If you pass in a non-existent (or filtered out) node path, the method will return `null`. **Example: Retrieving data by node path** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeIndex, setActiveIndex] = useState(0); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### getRowInfoByNodePath (`(nodePath: any[]) => InfiniteTableRowInfo | null`) > Retrieves the [row info object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) for the node with the specified path. If the node is not found, returns `null`. See related [`getDataByNodePath`](https://infinite-table.com/docs/reference/datasource-api/index.md#getDataByNodePath). The node path needs to be of an existing node. If you pass in a non-existent (or filtered out) node path, the method will return `null`. ### getRowInfoByIndex (`(index: number) => InfiniteTableRowInfo | null`) > Retrieves the [row info object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) for the row at the specified index. If none found, returns `null`. See related [`getRowInfoByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#getRowInfoByPrimaryKey). ### getRowInfoByPrimaryKey (`(id: any) => InfiniteTableRowInfo | null`) > Retrieves the [row info object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) for the row with the specified primary key. If none found, returns `null`. This method will only find row info objects for rows that are currently in the dataset and matching the filtering, if one is present. Can also be called for group rows. See related [`getDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#getDataByPrimaryKey) method, which retrieves the raw data object for the specified primary key, even if it has been filtered out. ### getRowInfoArray (`() => InfiniteTableRowInfo[]`) > Returns the current row info array. See [the type definition of the row info object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo). The row info array represents the current state of the DataSource. This array may contain more items than the actual data array fetched initially by the DataSource. This is because it includes group rows, when grouping is defined, as well as unfetched rows in some advanced scenarios. ### insertData (`(data: DATA_TYPE, { position, primaryKey }) => Promise`) > Inserts the given data at the specified position relative to the given primary key. The `position` can be one of the following: - `start` | `end` - inserts the data at the beginning or end of the data source. In this case, no `primaryKey` is needed. - `before` | `after` - inserts the data before or after the data item that has the specified primary key. **In thise case, the `primaryKey` is required.** We're intentionally not encouraging inserting at a specified `index`, as the index of rows in the visible viewport can change as the user sorts, filters or groups the data. For inserting an array of data, see the [`insertDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertDataArray) method. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. **Example: Inserting data at various locations** Click any row in the table to make it the current active row, and then use the second button to add a new row after the active row. ```ts import * as React from 'react'; import { DataSourceApi, InfiniteTable, InfiniteTableApi, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import { DataSource } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; reposCount: number; }; export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } const CURRENCIES = ['USD', 'CAD', 'EUR']; const stacks = ['frontend', 'backend', 'fullstack']; let ID = 0; const firstNames = ['John', 'Jane', 'Bob', 'Alice', 'Mike', 'Molly']; const lastNames = ['Smith', 'Doe', 'Johnson', 'Williams', 'Brown', 'Jones']; const getRow = (count?: number): Developer => { return { id: ID++, firstName: ID === 1 ? 'ROCKY' : firstNames[getRandomInt(0, firstNames.length - 1)] + (count ? ` ${count}` : ''), lastName: lastNames[getRandomInt(0, firstNames.length - 1)], currency: CURRENCIES[getRandomInt(0, 2)], salary: getRandomInt(1000, 10000), preferredLanguage: 'JavaScript', stack: stacks[getRandomInt(0, 2)], canDesign: getRandomInt(0, 1) === 0 ? 'yes' : 'no', age: getRandomInt(20, 100), reposCount: getRandomInt(0, 100), }; }; const dataSource: Developer[] = [...Array(10)].map(getRow); const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', style: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return {}; } return { color: 'black', background: value > 80 ? 'tomato' : value > 60 ? 'orange' : value > 40 ? 'yellow' : value > 20 ? 'lightgreen' : 'green', }; }, }, salary: { field: 'salary', type: 'number', }, reposCount: { field: 'reposCount', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; const buttonStyle = { border: '2px solid magenta', color: 'var(--infinite-cell-color)', background: 'var(--infinite-background)', }; export default () => { const [apis, onReady] = React.useState<{ api: InfiniteTableApi; dataSourceApi: DataSourceApi; }>(); const [currentActivePrimaryKey, setCurrentActivePrimaryKey] = React.useState(''); return ( data={dataSource} primaryKey="id"> debugId="insert-example" domProps={domProps} onReady={onReady} columnDefaultWidth={130} columnMinWidth={50} columns={columns} keyboardNavigation="row" onActiveRowIndexChange={(rowIndex) => { const id = apis?.dataSourceApi.getRowInfoArray()[rowIndex].id; setCurrentActivePrimaryKey(id); }} /> ); }; ``` ### insertDataArray (`(data: DATA_TYPE[], { position, primaryKey?, nodePath?, waitForNode? }) => Promise`) > Inserts an array of data at the specified position (and relative to the given primary key or node path). Just like the [`insertData`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertData) method, the `position` can be one of the following: - `start` | `end` - inserts the data at the beginning or end of the data source. In this case, no `primaryKey` is needed. If `nodePath` is provided, `"start"` means insert the data at the start of the children array of that node; `"end"` means insert the data at the end of the children array of that node. - `before` | `after` - inserts the data before or after the data item that has the specified primary key / node path. **In thise case, the `primaryKey` or the `nodePath` is required.** All the data items passed to this method will be inserted (in the order in the array) at the specified position. When using this method for tree nodes, `waitForNode` defaults to `true` (you can specify a boolean value, or a number to override the default timeout). When `waitForNode` is `true`, the method will insert the data in the respective node's children array, after making sure the node exists. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. ### updateDataArrayByNodePath (`({data, nodePath}[]) => Promise`) > Updates the data for the nodes specified by the node paths. The first parameter should be an array of objects, where each object has a `data` property (the data to update) and a `nodePath` property (the path of the node to update). ```tsx title="Updating an aray of tree nodes" dataSourceApi.updateDataArrayByNodePath([ { data: { fileName: 'Vacation.pdf', sizeInKB: 1000, }, nodePath: ['1', '10'], }, { data: { fileName: 'Report.docx', sizeInKB: 2000, }, nodePath: ['1', '11'], }, ]); ``` ### updateChildrenByNodePath (`(children: DATA_TYPE[] | any | (children, data) => DATA_TYPE[] | any, nodePath: NodePath) => Promise, options?`) > Updates the children of the node specified by the node path. The first parameter can be an array (or `null` or `undefined`) or a function that returns an array (or `null` or `undefined`). The second parameter is the node path. When a function is passed as the first parameter, it will be called with the children of the node. The return value will be used as the new children of the node. This gives you an opportunity to update the children based on the current children state. ```tsx title="Updating the children of a tree node" dataSourceApi.updateChildrenByNodePath( (currentChildren) => { return [ ...(currentChildren || []), { name: 'untitled.txt', id: '8', }, ]; }, ['1', '3'], ); ``` When using a function, it will be called with the current children of the node (1st parameter) and also with the node data object (2nd parameter). As a third parameter, you can pass in an options object, which supports the `waitForNode` property (`boolean` or `number` to override the default timeout). When `waitForNode` is `true`, the method will update the children of the node, after making sure the node exists (and waiting for the specified timeout if needed). ### waitForNodePath (`(nodePath: NodePath, options?: { timeout?: number }) => Promise`) > Returns a promise that tells if the node path exists. Calling this method will give you a promise that will tell you if the node path exists or not. If the `DataSource` can find the node path either immediately or before the specified timeout expires, the promise will resolve to `true`, otherwise it will resolve to `false`. If no `timeout` is specified, it will default to `1000`ms. ### updateDataByNodePath (`(data: Partial, nodePath: NodePath, options?) => Promise`) > Updates the data for the node specified by the node path. If the primary keys in your `` are unique globally (not just within the same node), you can still use the [`updateData`](https://infinite-table.com/docs/reference/datasource-props/index.md#updateData) method. ```tsx title="Updating a tree node by path" dataSourceApi.updateDataByNodePath({ fileName: 'New Name', sizeInKB: 1000, }, ['1', '10']); ``` **Example: Updating a tree node by path** ```ts import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeIndex, setActiveIndex] = useState(0); return ( <>
Use the buttons below to update the current active node.
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; let times = 0; const getRandomFileName = () => { const names = [ 'Report', 'Vacation', 'CV', 'Unknown', 'Sales.jpg', 'Finances.xls', 'Presentation.ppt', 'Budget.xlsx', 'Notes.txt', 'Resume.docx', 'Agenda.docx', 'Summary.docx', 'Proposal.docx', 'Report.docx', 'Invoice.pdf', 'Memo.docx', 'Letter.docx', 'Plan.docx', 'Guide.docx', ]; const name = names[Math.floor(Math.random() * names.length)]; const [namePart, extension] = name.split('.'); times++; return `${namePart}${times}${extension ? `.${extension}` : ''}`; }; ``` The third parameter is an options object, which can have a `waitForNode` property (either `boolean` or `number` - use a `number` to override the default timeout). NOTE: if you don't pass it, it defaults to `1000ms`. When `waitForNode` is used, if the node does not exist yet, it will wait for the specified timeout and then update the data. ### removeDataByNodePath (`(nodePath: NodePath) => Promise`) > Removes the node specified by the specified node path. If the primary keys in your `` are unique globally (not just within the same node), you can still use the [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-props/index.md#removeDataByPrimaryKey) method. ```tsx title="Removing a tree node by path" dataSourceApi.removeDataByNodePath(['1', '10']); ``` **Example: Removing a tree node by path** ```ts import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeIndex, setActiveIndex] = useState(0); return ( <>
Use the buttons below to update the current active node.
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; let times = 0; const getRandomFileName = () => { const names = [ 'Report', 'Vacation', 'CV', 'Unknown', 'Sales.jpg', 'Finances.xls', 'Presentation.ppt', 'Budget.xlsx', 'Notes.txt', 'Resume.docx', 'Agenda.docx', 'Summary.docx', 'Proposal.docx', 'Report.docx', 'Invoice.pdf', 'Memo.docx', 'Letter.docx', 'Plan.docx', 'Guide.docx', ]; const name = names[Math.floor(Math.random() * names.length)]; const [namePart, extension] = name.split('.'); times++; return `${namePart}${times}${extension ? `.${extension}` : ''}`; }; ``` ### updateData (`(data: Partial, options?) => Promise`) > Updates the data item to match the given data object. For updating tree nodes by path, see the [`updateDataByNodePath`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataByNodePath) method. The data object must have a primary key that matches the primary key of the data item that you want to update. Besides the primary key, it can contain any number of properties that you want to update. ```ts dataSourceApi.updateData({ // if the primaryKey is the id, make sure to include it id: 1, // and then include any properties you want to update - in this case, the name and age name: 'John Doe', age: 30, }); ``` **Example: Updating a row** ```ts import * as React from 'react'; import { DataSourceApi, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import { DataSource } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; }; const data: Developer[] = [ { id: 1, firstName: 'John', lastName: 'Bob', age: 20, canDesign: 'yes', currency: 'USD', preferredLanguage: 'JavaScript', stack: 'frontend', }, { id: 2, firstName: 'Marry', lastName: 'Bob', age: 25, canDesign: 'yes', currency: 'USD', preferredLanguage: 'JavaScript', stack: 'frontend', }, { id: 3, firstName: 'Bill', lastName: 'Bobson', age: 30, canDesign: 'no', currency: 'CAD', preferredLanguage: 'TypeScript', stack: 'frontend', }, { id: 4, firstName: 'Mark', lastName: 'Twain', age: 31, canDesign: 'yes', currency: 'CAD', preferredLanguage: 'Rust', stack: 'backend', }, { id: 5, firstName: 'Matthew', lastName: 'Hilson', age: 29, canDesign: 'yes', currency: 'CAD', preferredLanguage: 'Go', stack: 'backend', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', }, firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const currencies = ['USD', 'CAD', 'EUR', 'GBP', 'RUB', 'UAH', 'CNY']; const stacks = ['frontend', 'backend', 'fullstack']; const languages = [ 'JavaScript', 'TypeScript', 'Go', 'Rust', 'Python', 'Java', 'C#', ]; const getRandomDeveloperUpdate = (data: Developer): Partial => ({ id: data.id, age: Math.floor(Math.random() * (60 - 20 + 1)) + 20, canDesign: ['yes', 'no'][ Math.floor(Math.random() * 2) ] as Developer['canDesign'], currency: currencies[Math.floor(Math.random() * currencies.length)], preferredLanguage: languages[Math.floor(Math.random() * languages.length)], stack: stacks[Math.floor(Math.random() * stacks.length)], }); export default () => { const [dataSourceApi, setDataSourceApi] = React.useState>(); const [activeIndex, setActiveIndex] = React.useState(0); return ( <> data={data} primaryKey="id" onReady={setDataSourceApi} > debugId="simple-updateData-example" activeRowIndex={activeIndex} onActiveRowIndexChange={setActiveIndex} keyboardNavigation="row" columnDefaultWidth={100} columnMinWidth={50} columns={columns} /> ); }; ``` For updating an array of data, see the [`updateDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataArray) method. The second parameter is an options object, which can have a `waitForNode` property (either `boolean` or `number` - use a `number` to override the default timeout). NOTE: if you don't pass it, it defaults to `1000ms`. When `waitForNode` is used, if the node does not exist yet, it will wait for the specified timeout and then update the data. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. **Example: Live data updates with DataSourceApi.updateData** The DataSource has 10k items. In this example, we're updating 5 rows (in the visible viewport) every 30ms. The update rate could be much higher, but we're keeping it at current levels to make it easier to see the changes. ```ts import * as React from 'react'; import { DataSourceApi, InfiniteTable, InfiniteTableApi, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import { DataSource } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; reposCount: number; }; const dataSource = () => { return fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/developers10k-sql`) .then((r) => r.json()) .then((data: Developer[]) => { return data; }); }; export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } const CURRENCIES = ['USD', 'CAD', 'EUR']; const stacks = ['frontend', 'backend', 'fullstack']; const updateRow = (api: DataSourceApi, data: Developer) => { const getDelta = (num: number): number => Math.ceil(0.2 * num); const initialData = data; if (!initialData) { return; } const salaryDelta = getDelta(initialData?.salary); const reposCountDelta = getDelta(initialData?.reposCount); const newSalary = initialData.salary + getRandomInt(-salaryDelta, salaryDelta); const newReposCount = initialData.reposCount + getRandomInt(-reposCountDelta, reposCountDelta); const newData: Partial = { id: initialData.id, salary: newSalary, reposCount: newReposCount, currency: CURRENCIES[getRandomInt(0, CURRENCIES.length - 1)] || CURRENCIES[0], stack: stacks[getRandomInt(0, stacks.length - 1)] || stacks[0], age: getRandomInt(0, 100), }; api.updateData(newData); }; let STARTED = false; const ROWS_TO_UPDATE_PER_FRAME = 5; const UPDATE_INTERVAL_MS = 30; const randomlyUpdateData = ({ api, dataSourceApi, }: { api: InfiniteTableApi; dataSourceApi: DataSourceApi; }) => { // protect for React.StrictMode potentially calling this twice if (STARTED) { return; } STARTED = true; setInterval(() => { const { renderStartIndex, renderEndIndex } = api.getVerticalRenderRange(); const dataArray = dataSourceApi.getRowInfoArray(); const data = dataArray .slice(renderStartIndex, renderEndIndex) .map((x) => x.data as Developer); for (let i = 0; i < ROWS_TO_UPDATE_PER_FRAME; i++) { const row = data[getRandomInt(0, data.length - 1)]; if (row) { updateRow(dataSourceApi, row); } } }, UPDATE_INTERVAL_MS); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', }, firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', style: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return {}; } return { color: 'black', background: value > 80 ? 'tomato' : value > 60 ? 'orange' : value > 40 ? 'yellow' : value > 20 ? 'lightgreen' : 'green', }; }, }, salary: { field: 'salary', type: 'number', }, reposCount: { field: 'reposCount', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( data={dataSource} primaryKey="id"> debugId="live-updates-example" domProps={domProps} onReady={randomlyUpdateData} columnDefaultWidth={130} columnMinWidth={50} columns={columns} /> ); }; ``` ### updateDataArray (`(data: Partial[], options?) => Promise`) > Updates an array of data items to match the given data objects. See related [`updateData`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateData) method. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. ### onReady (`(api: DataSourceApi) => void`) > Called only once, after the DataSource component has been mounted. This callback prop will be called with an `DataSourceApi` instance. For retrieving the [`InfiniteTableApi`](https://infinite-table.com/docs/reference/api/index.md), see the `InfiniteTable` [`onReady`](https://infinite-table.com/docs/reference/infinite-table-props.md#onReady) callback prop. ### removeData (`(data: Partial) => Promise`) > Removes the data item that matches the given data object. The data object must at least have a primary key that matches the primary key of the data item that you want to remove. All the other properties are ignored. For removing an array of data, see the [`removeDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArray) method. If you only want to remove by a primary key, you can call [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByPrimaryKey) instead. If you have an array of primary keys, you can call [`removeDataArrayByPrimaryKeys`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArrayByPrimaryKeys) instead. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. ### getOriginalDataArray (`() => DATA_TYPE[]`) > Returns the data array that was last loaded by the `DataSource` This is the array loaded by the `DataSource`, before any filtering, sorting or grouping is applied. ### removeDataArray (`(data: Partial[]) => Promise`) > Removes the data items that match the given data objects. The data objects must at least have a primary key that matches the primary key of the data item that you want to remove. All the other properties are ignored. For removing only one item, see the [`removeData`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeData) method. If you only want to remove by a primary key, you can call [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByPrimaryKey) instead. If you have an array of primary keys, you can call [`removeDataArrayByPrimaryKeys`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArrayByPrimaryKeys) instead. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. ### removeDataArrayByPrimaryKeys (`(primaryKeys: (string | number)[]) => Promise`) > Removes the data items with the specified primary keys. For removing only one data item, see the [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByPrimaryKey) method. If you have a data object, you can call [`removeData`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeData) instead. If you have an array of data objects, you can call [`removeDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArray) instead. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. ### removeDataByPrimaryKey (`(primaryKey: string | number) => Promise`) > Removes the data item with the specified primary key. For removing an array of data, see the [`removeDataArrayByPrimaryKeys`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArrayByPrimaryKeys) method. If you have a data object, you can call [`removeData`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeData) instead. If you have an array of data objects, you can call [`removeDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArray) instead. [`onDataMutations`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataMutations) allows you to listen to data mutations. --- # DataSource Props > Props Reference page for your DataSource in Infinite Table - with complete examples Canonical page: https://infinite-table.com/docs/reference/datasource-props/ In the API Reference below we'll use **`DATA_TYPE`** to refer to the TypeScript type that represents the data the component is bound to. ### primaryKey (`string | (data: DATA_TYPE) => string`) > The name of the id/primary key property of an item in the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) array. The value of this property needs to be unique. This is probably one of the most important properties of the `` component, as it is used to identify items in the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) array. Unlike with other DataGrid components, with `InfiniteTable` you don't need to have a column mapped to the primary key field. The primary key is used internally by the component and is not displayed in the grid if you don't explicitly have a column bound to that field. If the primary key is not unique, Infinite Table DataGrid won't work properly. **Example: Simple demo of using primaryKey** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Employee = { id: string | number; name: string; salary: number; department: string; company: string; }; const employees: Employee[] = [ { id: 1, name: 'Bob', salary: 10_000, department: 'IT', company: 'Bobsons', }, { id: 2, name: 'Alice', salary: 20_000, department: 'IT', company: 'Bobsons', }, { id: 3, name: 'John', salary: 30_000, department: 'IT', company: 'Bobsons', }, { id: 4, name: 'Jane', salary: 35_000, department: 'Marketing', company: 'Janies', }, { id: 5, name: 'Mary', salary: 40_000, department: 'Marketing', company: 'Janies', }, ]; // this can be an array, a promise or a function returning array/promise const data = new Promise((resolve) => { setTimeout(() => { resolve(employees); }, 100); }); export default function App() { return ( data={data} primaryKey="id"> debugId="data-example" columnDefaultWidth={130} columns={columns} /> ); } const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 80, }, name: { field: 'name', }, salary: { field: 'salary', type: 'number' }, department: { field: 'department', header: 'Dep.' }, company: { field: 'company' }, }; ``` The primary key can be either a string (the name of a property in the data object), or a function that returns a string. Using functions (for more dynamic primary keys) is supported, but hasn't been tested extensively - so please report any issues you might encounter. ### treeExpandState (`TreeExpandStateValue`) > Specifies the expand/collapse state of the tree nodes. See [`TreeExpandStateValue`](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeExpandStateValue) for the shape of this object. For the uncontrolled version, see [`defaultTreeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeExpandState). If no `treeExpandState` prop is specified, the tree will be rendered as fully expanded by default. When using the controlled version, make sure to update the `treeExpandState` prop by using the [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange) callback. **Example: Using controlled tree expand state** ```ts import { InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeExpandState, setTreeExpandState] = useState({ defaultExpanded: true, collapsedPaths: [['1', '10'], ['3']], }); return ( <>
Current tree expand state:
{JSON.stringify(treeExpandState, null, 2)}
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### onTreeExpandStateChange (`(treeExpandState: TreeExpandStateValue, {dataSourceApi,nodePath, nodeState}) => void`) > Called when the tree expand state changes. When the user interacts with the tree (by expanding or collapsing a node), this callback is called with the new tree state. The first parameter is the new tree state, and the second parameter is an object with the following properties: - `dataSourceApi` - the [DataSource API](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceApi) instance - `nodePath` - the path of the node that changed state. If the state was produced by an [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) or [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) call, this will be `null`. - `nodeState` - the new state of the node (`"collapsed"` or `"expanded"`) **Example: Using the onTreeExpandStateChange callback** ```ts import { InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeExpandState, setTreeExpandState] = useState({ defaultExpanded: true, collapsedPaths: [['1', '10'], ['3']], }); return ( <>
Current tree expand state:
{JSON.stringify(treeExpandState, null, 2)}
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### onNodeExpand (`(nodePath: NodePath, {dataSourceApi}) => void`) > Called when a node is expanded. See related [`onNodeCollapse`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeCollapse) and [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange) props. The [`onNodeExpand`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeExpand) and [`onNodeCollapse`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeCollapse) callbacks are called when a node is expanded or collapsed, respectively - either via user interaction or by an API call. However, they will not be called when the [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) or [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) methods are called. ### isNodeExpanded (`(rowInfo: InfiniteTable_Tree_RowInfoParentNode, treeExpandState: TreeExpandState) => boolean`) > Decides if the current (non-leaf) node is expanded. The inverse prop, [`isNodeCollapsed`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeCollapsed) is also available. Only one of these props can be specified. If this prop is specified, [`treeSelectionState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelectionState) is ignored. ### isNodeCollapsed (`(rowInfo: InfiniteTable_Tree_RowInfoParentNode, treeExpandState: TreeExpandState) => boolean`) > Decides if the current (non-leaf) node is collapsed. See related [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState) prop. The inverse prop, [`isNodeExpanded`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeExpanded) is also available. Only one of these props can be specified. If this prop is specified, [`treeSelectionState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelectionState) is ignored. ### isNodeReadOnly (`(rowInfo: InfiniteTable_Tree_RowInfoParentNode) => boolean`) > Decides if the current (non-leaf) node can be expanded or collapsed and if the tree icon is disabled. By default, parent nodes with `children: []` are read-only, meaning they won't respond to expand/collapse clicks. However, if you specify a custom `isNodeReadOnly` function, you can change this behavior. When a node is read-only, the [`expandNode`](https://infinite-table.com/docs/reference/tree-api/index.md#expandNode) and [`collapseNode`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseNode) methods need the `options.force` flag to be set to `true` in order to override the read-only restriction. However, [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) and [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) will work regardless of the `isNodeReadOnly` setting. For full control over the expand/collapse state of read-only nodes, you can use the [`isNodeExpanded`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeExpanded)/[`isNodeCollapsed`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeCollapsed) props. **Example: Using a custom isNodeReadOnly function** ```ts import { InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; const defaultTreeExpandState: TreeExpandStateValue = { defaultExpanded: true, collapsedPaths: [['1', '10']], }; const returnFalse = () => false; export default function App() { const [allowEmptyNodesExpand, setAllowEmptyNodesExpand] = useState(false); return ( <>
            Empty parent nodes {allowEmptyNodesExpand ? 'can' : 'cannot'} be
            expanded.
          
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop - empty', sizeInKB: 1000, type: 'folder', children: [], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### onNodeCollapse (`(nodePath: NodePath, {dataSourceApi}) => void`) > Called when a node is collapsed. See related [`onNodeExpand`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeExpand) and [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange) props. The [`onNodeExpand`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeExpand) and [`onNodeCollapse`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeCollapse) callbacks are called when a node is expanded or collapsed, respectively - either via user interaction or by an API call. However, they will not be called when the [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) or [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) methods are called. ### nodesKey (`string`) > The name of the property in the data object that contains the child nodes for each tree node. Only available when you're using the `` component. If not specified, it defaults to `"children"`. Each node gets a `nodePath` property, which is the array with the ids of all the parent nodes leading down to the current node. The node path includes the id of the current node ```tsx {2} title="Node path vs row id" const data = [ { id: '1', name: 'Documents', // path: ['1'] children: [ { id: '10', name: 'Private', // path: ['1', '10'] children: [ { id: '100', name: 'Report.docx' }, // path: ['1', '10', '100'] { id: '101', name: 'Vacation.docx' },// path: ['1', '10', '101'] ], }, ] }, { id: '2', name: 'Downloads', // path: ['2'] children: [ { id: '20', name: 'cat.jpg', // path: ['2', '20'] }, ], }, ]; ``` **Example: Using a custom nodesKey prop** ```ts import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; nodes?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( <> ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', nodes: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', nodes: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', nodes: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', nodes: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', nodes: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', nodes: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### defaultTreeExpandState (`TreeExpandStateValue`) > Specifies the expand/collapse state of the tree nodes. See [`TreeExpandStateValue`](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeExpandStateValue) for the shape of this object. For the controlled version, see [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState). **Example: Using uncontrolled tree expand state** ```ts import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; const defaultTreeExpandState: TreeExpandStateValue = { defaultExpanded: true, collapsedPaths: [['1', '10'], ['3']], }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### isRowDisabled (`(rowInfo: InfiniteTableRowInfo) => boolean`) > This function ultimately decides the disabled state of a row. It overrides both [`defaultRowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowDisabledState)/[`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) props. It's called with a single argument - the [row info object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) for the row in question. It should return `true` if the row is disabled, and `false` otherwise. When this prop is used, [`onRowDisabledStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowDisabledStateChange) will not be called. ### defaultRowDisabledState (`{enabledRows,disabledRows}`) > The uncontrolled prop for managing row enabled/disabled state. For the controlled version see [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState). For listening to row disabled state changes, see [`onRowDisabledStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowDisabledStateChange). The value for this prop is an object with two properties: - `enabledRows` - either `true` or an array of row ids that are enabled. When `true` is passed, `disabledRows` should be an array of row ids that are disabled. - `disabledRows` - either `true` or an array of row ids that are disabled. When `true` is passed, `enabledRows` should be an array of row ids that are enabled. The values in the `enabledRows`/`disabledRows` arrays are row ids, and not indexes. This prop can be overriden by using the [`isRowDisabled`](https://infinite-table.com/docs/reference/datasource-props/index.md#isRowDisabled) prop. Here's an example of how to use the `defaultRowDisabledState` prop: **Example: Using uncontrolled row disabled state** Rows with ids `1`, `3`, `4` and `5` are disabled. ```ts import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { return ( <> data={data} primaryKey="id" defaultRowDisabledState={{ enabledRows: true, disabledRows: [1, 3, 4, 5], }} > debugId="defaultRowDisabledState-example" keyboardNavigation="row" columnDefaultWidth={120} columnMinWidth={50} columns={columns} /> ); }; ``` ### rowDisabledState (`{enabledRows,disabledRows}`) > Manages row enabled/disabled state. For the uncontrolled version see [`defaultRowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowDisabledState). For listening to row disabled state changes, see [`onRowDisabledStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowDisabledStateChange). The value for this prop is an object with two properties: - `enabledRows` - either `true` or an array of row ids that are enabled. When `true` is passed, `disabledRows` should be an array of row ids that are disabled. - `disabledRows` - either `true` or an array of row ids that are disabled. When `true` is passed, `enabledRows` should be an array of row ids that are enabled. When using this controlled prop, you will need to update the `rowDisabledState` prop by using the [`onRowDisabledStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowDisabledStateChange) callback. This prop can be overriden by using the [`isRowDisabled`](https://infinite-table.com/docs/reference/datasource-props/index.md#isRowDisabled) prop. **Example: Using controlled row disabled state** Rows with ids `1`, `3`, `4` and `5` are disabled initially. Right click rows and use the context menu to enable/disable rows. ```ts import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, RowDisabledStateObject, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { const [rowDisabledState, setRowDisabledState] = React.useState< RowDisabledStateObject >({ enabledRows: true, disabledRows: [1, 3, 4, 5], }); return ( <> data={data} primaryKey="id" rowDisabledState={rowDisabledState} onRowDisabledStateChange={(rowState) => { setRowDisabledState(rowState.getState()); }} > debugId="rowDisabledState-example" getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { const rowDisabled = dataSourceApi.isRowDisabledAt( rowInfo.indexInAll, ); return { columns: [{ name: 'label' }], items: [ { label: 'Disable row', disabled: rowDisabled, key: 'disable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); hideMenu(); }, }, { label: 'Enable row', disabled: !rowDisabled, key: 'enable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled(rowInfo.id, true); hideMenu(); }, }, { label: 'Toggle row enable/disable', key: 'toggle-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled( rowInfo.id, dataSourceApi.isRowDisabled(rowInfo.id), ); hideMenu(); }, }, ], }; }} keyboardNavigation="row" columnDefaultWidth={120} columnMinWidth={50} columns={columns} /> ); }; ``` ### treeSelection (`TreeSelectionValue`) > Determines what nodes are selected and deselected. For the uncontrolled version see [`defaultTreeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeSelection). The value of this prop determines if a node is selected or not. See [`TreeSelectionValue`](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeSelectionValue) for details on the shape of this object. **Example: Using controlled tree selection** ```ts import { InfiniteTableColumn, TreeDataSource, TreeGrid, TreeSelectionValue, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeSelection, setTreeSelection] = useState({ defaultSelection: false, selectedPaths: [['1', '10'], ['3']], }); return ( <>
Current tree selection:
{JSON.stringify(treeSelection, null, 2)}
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### onTreeSelectionChange (`(treeSelection: TreeSelectionValue, context) => void`) > Called when the tree selection changes. See [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection). **Example: Reacting to tree selection changes** ```ts import { InfiniteTableColumn, TreeDataSource, TreeGrid, TreeSelectionValue, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeSelection, setTreeSelection] = useState({ defaultSelection: false, selectedPaths: [['1', '10'], ['3']], }); return ( <>
Current tree selection:
{JSON.stringify(treeSelection, null, 2)}
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` When using `multi-row` [`selectionMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode), the signature of this callback is: - `treeSelection` - the new [tree selection state](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeSelectionValue) - `context` - an object with the following properties: - `selectionMode` - will be `"multi-row"` - `lastUpdatedNodePath` - the path of the node that was last updated (either via user action or api call). Will be `null` of the action that triggered this callback was [`selectAll`](https://infinite-table.com/docs/reference/tree-api/index.md#selectAll) or [`deselectAll`](https://infinite-table.com/docs/reference/tree-api/index.md#deselectAll). - `dataSourceApi` - the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) instance ### defaultTreeSelection (`TreeSelectionValue`) > Determines what nodes are selected and deselected. For the controlled version see [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection). The value of this prop determines if a node is selected or not. See [`TreeSelectionValue`](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeSelectionValue) for details on the shape of this object. **Example: Using uncontrolled tree selection** ```ts import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, TreeSelectionValue, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; const defaultTreeSelection: TreeSelectionValue = { defaultSelection: false, selectedPaths: [['1', '10'], ['3']], }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(null); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### onRowDisabledStateChange (`(rowDisabledState) => void`) > Called when the row disabled state changes. It's called with just 1 argument (`rowDisabledState`), which is an instance of the `RowDisabledState` class. To get a literal object that represents the row disabled state, call the `rowDisabledState.getState()` method. ```tsx {3,19} import { DataSource, RowDisabledStateObject, } from '@infinite-table/infinite-react'; function App() { const [rowDisabledState, setRowDisabledState] = React.useState< RowDisabledStateObject >({ enabledRows: true, disabledRows: [1, 3, 4, 5], }); return ( <> data={data} primaryKey="id" rowDisabledState={rowDisabledState} onRowDisabledStateChange={(rowState) => { setRowDisabledState(rowState.getState()); }} /> ); } ``` When using the controlled [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) prop, you will need to update the `rowDisabledState` by using this callback. **Example: Using the onRowDisabledStateChange callback to update row disabled state** Rows with ids `1`, `3`, `4` and `5` are disabled initially. Right click rows and use the context menu to enable/disable rows. ```ts import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, RowDisabledStateObject, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; export default () => { const [rowDisabledState, setRowDisabledState] = React.useState< RowDisabledStateObject >({ enabledRows: true, disabledRows: [1, 3, 4, 5], }); return ( <> data={data} primaryKey="id" rowDisabledState={rowDisabledState} onRowDisabledStateChange={(rowState) => { setRowDisabledState(rowState.getState()); }} > debugId="rowDisabledState-example" getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { const rowDisabled = dataSourceApi.isRowDisabledAt( rowInfo.indexInAll, ); return { columns: [{ name: 'label' }], items: [ { label: 'Disable row', disabled: rowDisabled, key: 'disable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); hideMenu(); }, }, { label: 'Enable row', disabled: !rowDisabled, key: 'enable-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled(rowInfo.id, true); hideMenu(); }, }, { label: 'Toggle row enable/disable', key: 'toggle-row', onAction: ({ hideMenu }) => { dataSourceApi.setRowEnabled( rowInfo.id, dataSourceApi.isRowDisabled(rowInfo.id), ); hideMenu(); }, }, ], }; }} keyboardNavigation="row" columnDefaultWidth={120} columnMinWidth={50} columns={columns} /> ); }; ``` ### aggregationReducers (`Record`) > Specifies the functions to use for aggregating data. The object is a map where the keys are ids for aggregations and values are object of the shape described below. The `DataSourceAggregationReducer` type can have the following properties - `initialValue` - type `any`, mandatory for client-side aggregations. It can be a function, in which case, it will be called to compute the initial value for the aggregation. Otherwise, the initial value will be used as is. - `field` - the field to aggregate on. Optional - if not specified, make sure you specify `getter` - `getter`: `(data:T)=> any` - a getter function, called with the current `data` object. - `reducer`: `string | (accumulator, value, data: T) => any` - either a string (for server-side aggregations) or a mandatory aggregation function for client-side aggregations. - `done`: `(accumulator, arr: T[]) => any` - a function that is called to finish the aggregation after all values have been accumulated. The function should return the final value of the aggregation. Only used for client-side aggregations. - `name` - useful especially in combination with [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy), as it will be used as the pivot column header. - `pivotColumn` - if specified, will configure the pivot column generated for this aggregation. This object has the same shape as a normal [column](https://infinite-table.com/docs/reference/infinite-table-props.md#columns), but supports an extra `inheritFromColumn` property, which can either be a `string` (a column id), or a `boolean`. The default behavior for a pivot column is to inherit the configuration of the initial column that has the same `field` property. `inheritFromColumn` allows you to specify another column to inherit from, or, if `false` is passed, the pivot column will not inherit from any other column. **Example: Aggregation demo - see `salary` column** ```ts files=["groupBy-example.page.tsx","columns.ts"] ``` Aggregation reducers can be used in combination with grouping and pivoting. The example below shows aggregations used with server-side pivoting **Example: Aggregations used together with server-side pivoting** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns, InfiniteTablePropColumnPinning, DataSourceData, DataSourcePropAggregationReducers, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const DATA_SOURCE_SIZE = '10k'; const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, groupKeys = [], }) => { const args = [ pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers${DATA_SOURCE_SIZE}-sql?` + args, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { // the aggregation name will be used as the column header name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; // make the row labels column (id: 'labels') be pinned const defaultColumnPinning: InfiniteTablePropColumnPinning = { // make the generated group columns pinned to start 'group-by-country': 'start', 'group-by-stack': 'start', }; // make all rows collapsed by default const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function RemotePivotExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', column: { // give the group column for the country prop a custom id id: 'group-by-country', }, }, { field: 'stack', column: { // give the group column for the stack prop a custom id id: 'group-by-stack', }, }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'preferredLanguage' }, { field: 'canDesign', // customize the column group columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: `${ columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer' }`, }; }, // customize columns generated under this column group column: ({ column }) => ({ ...column, header: `🎉 ${column.header}`, }), }, ], [], ); return ( primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={aggregationReducers} defaultGroupRowsState={groupRowsState} lazyLoad={true} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="remote-pivoting-example" defaultColumnPinning={defaultColumnPinning} columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={220} /> ); }} ); } ``` Pivot columns generated for aggregations will inehrit from initial columns - the example shows how to leverage this behavior and how to extend it **Example: Pivot columns inherit from original columns bound to the same field** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropAggregationReducers, InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const columnAggregations: DataSourcePropAggregationReducers = { avgSalary: { field: 'salary', name: 'Average salary', ...avgReducer, }, avgAge: { field: 'age', ...avgReducer, pivotColumn: { defaultWidth: 500, inheritFromColumn: 'firstName', }, }, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName', style: { fontWeight: 'bold', }, renderValue: ({ value }) => <>{value}!, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', header: 'Salary', style: { color: 'red' }, }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function PivotByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={columnAggregations} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivot-column-inherit-example" columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={180} /> ); }} ); } ``` ### data (`DATA_TYPE[]|Promise DATA_TYPE[]|Promise`) > Specifies the data the component is bound to. Can be one of the following: - an array of the bound type - eg: `Employee[]` - a Promise tha resolves to an array like the above - a function that returns an any of the above If the `data` prop is a function, it will be called with an object of type [`DataSourceDataParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceDataParams). [Click to see more details.](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceDataParams) **Example: Data loading example with promise** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Employee = { id: string | number; name: string; salary: number; department: string; company: string; }; const employees: Employee[] = [ { id: 1, name: 'Bob', salary: 10_000, department: 'IT', company: 'Bobsons', }, { id: 2, name: 'Alice', salary: 20_000, department: 'IT', company: 'Bobsons', }, { id: 3, name: 'John', salary: 30_000, department: 'IT', company: 'Bobsons', }, { id: 4, name: 'Jane', salary: 35_000, department: 'Marketing', company: 'Janies', }, { id: 5, name: 'Mary', salary: 40_000, department: 'Marketing', company: 'Janies', }, ]; // this can be an array, a promise or a function returning array/promise const data = new Promise((resolve) => { setTimeout(() => { resolve(employees); }, 100); }); export default function App() { return ( data={data} primaryKey="id"> debugId="data-example" columnDefaultWidth={130} columns={columns} /> ); } const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 80, }, name: { field: 'name', }, salary: { field: 'salary', type: 'number' }, department: { field: 'department', header: 'Dep.' }, company: { field: 'company' }, }; ``` It's important to note you can re-fetch data by changing the reference you pass as the `data` prop to the `` component. Passing another `data` function, will cause the component to re-execute the function and thus load new data. **Example: Re-fetching data** ```ts files=["$DOCS/learn/working-with-data/refetch-example.page.tsx","$DOCS/learn/working-with-data/columns.ts"] ``` ### defaultFilterValue (`{field?, id?, filter: {type, operator, value}[]`) > Uncontrolled prop used for filtering. Can be used for both [client-side](https://infinite-table.com/docs/learn/filtering/filtering-client-side.md) and [server-side](https://infinite-table.com/docs/learn/filtering/filtering-server-side.md) filtering. If you want to show the column filter editors, you have to either specify this property, or the controlled [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) - even if you have no initial filters. For no initial filters, use `defaultFilterValue=[]`. For the controlled version, and more details on the shape of the objects in the array, see [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue). **Example: Initial filtering applied via defaultFilterValue** ```ts import * as React from 'react'; import { DataSource, DataSourceData, DataSourceProps, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; const defaultFilterValue: DataSourceProps['filterValue'] = [ { field: 'salary', filter: { operator: 'gt', value: 50000, type: 'number', }, }, ]; export default () => { return ( <>

{`By default, only showing records with salary > 50000`}

data={data} primaryKey="id" defaultFilterValue={defaultFilterValue} filterDelay={0} filterMode="local" > debugId="defaultFilterValue-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} />
); }; ``` You can control the visibility of the column filters by using the [`showColumnFilters`](https://infinite-table.com/docs/reference/infinite-table-props.md#showColumnFilters) prop. ### defaultRowSelection (`string|number|null|object`) > Describes the selected row(s) in the `DataSource` See more docs in the controlled version of this prop, [`rowSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowSelection) For single selection, the prop will be of type: `number | string | null`. Use `null` for empty selection in single selection mode. For multiple selection, the prop will have the following shape: ```ts const rowSelection = { selectedRows: [3, 6, 100, 23], // those specific rows are selected defaultSelection: false, // all rows deselected by default }; // or const rowSelection = { deselectedRows: [3, 6, 100, 23], // those specific rows are deselected defaultSelection: true, // all other rows are selected }; // or, for grouped data - this example assumes groupBy=continent,country,city const rowSelection = { selectedRows: [ 45, // row with id 45 is selected, no matter the group ['Europe', 'France'], // all rows in Europe/France are selected ['Asia'], // all rows in Asia are selected ], deselectedRows: [ ['Europe', 'France', 'Paris'], // all rows in Paris are deselected ], defaultSelection: false, // all other rows are selected }; ``` For using group keys in the selection value, see related [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) **Example: Uncontrolled, multiple row selection with checkbox column** ```ts import { InfiniteTable, DataSource, DataSourcePropRowSelection, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', renderSelectionCheckBox: true, }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; const defaultRowSelection: DataSourcePropRowSelection = { selectedRows: [3, 5], defaultSelection: false, }; export default function App() { return ( <> data={dataSource} defaultRowSelection={defaultRowSelection} primaryKey="id" > debugId="uncontrolled-multiple-row-selection-example" columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### defaultSortInfo (`DataSourceSingleSortInfo|DataSourceSingleSortInfo[]|null`) > Information for sorting the data. This is an uncontrolled prop. For detailed explanations, see [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) (controlled property). When you provide a `defaultSortInfo` prop and the sorting information uses a custom [sortType](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes), make sure you specify that as the `type` property of the sorting info object. ```tsx defaultSortInfo={{ field: 'color', dir: 1, // note this custom sort type type: 'color', }} ``` You will need to have a property for that type in your [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) object as well. ```tsx sortTypes={{ color: (a, b) => //... }} ``` **Example: Local uncontrolled single sorting** ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: false, }; export default function LocalUncontrolledSingleSortingExampleWithRemoteData() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={{ field: 'salary', dir: -1 }} shouldReloadData={shouldReloadData} > debugId="local-uncontrolled-single-sorting-example-with-remote-data" columns={columns} columnDefaultWidth={220} /> ); } ``` **Example: Custom sort by color - magenta will come first** ```ts import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export type CarSale = { id: number; make: string; model: string; year: number; sales: number; color: string; }; const carsales: CarSale[] = [ { make: 'Volkswagen', model: 'GTI', year: 2009, sales: 6, color: 'red', id: 0, }, { make: 'Honda', model: 'Element 2WD', year: 2009, sales: 739, color: 'red', id: 1, }, { make: 'Acura', model: 'RDX 4WD', year: 2008, sales: 2, color: 'magenta', id: 2, }, { make: 'Honda', model: 'Fit', year: 2009, sales: 211, color: 'blue', id: 3, }, { make: 'Mazda', model: '6', year: 2009, sales: 31, color: 'blue', id: 4, }, { make: 'Acura', model: 'TSX', year: 2009, sales: 14, color: 'yellow', id: 5, }, { make: 'Acura', model: 'TSX', year: 2010, sales: 14, color: 'red', id: 6, }, { make: 'Audi', model: 'A3', year: 2009, sales: 2, color: 'magenta', id: 7, }, ]; const columns: Record> = { color: { field: 'color', sortType: 'color' }, make: { field: 'make' }, model: { field: 'model' }, sales: { field: 'sales', sortType: 'number', }, year: { field: 'year', sortType: 'number', }, }; const newSortTypes = { color: (one: string, two: string) => { if (one === 'magenta') { // magenta comes first return -1; } if (two === 'magenta') { // magenta comes first return 1; } return one.localeCompare(two); }, }; export default function DataTestPage() { return ( data={carsales} primaryKey="id" defaultSortInfo={{ field: 'color', dir: 1, type: 'color', }} sortTypes={newSortTypes} > debugId="customSortType-with-uncontrolled-sortInfo-example" columns={columns} /> ); } ``` ### filterDelay (`number`) > The delay in milliseconds before the filter is applied. This is useful when you want to wait for the user to finish typing before applying the filter. This is especially useful in order to reduce the number of requests sent to the server, when [remote filtering](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode) is used. If not specified, defaults to `200` milliseconds. This means, any changes to the column filters, that happen inside a 200ms window (or the current value of [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay)), will be debounced and only the last value will be sent to the server. If you want to prevent debouncing/batching filter values, you can set [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) to `0`. ### batchOperationDelay (`number`) > The delay in milliseconds to wait before mutations are applied. This is useful to batch multiple mutations together. If not specified, a `requestAnimationFrame` will be used to batch mutations. The following mutative operations are batched: - [`addData`](https://infinite-table.com/docs/reference/datasource-api/index.md#addData) - [`addDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#addDataArray) - [`insertData`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertData) - [`insertDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertDataArray) - [`updateData`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateData) - [`updateDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataArray) - [`removeData`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeData) - [`removeDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArray) - [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByPrimaryKey) - [`removeDataArrayByPrimaryKeys`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArrayByPrimaryKeys) - [`replaceAllData`](https://infinite-table.com/docs/reference/datasource-api/index.md#replaceAllData) - [`clearAllData`](https://infinite-table.com/docs/reference/datasource-api/index.md#clearAllData) ### treeFilterFunction (`({ data, filterTreeNode, primaryKey }) => DATA_TYPE | boolean`) > A function to be used for filtering a `TreeDataSource`. The function should return a boolean value or a data object. - when returning `false` the current data object will be filtered out. - when returning `true`, the current data object will be included in the filtered data, with no changes. - when returning a data object, the object will be used instead of the current data object for the row. This means that you can modify the data object to only include some of its children (which match a specific criteria) The `treeFilterFunction` is called with an object that has a `filterTreeNode` function property. This function is a helper function you can use to continue the filtering further down the tree on the current (non-leaf) node. This function will call the filtering function for each child of the current node. If all the children are filtered out, the current node will be filtered out as well. If there are any children that match the criteria, a clone of the current node will be returned with only the matching children. You can opt to not use this helper function, and instead implement your own filtering logic. In this case, make sure you don't mutate data objects but rather return cloned versions of them. **Example: Tree filtering via treeFilterFunction** ```ts file=tree-filter-function-example.page.tsx ``` ### filterFunction (`({ data, dataArray, index, primaryKey }) => boolean`) > A function to be used for client-side filtering. Using this function will not show any special filtering UI for columns. For filtering when using a `TreeGrid`, see [`treeFilterFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeFilterFunction). **Example: Custom filterFunction example** Loads data from remote location but will only show rows that have `id > 100`. ```ts import * as React from 'react'; import { DataSourceData, DataSource, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', filterType: 'salary', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency', defaultFilterable: false }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" filterFunction={({ data }) => { return data.id > 100; }} > debugId="custom-filter-function-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ### shouldReloadData.filterValue (`boolean`) > Explicitly configures where filtering will take place and if changes in the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) should trigger a reload of the data source - applicable when [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) is a function. Replaces the deprecated [`filterMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode) - `false` (the default) - filtering will be done on the client side and the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function will not be invoked again. - `true` - filtering will be done on the server side - the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function will be called with an object that includes the `filterValue` property, so it can be sent to the server ### filterMode (`'local'|'remote'`) > Explicitly configures where filtering will take place. Update to use the [`shouldReloadData.filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.filterValue) prop. - `'local'` - filtering will be done on the client side - `'remote'` - filtering will be done on the server side - the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function will be called with an object that includes the `filterValue` property, so it can be sent to the server ### filterTypes (`Record`) > Specifies the available types of filters for the columns. A filter type is a concept that defines how a certain type of data is to be filtered. A filter type will have a key, used to define the filter in the `filterTypes` object, and also the following properties: - `label` - `emptyValues` - an array of values considered to be empty values - when any of these values is used in the filter, the filter will match all records. - `operators` - an array of operator this filter type supports - `defaultOperator` - the default operator for the filter type - `components` - an object that describes the custom components to be used for the filter type - `FilterEditor` - a custom filter editor component for this filter type - `FilterOperatorSwitch` - a custom component that is displayed at the left of the `FilterEditor` and can be used for switching between operators - only needed for very very advanced use-cases. Let's imagine you have a `DataSource` with developers, each with a `salary` column, and for that column you want to allow `>`, `>=`, `<` and `<=` comparisons (operators). For this, you would define the following filter type: ```tsx const filterTypes = { income: { label: 'Income', emptyValues: ['', null, undefined], defaultOperator: 'gt', operators: [ { name: 'gt', label: 'Greater than', fn: ({ currentValue, filterValue }) => { return currentValue > filterValue; }, }, { name: 'gte', //... }, { name: 'lt', //... }, { name: 'lte', //... }, ], }, }; ``` Each operator for a certain filter type needs to at least have a `name` and `fn` defined. The `fn` property is a function that will be called when client-side filtering is enabled, with an object that has the following properties: - `currentValue` - the cell value of the current row for the column being filtered - `filterValue` - the value of the filter editor - `emptyValues` - the array of values considered to be empty values for the filter type - `data` - the current row data object - `typeof DATA_TYPE` - `index` - the index of the current row in the table - `number` - `dataArray` - the array of all rows originally in the table - `typeof DATA_TYPE[]` - `field?` - the field the current column is bound to (can be undefined if the column is not bound to a field) **Example: Custom filter type used for the salary column** The `salary` column has a custom filter type, with the following operators: `gt`, `gte`, `lt` and `lte`. ```ts import * as React from 'react'; import { DataSource, DataSourceData, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', filterType: 'salary', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency', defaultFilterable: false }, }; function getIcon(icon: string) { return () => (
{icon}
); } const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" filterTypes={{ salary: { defaultOperator: 'gt', emptyValues: ['', null, undefined], operators: [ { name: 'gt', label: 'Greater Than', components: { Icon: getIcon('>'), }, fn: ({ currentValue, filterValue }) => { return currentValue > filterValue; }, }, { name: 'gte', components: { Icon: getIcon('>='), }, label: 'Greater Than or Equal', fn: ({ currentValue, filterValue }) => { return currentValue >= filterValue; }, }, { name: 'lt', components: { Icon: getIcon('<'), }, label: 'Less Than', fn: ({ currentValue, filterValue }) => { return currentValue < filterValue; }, }, { name: 'lte', components: { Icon: getIcon('<='), }, label: 'Less Than or Equal', fn: ({ currentValue, filterValue }) => { return currentValue <= filterValue; }, }, ], }, }} > debugId="filter-types-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` By default, the `string` and `number` filter types are available. You can import the default filter types like this: ```ts import { defaultFilterTypes } from '@infinite-table/infinite-react'; ``` If you want to make all your instances of `InfiniteTable` have new operators for those filter types, you can simply mutate the exported `defaultFilterTypes` object. **Example: Enhanced string filter type - new 'Not includes' operator** The `string` columns have a new `Not includes` operator. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, defaultFilterTypes, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; defaultFilterTypes.string.operators.push({ name: 'Not includes', label: 'Not Includes', fn: ({ currentValue, filterValue }) => { return ( typeof currentValue === 'string' && typeof filterValue == 'string' && !currentValue.toLowerCase().includes(filterValue.toLowerCase()) ); }, }); const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency', defaultFilterable: false }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" > debugId="default-filter-types-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` When you specify new [`filterTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes), the default filter types of `string` and `number` are still available - unless the new object contains those keys and overrides them explicitly. The current implementation of the default filter types is the following: ```tsx export const defaultFilterTypes: Record> = { string: { label: 'Text', emptyValues: [''], defaultOperator: 'includes', components: { FilterEditor: StringFilterEditor, }, operators: [ { name: 'includes', components: { Icon: // custom icon as a React component ... }, label: 'includes', fn: ({ currentValue, filterValue }) => { return ( typeof currentValue === 'string' && typeof filterValue == 'string' && currentValue.toLowerCase().includes(filterValue.toLowerCase()) ); }, }, { label: 'Equals', components: { Icon: // custom icon as a React component ... }, name: 'eq', fn: ({ currentValue: value, filterValue }) => { return typeof value === 'string' && value === filterValue; }, }, { name: 'startsWith', components: { Icon: // custom icon as a React component ... }, label: 'Starts With', fn: ({ currentValue: value, filterValue }) => { return value.startsWith(filterValue); }, }, { name: 'endsWith', components: { Icon: // custom icon as a React component ... }, label: 'Ends With', fn: ({ currentValue: value, filterValue }) => { return value.endsWith(filterValue); }, }, ], }, number: { label: 'Number', emptyValues: ['', null, undefined], defaultOperator: 'eq', components: { FilterEditor: NumberFilterEditor, }, operators: [ { label: 'Equals', components: { Icon: // custom icon as a React component ... }, name: 'eq', fn: ({ currentValue, filterValue }) => { return currentValue == filterValue; }, }, { label: 'Not Equals', components: { Icon: // custom icon as a React component ... }, name: 'neq', fn: ({ currentValue, filterValue }) => { return currentValue != filterValue; }, }, { name: 'gt', label: 'Greater Than', components: { Icon: // custom icon as a React component ... }, fn: ({ currentValue, filterValue, emptyValues }) => { if (emptyValues.includes(currentValue)) { return true; } return currentValue > filterValue; }, }, { name: 'gte', components: { Icon: // custom icon as a React component ... }, label: 'Greater Than or Equal', fn: ({ currentValue, filterValue, emptyValues }) => { if (emptyValues.includes(currentValue)) { return true; } return currentValue >= filterValue; }, }, { name: 'lt', components: { Icon: // custom icon as a React component ... }, label: 'Less Than', fn: ({ currentValue, filterValue, emptyValues }) => { if (emptyValues.includes(currentValue)) { return true; } return currentValue < filterValue; }, }, { name: 'lte', components: { Icon: // custom icon as a React component ... }, label: 'Less Than or Equal', fn: ({ currentValue, filterValue, emptyValues }) => { if (emptyValues.includes(currentValue)) { return true; } return currentValue <= filterValue; }, } ], }, }; ``` ### filterTypes.components.FilterEditor > A custom React component to be used as an editor for the current filter type Every filter type can define the following `components` - `FilterEditor` - a React component to be used as an editor for the current filter type - `FilterOperatorSwitch` - a custom component that is displayed at the left of the `FilterEditor` and can be used for switching between operators - only needed for very very advanced use-cases. Filter type operators can override the `FilterEditor` component - they can specify the following components: - `FilterEditor` - if specified, it overrides the `FilterEditor` of the filter type - `Icon` - a React component to be used as an icon for the operator - displayed by the menu triggered when clicking on the `FilterOperatorSwitch` component **Example: Demo of a custom filter editor** The `canDesign` column is using a custom `bool` filter type with a custom filter editor. The checkbox has indeterminate state, which will match all values in the data source. ```ts import * as React from 'react'; import { InfiniteTable, InfiniteTablePropColumns, DataSource, components, useInfiniteColumnFilterEditor, } from '@infinite-table/infinite-react'; const { CheckBox } = components; type Developer = { id: number; firstName: string; canDesign: boolean; stack: string; hobby: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', canDesign: true, stack: 'frontend', hobby: 'gaming', }, { id: 2, firstName: 'Jane', canDesign: false, stack: 'backend', hobby: 'reading', }, { id: 3, firstName: 'Jack', canDesign: true, stack: 'frontend', hobby: 'gaming', }, { id: 4, firstName: 'Jill', canDesign: false, stack: 'backend', hobby: 'reading', }, { id: 5, firstName: 'Seb', canDesign: false, stack: 'backend', hobby: 'reading', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, canDesign: { field: 'canDesign', filterType: 'bool', renderValue: ({ value }) => (value ? 'Yes' : 'No'), }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, }; const domProps = { style: { height: '100%', }, }; function BoolFilterEditor() { const { value, setValue, className } = useInfiniteColumnFilterEditor(); return (
{ if (value === true) { // after the value was true, make it go to indeterminate state newValue = null; } if (value === null) { // from indeterminate, goto false newValue = false; } setValue(newValue); }} />
); } export default () => { return ( <> data={dataSource} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterTypes={{ bool: { defaultOperator: 'eq', emptyValues: [null], components: { FilterEditor: BoolFilterEditor, FilterOperatorSwitch: () => null, }, operators: [ { name: 'eq', label: 'Equals', fn: ({ currentValue, filterValue }) => currentValue === filterValue, }, ], }, }} > debugId="custom-filter-editor-hooks-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ### filterValue (`{field?, id?, filter: {type, operator, value}[]`) > Controlled prop used for filtering. Can be used for both [client-side](https://infinite-table.com/docs/learn/filtering/filtering-client-side.md) and [server-side](https://infinite-table.com/docs/learn/filtering/filtering-server-side.md) filtering. For the uncontrolled version, see [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue) If you want to show the column filter editors, you have to either specify this property, or the uncontrolled [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue) - even if you have no initial filters. For no initial filters, use `filterValue=[]`. The objects in this array have the following shape: - `filter` - an object describing the filter - `filter.value` - the value to filter by - `filter.type` - the current type of the filter (eg: `string`, `number` or another custom type you specify in the [filterTypes](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes) prop) - `filter.operator` - the name of the operator being applied - `field` - the field being filtered - generally matched with a column. This is optional, as some columns can have no field. - `id` - the id of the column being filtered. This is optional - for columns bound to a field, the `field` should be used instead of the `id`. - `disabled` - whether this filter is applied or not **Example: Controlled filters with onFilterValueChange** ```ts import * as React from 'react'; import { DataSource, DataSourceData, DataSourceProps, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default () => { const [filterValue, setFilterValue] = React.useState< DataSourceProps['filterValue'] >([ { field: 'salary', filter: { operator: 'gt', value: 50000, type: 'number', }, }, ]); return ( <>
Current filters:{' '}
          {JSON.stringify(filterValue, null, 2)}
        
data={data} primaryKey="id" filterValue={filterValue} onFilterValueChange={setFilterValue} filterDelay={0} filterMode="local" > debugId="onFilterValueChange-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` You can control the visibility of the column filters by using the [`showColumnFilters`](https://infinite-table.com/docs/reference/infinite-table-props.md#showColumnFilters) prop. ### lazyLoad (`boolean|{batchSize:number}`) > Whether the datasource will load data lazily - useful for server-side grouping and pivoting. If set to `true` or to an object (with `batchSize` property), the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) prop must be a function that returns a promise. **Example: Server-side pivoting with full lazy load** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns, InfiniteTablePropColumnPinning, DataSourceData, DataSourcePropAggregationReducers, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const DATA_SOURCE_SIZE = '10k'; const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, groupKeys = [], }) => { const args = [ pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers${DATA_SOURCE_SIZE}-sql?` + args, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { // the aggregation name will be used as the column header name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; // make the row labels column (id: 'labels') be pinned const defaultColumnPinning: InfiniteTablePropColumnPinning = { // make the generated group columns pinned to start 'group-by-country': 'start', 'group-by-stack': 'start', }; // make all rows collapsed by default const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function RemotePivotExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', column: { // give the group column for the country prop a custom id id: 'group-by-country', }, }, { field: 'stack', column: { // give the group column for the stack prop a custom id id: 'group-by-stack', }, }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'preferredLanguage' }, { field: 'canDesign', // customize the column group columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: `${ columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer' }`, }; }, // customize columns generated under this column group column: ({ column }) => ({ ...column, header: `🎉 ${column.header}`, }), }, ], [], ); return ( primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={aggregationReducers} defaultGroupRowsState={groupRowsState} lazyLoad={true} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="remote-pivoting-example" defaultColumnPinning={defaultColumnPinning} columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={220} /> ); }} ); } ``` ### groupRowsState (`{collapsedRows:true|[][], expandedRows:true|[][]}`) > Controls the expand/collapse state of group rows, when [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) is used See related [`defaultGroupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultGroupRowsState), [`onGroupRowsStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onGroupRowsStateChange) and [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) ```tsx title="Specifying the state for group rows" const groupRowsState: DataSourcePropGroupRowsStateObject = { collapsedRows: true, expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']], }; ``` The two properties in this object are `collapsedRows` and `expandedRows`, and each can have the following values: - `true` - meaning that all groups have this state - an array of arrays - representing the exceptions to the default value So if you have `collapsedRows` set to `true` and then `expandedRows` set to `[['Mexico'], ['Mexico', 'backend'], ['India']]` then all rows are collapsed by default, except the rows specified in the `expandedRows`. **Example: Using controlled expanded/collapsed state for group rows** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, DataSourcePropGroupRowsStateObject, GroupRowsState, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'country', }, { field: 'stack', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, }; export default function App() { const [groupRowsState, setGroupRowsState] = React.useState< DataSourcePropGroupRowsStateObject >(() => { return { collapsedRows: true, expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']], }; }); const onGroupRowsStateChange = React.useCallback( (groupRowsState: GroupRowsState) => { setGroupRowsState(groupRowsState.getState()); }, [], ); return ( <> data={dataSource} primaryKey="id" groupBy={groupBy} groupRowsState={groupRowsState} onGroupRowsStateChange={onGroupRowsStateChange} > debugId="group-rows-state-controlled-example" columns={columns} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### onGroupRowsStateChange (`(state: GroupRowsState) => void`) > Callback prop when the [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState) changes. See related [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState) and [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) This function is called with an object that's an instance of [`GroupRowsState`](https://infinite-table.com/docs/reference/type-definitions/index.md#GroupRowsState), when the user interacts with group rows and expands/collapses them. If you want to get a plain object from this instance, call the `.getState()` method. See [`GroupRowsState`](https://infinite-table.com/docs/reference/type-definitions/index.md#GroupRowsState) reference to find out all the utility methods this instance gives you. ### defaultGroupRowsState (`{collapsedRows:true|[][], expandedRows:true|[][]}`) > Specifies the initial expand/collapse state of group rows, when [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) is used For the controlled version, see related [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState). ```tsx title="Specifying the initial state for group rows" const defaultGroupRowsState: DataSourcePropGroupRowsStateObject = { expandedRows: true, collapsedRows: [['Mexico'], ['Mexico', 'backend'], ['India']], }; ``` The two properties in this object are `collapsedRows` and `expandedRows`, and each can have the following values: - `true` - meaning that all groups have this state - an array of arrays - representing the exceptions to the default value So if you have `expandedRows` set to `true` and then `collapsedRows` set to `[['Mexico'], ['Mexico', 'backend'], ['India']]` then all rows are expanded by default, except the rows specified in the `collapsedRows`. **Example: Specifying initial expanded/collapsed state for group rows** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, DataSourcePropGroupRowsStateObject, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'country', }, { field: 'stack', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, }; const defaultGroupRowsState: DataSourcePropGroupRowsStateObject = { collapsedRows: true, expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']], }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy} defaultGroupRowsState={defaultGroupRowsState} > debugId="group-rows-initial-state-example" columns={columns} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### groupBy > An array of objects with `field` properties, that control how rows are being grouped. Each item in the array can have the following properties: - field - `keyof DATA_TYPE` - column - config object for the group [column](https://infinite-table.com/docs/reference/infinite-table-props.md#column) - see [`groupBy.column`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy.column). When using [groupRenderStrategy="multi-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy), it can be very useful for each group to configure it's own column - use [`groupBy.column`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy.column) for this. See [`DataSourcePropGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePropGroupBy) for the type definition. **Example** ```ts files=["groupBy-example.page.tsx","columns.ts"] ``` ### pivotBy (`DataSourcePivotBy[]`) > An array of objects with `field` properties that control how pivoting works. Pivoting is very often associated with aggregations, so see related [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) for more details. Each item in the array can have the following properties: - field - `keyof DATA_TYPE` - column - config object or function for generated pivot columns. For more details on the type of the items in this array prop, see [`DataSourcePivotBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePivotBy). **Example: Pivoting with customized pivot column** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropAggregationReducers, InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, field: 'salary', reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const columnAggregations: DataSourcePropAggregationReducers = { salary: avgReducer, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function PivotByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', column: ({ column: pivotCol }) => { const lastKey = pivotCol.pivotGroupKeys[pivotCol.pivotGroupKeys.length - 1]; return { header: lastKey === 'yes' ? '💅 Designer' : '💻 Non-designer', }; }, }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={columnAggregations} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivoting-customize-column-example" columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={180} /> ); }} ); } ``` ### groupBy.column (`Partial>`) > An object that configures how the column for the current group should look like If [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) is specified, it overrides this property (the objects actually get merged, with `groupColumn` having higher priority and being merged last). If you are using a [groupRenderStrategy="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy), then using `groupBy.column` should not be used, as you could have many groups with conflicting column configuration. In this case, use the [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) prop. **Example** This example uses `groupBy.column` to configure the generated columns corresponding to each group. ```ts files=["groupBy-multi-with-column-example.page.tsx","columns.ts"] ``` ### livePagination (`boolean`) > Whether the component should use live pagination. Use this in combination with [`livePaginationCursor`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePaginationCursor) and [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange) **Example: Live pagination - with react-query** ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, InfiniteTableColumn, DataSource, DataSourceSingleSortInfo, DataSourceDataParams, DataSourceLivePaginationCursorFn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useCallback } from 'react'; import { QueryClient, QueryClientProvider, useInfiniteQuery, keepPreviousData, } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, }); const emptyArray: Employee[] = []; export const columns: Record> = { id: { field: 'id' }, country: { field: 'country', }, city: { field: 'city' }, team: { field: 'team' }, department: { field: 'department' }, firstName: { field: 'firstName' }, lastName: { field: 'lastName' }, salary: { field: 'salary' }, age: { field: 'age' }, }; type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: number; department: string; team: string; salary: number; age: number; email: string; }; const PAGE_SIZE = 10; const dataSource = ({ sortInfo, livePaginationCursor = 0, }: { sortInfo: DataSourceSingleSortInfo | null; livePaginationCursor: number; }) => { return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${ sortInfo?.dir === 1 ? 'asc' : 'desc' }&_start=${livePaginationCursor}`, ) .then(async (r) => { const data = await r.json(); // we need the remote count, so we take it from headers const total = Number(r.headers.get('X-Total-Count')!); return { data, total }; }) .then(({ data, total }: { data: Employee[]; total: number }) => { const page = livePaginationCursor / PAGE_SIZE + 1; const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0); return { data, hasMore: total > PAGE_SIZE * page, page, prevPageCursor, nextPageCursor: prevPageCursor + data.length, }; }) .then( ( response, ): Promise<{ data: Employee[]; hasMore: boolean; page: number; nextPageCursor: number; prevPageCursor: number; }> => { return new Promise((resolve) => { setTimeout(() => { resolve(response); }, 150); }); }, ); }; const Example = () => { const [dataParams, setDataParams] = React.useState< Partial> >({ groupBy: [], sortInfo: undefined, livePaginationCursor: null, }); const { data, fetchNextPage: fetchNext, isFetchingNextPage, } = useInfiniteQuery({ initialPageParam: 0, queryKey: ['employees', dataParams.sortInfo, dataParams.groupBy], queryFn: ({ pageParam = 0 }) => { const params = { livePaginationCursor: pageParam, sortInfo: dataParams.sortInfo as DataSourceSingleSortInfo | null, }; return dataSource(params); }, placeholderData: keepPreviousData, getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0, getNextPageParam: (lastPage) => { const nextPageCursor = lastPage.hasMore ? lastPage.nextPageCursor : undefined; return nextPageCursor; }, select: (data) => { const flatData = data.pages.flatMap((x) => x.data); const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor; const result = { pages: flatData, pageParams: [nextPageCursor], }; return result; }, }); const onDataParamsChange = useCallback( (dataParams: DataSourceDataParams) => { const params = { groupBy: dataParams.groupBy, sortInfo: dataParams.sortInfo, livePaginationCursor: dataParams.livePaginationCursor, }; setDataParams(params); }, [], ); const [scrollTopId, setScrollTop] = React.useState(0); React.useEffect(() => { // when sorting changes, scroll to the top setScrollTop(Date.now()); }, [dataParams.sortInfo]); const fetchNextPage = () => { if (isFetchingNextPage) { return; } fetchNext(); }; React.useEffect(() => { fetchNextPage(); }, [dataParams.livePaginationCursor]); const livePaginationCursorFn: DataSourceLivePaginationCursorFn = useCallback(({ length }) => { return length; }, []); return ( primaryKey="id" // take the data from `data.pages`, // as returned from our react-query select function data={data?.pages || emptyArray} loading={isFetchingNextPage} onDataParamsChange={onDataParamsChange} livePagination livePaginationCursor={livePaginationCursorFn} > debugId="live-pagination-example" scrollTopKey={scrollTopId} columnDefaultWidth={200} columns={columns} /> ); }; function App() { return ( ); } export default App; ``` ### onDataMutations (`({ mutations, dataArray, primaryKeyField }) => void`) > Callback prop to be called when the data changes via the DataSource API. Called when any of the following methods have been called in the `DataSource` api - [`updateData`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateData) - [`updateDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataArray) - [`removeData`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeData) - [`removeDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArray) - [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByPrimaryKey) - [`removeDataArrayByPrimaryKeys`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArrayByPrimaryKeys) - [`insertData`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertData) - [`insertDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertDataArray) - [`addData`](https://infinite-table.com/docs/reference/datasource-api/index.md#addData) - [`addDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#addDataArray) This callback is called with an object that has the following properties: - `primaryKeyField` - the field configured as the primary key for the `` - `mutations` - a `Map` with mutations. The keys in the map are primary keys of the mutated data The values in the mutations are object descriptors of mutations, that have the following shape: - `type`: `'insert'|'update'|'delete'` - `originalData`: `DATA_TYPE | null` - the original data before the mutation. In case of `insert`, it will be `null` - `data`: `Partial` - the updates to be performed on the data. In case of `delete`, it will be `undefined`. This is an object that will contain the primary key, and the updated values for the data (not necessarily the full object, except for `insert`, where it will be of type `DATA_TYPE`). ### livePaginationCursor (`string|number|((params) =>string|number)`) > A cursor value for live pagination. A good value for this is the id of the last item in the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) array. It can also be defined as a function Use this in combination with [`livePagination`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePagination) and [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange) When this is a function, it is called with a parameter object that has the following properties: - `array` - the current array of data - `lastItem` - the last item in the array - `length` - the length of the data array **Example: Live pagination - with react-query** ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, InfiniteTableColumn, DataSource, DataSourceSingleSortInfo, DataSourceDataParams, DataSourceLivePaginationCursorFn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useCallback } from 'react'; import { QueryClient, QueryClientProvider, useInfiniteQuery, keepPreviousData, } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, }); const emptyArray: Employee[] = []; export const columns: Record> = { id: { field: 'id' }, country: { field: 'country', }, city: { field: 'city' }, team: { field: 'team' }, department: { field: 'department' }, firstName: { field: 'firstName' }, lastName: { field: 'lastName' }, salary: { field: 'salary' }, age: { field: 'age' }, }; type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: number; department: string; team: string; salary: number; age: number; email: string; }; const PAGE_SIZE = 10; const dataSource = ({ sortInfo, livePaginationCursor = 0, }: { sortInfo: DataSourceSingleSortInfo | null; livePaginationCursor: number; }) => { return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${ sortInfo?.dir === 1 ? 'asc' : 'desc' }&_start=${livePaginationCursor}`, ) .then(async (r) => { const data = await r.json(); // we need the remote count, so we take it from headers const total = Number(r.headers.get('X-Total-Count')!); return { data, total }; }) .then(({ data, total }: { data: Employee[]; total: number }) => { const page = livePaginationCursor / PAGE_SIZE + 1; const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0); return { data, hasMore: total > PAGE_SIZE * page, page, prevPageCursor, nextPageCursor: prevPageCursor + data.length, }; }) .then( ( response, ): Promise<{ data: Employee[]; hasMore: boolean; page: number; nextPageCursor: number; prevPageCursor: number; }> => { return new Promise((resolve) => { setTimeout(() => { resolve(response); }, 150); }); }, ); }; const Example = () => { const [dataParams, setDataParams] = React.useState< Partial> >({ groupBy: [], sortInfo: undefined, livePaginationCursor: null, }); const { data, fetchNextPage: fetchNext, isFetchingNextPage, } = useInfiniteQuery({ initialPageParam: 0, queryKey: ['employees', dataParams.sortInfo, dataParams.groupBy], queryFn: ({ pageParam = 0 }) => { const params = { livePaginationCursor: pageParam, sortInfo: dataParams.sortInfo as DataSourceSingleSortInfo | null, }; return dataSource(params); }, placeholderData: keepPreviousData, getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0, getNextPageParam: (lastPage) => { const nextPageCursor = lastPage.hasMore ? lastPage.nextPageCursor : undefined; return nextPageCursor; }, select: (data) => { const flatData = data.pages.flatMap((x) => x.data); const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor; const result = { pages: flatData, pageParams: [nextPageCursor], }; return result; }, }); const onDataParamsChange = useCallback( (dataParams: DataSourceDataParams) => { const params = { groupBy: dataParams.groupBy, sortInfo: dataParams.sortInfo, livePaginationCursor: dataParams.livePaginationCursor, }; setDataParams(params); }, [], ); const [scrollTopId, setScrollTop] = React.useState(0); React.useEffect(() => { // when sorting changes, scroll to the top setScrollTop(Date.now()); }, [dataParams.sortInfo]); const fetchNextPage = () => { if (isFetchingNextPage) { return; } fetchNext(); }; React.useEffect(() => { fetchNextPage(); }, [dataParams.livePaginationCursor]); const livePaginationCursorFn: DataSourceLivePaginationCursorFn = useCallback(({ length }) => { return length; }, []); return ( primaryKey="id" // take the data from `data.pages`, // as returned from our react-query select function data={data?.pages || emptyArray} loading={isFetchingNextPage} onDataParamsChange={onDataParamsChange} livePagination livePaginationCursor={livePaginationCursorFn} > debugId="live-pagination-example" scrollTopKey={scrollTopId} columnDefaultWidth={200} columns={columns} /> ); }; function App() { return ( ); } export default App; ``` ### onDataParamsChange (`(dataParams: DataSourceDataParams)=>void`) > A function to be called when data-related state changes. Can be used to implement [`livePagination`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePagination) The function is called with an object that has the following properties: - `sortInfo` - current sort information - see [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) for details - `groupBy` - current grouping information - see [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) for details - `filterValue` - current filtering information - see [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) for details - `livePaginationCursor` - the value for the live pagination cursor - see [`livePaginationCursor`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePaginationCursor) for details - `changes` - an object that can help you figure out what change caused `onDataParamsChange` to be called. **Example: Live pagination - with react-query** ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, InfiniteTableColumn, DataSource, DataSourceSingleSortInfo, DataSourceDataParams, DataSourceLivePaginationCursorFn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useCallback } from 'react'; import { QueryClient, QueryClientProvider, useInfiniteQuery, keepPreviousData, } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, }); const emptyArray: Employee[] = []; export const columns: Record> = { id: { field: 'id' }, country: { field: 'country', }, city: { field: 'city' }, team: { field: 'team' }, department: { field: 'department' }, firstName: { field: 'firstName' }, lastName: { field: 'lastName' }, salary: { field: 'salary' }, age: { field: 'age' }, }; type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: number; department: string; team: string; salary: number; age: number; email: string; }; const PAGE_SIZE = 10; const dataSource = ({ sortInfo, livePaginationCursor = 0, }: { sortInfo: DataSourceSingleSortInfo | null; livePaginationCursor: number; }) => { return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${ sortInfo?.dir === 1 ? 'asc' : 'desc' }&_start=${livePaginationCursor}`, ) .then(async (r) => { const data = await r.json(); // we need the remote count, so we take it from headers const total = Number(r.headers.get('X-Total-Count')!); return { data, total }; }) .then(({ data, total }: { data: Employee[]; total: number }) => { const page = livePaginationCursor / PAGE_SIZE + 1; const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0); return { data, hasMore: total > PAGE_SIZE * page, page, prevPageCursor, nextPageCursor: prevPageCursor + data.length, }; }) .then( ( response, ): Promise<{ data: Employee[]; hasMore: boolean; page: number; nextPageCursor: number; prevPageCursor: number; }> => { return new Promise((resolve) => { setTimeout(() => { resolve(response); }, 150); }); }, ); }; const Example = () => { const [dataParams, setDataParams] = React.useState< Partial> >({ groupBy: [], sortInfo: undefined, livePaginationCursor: null, }); const { data, fetchNextPage: fetchNext, isFetchingNextPage, } = useInfiniteQuery({ initialPageParam: 0, queryKey: ['employees', dataParams.sortInfo, dataParams.groupBy], queryFn: ({ pageParam = 0 }) => { const params = { livePaginationCursor: pageParam, sortInfo: dataParams.sortInfo as DataSourceSingleSortInfo | null, }; return dataSource(params); }, placeholderData: keepPreviousData, getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0, getNextPageParam: (lastPage) => { const nextPageCursor = lastPage.hasMore ? lastPage.nextPageCursor : undefined; return nextPageCursor; }, select: (data) => { const flatData = data.pages.flatMap((x) => x.data); const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor; const result = { pages: flatData, pageParams: [nextPageCursor], }; return result; }, }); const onDataParamsChange = useCallback( (dataParams: DataSourceDataParams) => { const params = { groupBy: dataParams.groupBy, sortInfo: dataParams.sortInfo, livePaginationCursor: dataParams.livePaginationCursor, }; setDataParams(params); }, [], ); const [scrollTopId, setScrollTop] = React.useState(0); React.useEffect(() => { // when sorting changes, scroll to the top setScrollTop(Date.now()); }, [dataParams.sortInfo]); const fetchNextPage = () => { if (isFetchingNextPage) { return; } fetchNext(); }; React.useEffect(() => { fetchNextPage(); }, [dataParams.livePaginationCursor]); const livePaginationCursorFn: DataSourceLivePaginationCursorFn = useCallback(({ length }) => { return length; }, []); return ( primaryKey="id" // take the data from `data.pages`, // as returned from our react-query select function data={data?.pages || emptyArray} loading={isFetchingNextPage} onDataParamsChange={onDataParamsChange} livePagination livePaginationCursor={livePaginationCursorFn} > debugId="live-pagination-example" scrollTopKey={scrollTopId} columnDefaultWidth={200} columns={columns} /> ); }; function App() { return ( ); } export default App; ``` ### onFilterValueChange (`({field?, id?, filter: {type, operator, value}[]) => void`) > Callback prop called when the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) changes. This might not be called immediately, as there might be a [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) set. **Example: Controlled filters with onFilterValueChange** ```ts import * as React from 'react'; import { DataSource, DataSourceData, DataSourceProps, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default () => { const [filterValue, setFilterValue] = React.useState< DataSourceProps['filterValue'] >([ { field: 'salary', filter: { operator: 'gt', value: 50000, type: 'number', }, }, ]); return ( <>
Current filters:{' '}
          {JSON.stringify(filterValue, null, 2)}
        
data={data} primaryKey="id" filterValue={filterValue} onFilterValueChange={setFilterValue} filterDelay={0} filterMode="local" > debugId="onFilterValueChange-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ### onLivePaginationCursorChange (`(cursor)=> void`) > A function to be called when the [`livePaginationCursor`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePaginationCursor) changes. Also see related [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange). ### onReady (`(dataSourceApi: DataSourceApi) => void`) > The callback that is called when the `DataSource` is ready. The [`dataSourceApi`](https://infinite-table.com/docs/reference/datasource-api/index.md) is passed as the first argument. ### onCellSelectionChange (`(cellSelection, selectionMode='multi-cell') => void`) > A function to be called when the [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) changes. **Example: Controlled cell selection with onCellSelectionChange** Use your mouse to select/deselect cells. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, DataSourcePropCellSelection_MultiCell, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 60 }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [cellSelection, setCellSelection] = React.useState({ defaultSelection: false, selectedCells: [ [3, 'stack'], [0, 'firstName'], ], }); return (
Current selection:
{JSON.stringify(cellSelection, null, 2)}
primaryKey="id" data={dataSource} cellSelection={cellSelection} onCellSelectionChange={setCellSelection} selectionMode="multi-cell" > debugId="controlled-cell-selection-example" columns={columns} columnDefaultWidth={100} />
); } ``` ### onRowSelectionChange (`(rowSelection, selectionMode='single-row'|'multi-row') => void`) > A function to be called when the [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) changes. **Example: Controlled row selection with onRowSelectionChange** Use your mouse or keyboard (press the spacebar) to select/deselect a single row. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { const [rowSelection, setRowSelection] = useState(3); return ( <>

Current row selection:

 {JSON.stringify(rowSelection)}.

data={dataSource} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} primaryKey="id" > debugId="controlled-single-row-selection-example" columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` **Example: Multi row checkbox selection with grouping** This example shows how you can use multiple row selection with a predefined controlled value. Go ahead and select some groups/rows and see the selection value adjust. The example also shows how you can use the `InfiniteTableApi` to retrieve the actual ids of the selected rows. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTableProps, InfiniteTablePropColumns, DataSourceProps, DataSourcePropRowSelection_MultiRow, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', renderGroupValue: ({ value }) => `Stack: ${value || ''}`, }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', renderGroupValue: ({ value }) => `Lang: ${value || ''}`, }, canDesign: { field: 'canDesign', renderGroupValue: ({ value }) => `Can design: ${value || ''}`, }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', renderSelectionCheckBox: true, defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { const [rowSelection, setRowSelection] = useState({ selectedRows: [0, 8, 10], defaultSelection: false, }); return (
Current row selection:
 {JSON.stringify(rowSelection)}.
data={dataSource} groupBy={defaultGroupBy} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} primaryKey="id" > debugId="controlled-multi-row-selection-example" columns={columns} domProps={domProps} groupColumn={groupColumn} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### onSortInfoChange (`(sortInfo | null) => void`) > Called when sorting changes on the DataSource. The sorting can change either via a user interaction or by calling an API method (from the [root API](api) or the [Column API](column-api)). See related [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) for controlled sorting and [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) for uncontrolled sorting. ### refetchKey (`string|number|object`) > A value that can be used to trigger a re-fetch of the data. By updating the value of this prop (eg: you can use it as a counter, and increment it) the `` component reloads it's [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) if it's defined as a function. More specifically, the `data` function is called again and the result will replace the current data. **Example: Re-fetching data via refetchKey updates** This example shows how you can use the `refetchKey` to trigger reloading the data ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data) => { return new Promise((resolve) => { // add a delay to make "reloading" more visible setTimeout(() => { resolve(data); }, 1000); }); }); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [refetchKey, setRefetchKey] = React.useState(0); return ( <> data={dataSource} primaryKey="id" refetchKey={refetchKey} > debugId="refetchKey-example" columns={columns} columnDefaultWidth={150} loadingText={`Refetching with key ${refetchKey}`} /> ); } ``` ### rowSelection (`string|number|null|object`) > Describes the selected row(s) in the `DataSource` For single selection, the prop will be of type: `number | string | null`. Use `null` for empty selection in single selection mode. For multiple selection, the prop will have the following shape: ```ts const rowSelection = { selectedRows: [3, 6, 100, 23], // those specific rows are selected defaultSelection: false, // all rows deselected by default }; // or const rowSelection = { deselectedRows: [3, 6, 100, 23], // those specific rows are deselected defaultSelection: true, // all other rows are selected }; // or, for grouped data - this example assumes groupBy=continent,country,city const rowSelection = { selectedRows: [ 45, // row with id 45 is selected, no matter the group ['Europe', 'France'], // all rows in Europe/France are selected ['Asia'], // all rows in Asia are selected ], deselectedRows: [ ['Europe', 'France', 'Paris'], // all rows in Paris are deselected ], defaultSelection: false, // all other rows are selected }; ``` For using group keys in the selection value, see related [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) **Example: Single row selection (controlled) with onRowSelectionChange** Use your mouse or keyboard (press the spacebar) to select/deselect a single row. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { const [rowSelection, setRowSelection] = useState(3); return ( <>

Current row selection:

 {JSON.stringify(rowSelection)}.

data={dataSource} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} primaryKey="id" > debugId="controlled-single-row-selection-example" columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` When [`lazyLoad`](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad) is being used - this means not all available groups/rows have actually been loaded yet in the dataset - we need a way to allow you to specify that those possibly unloaded rows/groups are selected or not. In this case, the `rowSelection.selectedRows`/`rowSelection.deselectedRows` arrays should not have row primary keys as strings/numbers, but rather rows/groups specified by their full path (so [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) should be set to `true`). ```ts {6} // this example assumes groupBy=continent,country,city const rowSelection = { selectedRows: [ // row with id 45 is selected - we need this because in the lazyLoad scenario, // not all parents might have been made available yet ['Europe','Italy', 'Rome', 45], ['Europe','France'], // all rows in Europe/France are selected ['Asia'] // all rows in Asia are selected ] deselectedRows: [ ['Europe','Italy','Rome'] // all rows in Rome are deselected // but note that row with id 45 is selected, so Rome will be rendered with an indeterminate selection state ], defaultSelection: false // all other rows are selected } ``` In the example above, we know that there are 3 groups (`continent`, `country`, `city`), so any item in the array that has a 4th element is a fully specified leaf node. While lazy loading, we need this fully specified path for specific nodes, so we know which group rows to render with indeterminate selection. The [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) prop can be used for both lazy and non-lazy `DataSource` components. **Example: Multi row checkbox selection with grouping** This example shows how you can use multiple row selection with a predefined controlled value. Go ahead and select some groups/rows and see the selection value adjust. The example also shows how you can use the `InfiniteTableApi` to retrieve the actual ids of the selected rows. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTableProps, InfiniteTablePropColumns, DataSourceProps, DataSourcePropRowSelection_MultiRow, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', renderGroupValue: ({ value }) => `Stack: ${value || ''}`, }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', renderGroupValue: ({ value }) => `Lang: ${value || ''}`, }, canDesign: { field: 'canDesign', renderGroupValue: ({ value }) => `Can design: ${value || ''}`, }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', renderSelectionCheckBox: true, defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { const [rowSelection, setRowSelection] = useState({ selectedRows: [0, 8, 10], defaultSelection: false, }); return (
Current row selection:
 {JSON.stringify(rowSelection)}.
data={dataSource} groupBy={defaultGroupBy} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} primaryKey="id" > debugId="controlled-multi-row-selection-example" columns={columns} domProps={domProps} groupColumn={groupColumn} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### selectionMode (`'single-row'|'multi-row'|'multi-cell'|false`) > Specifies the type of selection that should be enabled. Read more on row selection (`multi-row` and `single-row`). Read more on cell selection (`multi-cell` and `single-cell`). **Example: Choose your selection mode between multi cell or multi row** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 60 }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function App() { const [selectionMode, setSelectionMode] = React.useState< 'multi-cell' | 'multi-row' >('multi-cell'); const currentColumns = React.useMemo(() => { return { ...columns, id: { field: 'id', defaultWidth: 60, renderSelectionCheckBox: true, }, } as InfiniteTablePropColumns; }, [selectionMode]); return (

Please select the selection mode

primaryKey="id" data={dataSource} selectionMode={selectionMode} > debugId="selectionMode-example" columns={currentColumns} columnDefaultWidth={100} />
); } ``` ### sortFunction (`(sortInfo:DataSourceSingleSortInfo[], arr: T[]) => T[]`) > Custom sorting function to replace the `multisort` function used by default. The function specified in the [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) prop is called with the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) as the first argument and the data array as the second. It should return a sorted array, as per the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) it was called with. When [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) is specified, [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) will be forced to `false`, as the sorting is done in the browser. The `@infinite-table/infinite-react` package exports a `multisort` function - this is the default function used for local sorting. ```ts import { multisort } from '@infinite-table/infinite-react'; const arr: Developer[] = [ /*...*/ ]; const sortInfo = [ { field: 'age', dir: -1, }, { field: 'name', dir: 1, }, ]; multisort(sortInfo, arr); ``` If you want to implement your own custom sort function, the `multisort` fn is a good starting point you can use. **Example: Using a custom sortFunction** ```ts import { InfiniteTable, DataSource, DataSourceSingleSortInfo, multisort, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const defaultSortInfo: DataSourceSingleSortInfo = { field: 'stack', dir: 1, }; const sortFunction = ( sortInfo: DataSourceSingleSortInfo[], arr: Developer[], ) => { // you call the default sorting const result = multisort(sortInfo, arr); // and also apply your custom sorting // result.sort((a, b) => { // }) return result; }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={defaultSortInfo} sortFunction={sortFunction} > debugId="local-sortFunction-single-sorting-example-with-local-data-example" columns={columns} columnDefaultWidth={220} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', lastName: 'Klein', country: 'India', city: 'Unnao', age: 24, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', lastName: 'Runolfsson', country: 'Mexico', city: 'Cuitlahuac', age: 46, currency: 'USD', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', lastName: 'McGlynn', country: 'United Arab Emirates', city: 'Fujairah', age: 54, currency: 'JPY', preferredLanguage: 'Go', stack: 'frontend', canDesign: 'yes', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', lastName: 'McLaughlin', country: 'Mexico', city: 'Tlacolula de Matamoros', age: 43, currency: 'CHF', preferredLanguage: 'Rust', stack: 'backend', canDesign: 'no', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', lastName: 'Harber', country: 'France', city: 'Persan', age: 23, currency: 'EUR', preferredLanguage: 'Go', stack: 'backend', canDesign: 'yes', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', lastName: 'Schroeder', country: 'United States', city: 'Hays', age: 34, currency: 'EUR', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'no', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', lastName: 'Mills', country: 'Mexico', city: 'Nuevo Casas Grandes', age: 33, currency: 'AUD', preferredLanguage: 'JavaScript', stack: 'frontend', canDesign: 'no', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', lastName: 'Hayes', country: 'Sweden', city: 'Delsbo', age: 22, currency: 'JPY', preferredLanguage: 'TypeScript', stack: 'full-stack', canDesign: 'yes', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', lastName: 'Boyle', country: 'Germany', city: 'Bad Camberg', age: 11, currency: 'GBP', preferredLanguage: 'TypeScript', stack: 'backend', canDesign: 'yes', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', lastName: 'Deckow', country: 'Canada', city: 'Raymore', age: 31, currency: 'EUR', preferredLanguage: 'Rust', stack: 'frontend', canDesign: 'yes', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` ### sortInfo (`DataSourceSingleSortInfo|DataSourceSingleSortInfo[]|null`) > Information for sorting the data. This is a controlled prop. Also see related [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) (uncontrolled version), [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo), [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) and [`columns.sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortable). Sorting can be single (only one field/column can be sorted at a time) or multiple (multiple fields/columns can be sorted at the same time). Therefore, this property an be an array of objects or a single object (or null) - the shape of the objects (of type `DataSourceSingleSortInfo`)is the following. - `dir` - `1 | -1` - the direction of the sorting - `field`? - `keyof DATA_TYPE` - the field to sort - `id`? - `string` - if you don't sort by a field, you can specify an id of the column this sorting is bound to. Note that columns have a [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter), which will be used when doing local sorting and the column is not bound to an exact field. - `type` - the sort type - one of the keys in [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) - eg `"string"`, `"number"` - will be used for local sorting, to provide the proper comparison function. When you want to use multiple sorting, but have no default sort order/information, use `[]` (the empty array) to denote multiple sorting should be enabled. If no `sortInfo` is provided, by default, when clicking a sortable column, single sorting will be applied. For configuring if a column is sortable or not, see [`columns.sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortable) and [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable). By default, all columns are sortable. **Example: Remote + controlled multi sorting** ```ts import { InfiniteTable, DataSource, DataSourceData, DataSourcePropSortInfo, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = ({ sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function RemoteControlledMultiSortingExample() { const [sortInfo, setSortInfo] = React.useState< DataSourcePropSortInfo >([ { field: 'salary', dir: -1, }, ]); const shouldReloadData = { sortInfo: true, }; return ( <> primaryKey="id" data={dataSource} sortInfo={sortInfo} shouldReloadData={shouldReloadData} onSortInfoChange={setSortInfo} > debugId="remote-controlled-multi-sorting-example" columns={columns} columnDefaultWidth={220} /> ); } ``` ### shouldReloadData.sortInfo (`boolean`) > Specifies if changes in the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) should trigger a reload of the data source - applicable when [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) is a function. Replaces the deprecated [`sortMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortMode). See related [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) and [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo). When set to `false` (the default), the data is sorted locally (in the browser) after the data-source is loaded. When set to `true`, the data should be sorted by the server (or by the data-source function that serves the data). See [the Sorting page](https://infinite-table.com/docs/learn/sorting/overview.md) for more details. For configuring the sorting behavior when multiple sorting is enabled, see [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior). ### shouldReloadData (`{ sortInfo, groupBy, filterValue, pivotBy }`) > Specifies which changes in the data-related props should trigger a reload of the data source - applicable when [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) is a function. See [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo). See [`shouldReloadData.groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.groupBy). See [`shouldReloadData.filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.filterValue). See [`shouldReloadData.pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.pivotBy). ### sortMode (`'local'|'remote'`) > Specifies where the sorting should be done. Use [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) instead. See related [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) and [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo). When set to `'local'`, the data is sorted locally (in the browser) after the data-source is loaded. When set to `'remote'`, the data should be sorted by the server (or by the data-source function that serves the data). See [the Sorting page](https://infinite-table.com/docs/learn/sorting/overview.md) for more details. For configuring the sorting behavior when multiple sorting is enabled, see [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior). ### sortTypes (`Record number)>`) > Describes the available sorting functions used for local sorting. The object you provide will be merged into the default sort types. Currently there are two `sortTypes` available: - `"string"` - `"number"` - `"date"` Those values can be used for the [column.sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) and [column.dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) properties. ```ts // default implementation const sortTypes = { string: (a, b) => a.localeCompare(b), number: (a, b) => a - b, date: (a, b) => a - b, }; ``` When a column does not explicitly specify the [column.sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType), the [column.dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) will be used instead. And if no [column.dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) is defined, it will default to `string`. You can add new sort types to the DataSource and InfiniteTable components by specifying this property - the object will be merged into the default sort types. **Example: Custom sort by color - magenta will come first** ```ts import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export type CarSale = { id: number; make: string; model: string; year: number; sales: number; color: string; }; const carsales: CarSale[] = [ { make: 'Volkswagen', model: 'GTI', year: 2009, sales: 6, color: 'red', id: 0, }, { make: 'Honda', model: 'Element 2WD', year: 2009, sales: 739, color: 'red', id: 1, }, { make: 'Acura', model: 'RDX 4WD', year: 2008, sales: 2, color: 'magenta', id: 2, }, { make: 'Honda', model: 'Fit', year: 2009, sales: 211, color: 'blue', id: 3, }, { make: 'Mazda', model: '6', year: 2009, sales: 31, color: 'blue', id: 4, }, { make: 'Acura', model: 'TSX', year: 2009, sales: 14, color: 'yellow', id: 5, }, { make: 'Acura', model: 'TSX', year: 2010, sales: 14, color: 'red', id: 6, }, { make: 'Audi', model: 'A3', year: 2009, sales: 2, color: 'magenta', id: 7, }, ]; const columns: Record> = { color: { field: 'color', sortType: 'color' }, make: { field: 'make' }, model: { field: 'model' }, sales: { field: 'sales', sortType: 'number', }, year: { field: 'year', sortType: 'number', }, }; const newSortTypes = { color: (one: string, two: string) => { if (one === 'magenta') { // magenta comes first return -1; } if (two === 'magenta') { // magenta comes first return 1; } return one.localeCompare(two); }, }; export default function DataTestPage() { return ( <> data={carsales} primaryKey="id" defaultSortInfo={{ field: 'color', dir: 1, type: 'color', }} sortTypes={newSortTypes} > debugId="sortTypes-example" columns={columns} /> ); } ``` In this example, for the `"color"` column, we specified [column.sortType="color"](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) - we could have passed that as `column.dataType` instead, but if the grid had filtering, it wouldn't know what filters to use for "color" - so we used[column.sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) to only change how the data is sorted. ### useGroupKeysForMultiRowSelection (`boolean`) > Specifies whether [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) contains group keys or only row ids/primary keys. When this is `true`, you might want to use the [getSelectedPrimaryKeys](./selection-api#getSelectedPrimaryKeys) method. **Example: Multi row checkbox selection using group keys** This example shows how you can use have row selection with group keys instead of just the primary keys of rows. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTableProps, InfiniteTableApi, InfiniteTablePropColumns, DataSourceProps, DataSourcePropRowSelection_MultiRow, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useCallback, useRef, useEffect, useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', renderGroupValue: ({ value }) => `Stack: ${value || ''}`, }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', renderGroupValue: ({ value }) => `Lang: ${value || ''}`, }, canDesign: { field: 'canDesign', renderGroupValue: ({ value }) => `Can design: ${value || ''}`, }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', renderSelectionCheckBox: true, defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { const apiRef = useRef | null>(null); const [rowSelection, setRowSelection] = useState({ selectedRows: [ ['yes', 'backend', 'TypeScript'], ['yes', 'backend', 'Go'], 16, 26, 30, ['yes', 'frontend'], ], deselectedRows: [4, 2], defaultSelection: false, }); const [selectedIds, setSelectedIds] = useState([]); const onReady = useCallback( ({ api }: { api: InfiniteTableApi }) => { apiRef.current = api; setSelectedIds( api.rowSelectionApi.getSelectedPrimaryKeys(rowSelection) as string[], ); }, [], ); useEffect(() => { if (!apiRef.current) { return; } setSelectedIds( apiRef.current.rowSelectionApi.getSelectedPrimaryKeys( rowSelection, ) as string[], ); }, [rowSelection]); return (
Current row selection:
 {JSON.stringify(rowSelection, null, 2)}.
Current selected ids: {selectedIds.join(', ')}
data={dataSource} groupBy={defaultGroupBy} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} useGroupKeysForMultiRowSelection primaryKey="id" > debugId="controlled-multi-row-selection-example-with-group-keys" onReady={onReady} columns={columns} domProps={domProps} groupColumn={groupColumn} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` --- # Error Codes > Infinite Table Error Codes Canonical page: https://infinite-table.com/docs/reference/error-codes ### DS001 > The error happens when you pass a new `data` prop on every render. ```tsx title="DONT: Dont use a new reference of the data prop on every render" function App() { // this is a new reference on every render function data(){ return Promise.resolve([]) } return } ``` ```tsx title="DO: Use the same reference of the data" // this is the same reference on every render function data(){ return Promise.resolve([]) } function App() { const [dataFn, setDataFn] = useState(data) return { // you can update it if you want // but dont do it on every render setDataFn(data.bind(null)) }} /> } ``` --- # Infinite Table Hooks > Hooks Reference page for Infinite Table - with complete examples Canonical page: https://infinite-table.com/docs/reference/hooks/ Infinite Table exposes a few custom hooks that can be used to customize the component and its behavior. Most of the hooks will be useful when you want to implement custom components for `InfiniteTable` - like custom cells, headers, cell editors, etc. See below for the full list of hooks exposed by `InfiniteTable`, each with examples and code snippets. ### useMasterRowInfo > Gives you access to the master row info in the current [RowDetail](https://infinite-table.com/docs/reference/infinite-table-props.md#components.RowDetail) component. **Example** This example shows a master DataGrid with cities & countries. The details for each city shows a DataGrid with developers in that city. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, useMasterRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function RowDetail() { const rowInfo = useMasterRowInfo()!; console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-component-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } const components = { RowDetail, }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-component-example-2" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} components={components} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### useDataSourceState (`(selector: (DataSourceState) => any)`) > You can use it in your app components that are nested inside the `` ```ts import { useDataSourceState } from '@infinite-table/infinite-react' ``` Using it gives you access to the underlying data that InfiniteTable is using. Call this hook with a `selector` function, which accepts the current `DataSourceState` as the first parameter. ```ts title="Example usage - selecting the length of the data array" const length = useDataSourceState(state => state.dataArray.length) ``` Please make sure you know what you're doing. This is intended only for advanced and complex use-cases. ```tsx title="InfiniteTable can be nested anywhere inside the component"

Your DataGrid> ``` Any component nested inside the `` can access the underlying data. ```tsx import * as React from 'react'; import { InfiniteTable, DataSource, useDataSourceState, type InfiniteTableColumn, DataSourceState, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; preferredLanguage: string; stack: string; salary: number; currency: string; country: string; }; const columns: Record> = { firstName: { field: 'firstName', header: 'First Name' }, preferredLanguage: { field: 'preferredLanguage', header: 'Programming Language', }, stack: { field: 'stack', header: 'Stack' }, salary: { field: 'salary', type: 'number', defaultWidth: 210, }, currency: { field: 'currency', header: 'Currency', defaultWidth: 100 }, }; const domProps = { style: { flex: 1, }, }; export default function App() { return ( data={dataSource} primaryKey="id" defaultGroupBy={[{ field: 'country' }]} > ); } function AppGrid() { const dataLength = useDataSourceState( (state: DataSourceState) => state.dataArray.length, ); return (

Your DataGrid

Displaying {dataLength} rows. Collapse/expand rows to see this number change.

debugId="using-datasource-context" groupRenderStrategy="single-column" defaultActiveRowIndex={0} domProps={domProps} columns={columns} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### useInfiniteColumnCell > Use it inside the [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) or [`column.components.ColumnCell`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.components.ColumnCell) (or [other](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) rendering functions) to retrieve information about the cell that is being rendered. ```ts import { useInfiniteColumnCell } from '@infinite-table/infinite-react'; ``` For custom column header components, see related [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell). When using this hook inside a [custom column cell component](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell), make sure you get `domRef` from the hook result and pass it on to the final `JSX.Element` that is the DOM root of the component. ```tsx const CustomCellComponent = (props: React.HTMLProps) => { const { domRef, ...other } = useInfiniteColumnCell(); return (
{props.children}
); }; ``` You should not pass the `domRef` along when using the hook inside the [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) or [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) function. **Example: Column with render & useInfiniteColumnCell** ```tsx import { InfiniteTable, DataSource, useInfiniteColumnCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { HTMLProps } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; function CustomCell(_props: HTMLProps) { const { value, data } = useInfiniteColumnCell(); let emoji = '🤷'; switch (value) { case 'photography': emoji = '📸'; break; case 'cooking': emoji = '👨🏻‍🍳'; break; case 'dancing': emoji = '💃'; break; case 'reading': emoji = '📚'; break; case 'sports': emoji = '⛹️'; break; } const label = data?.stack === 'frontend' ? '⚛️' : ''; return ( {emoji} + {label} ); } const columns: InfiniteTablePropColumns = { id: { field: 'id', maxWidth: 80 }, firstName: { field: 'firstName' }, hobby: { field: 'hobby', // we're not using the arg of the render function directly // but CustomCell uses `useInfiniteColumnCell` to retrieve it instead render: () => , }, }; export default function ColumnRenderWithHooksExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-render-hooks-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ### useInfiniteColumnEditor > Allows you to write a custom editor to be used for [editing](https://infinite-table.com/docs/learn/editing/overview.md). The hook returns an [`InfiniteColumnEditorContextType`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteColumnEditorContextType) object shape. Inside this hook, you can also call [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) to get access to the cell-related information. See related [`columns.components.Editor`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.Editor) When writing a custom editor, it's probably good to stop the propagation of the `KeyDown` event, so that the table doesn't react to the key presses (and do navigation and other stuff). **Example: Column with custom editor** Try editing the `salary` column - it has a custom editor ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, useInfiniteColumnEditor, } from '@infinite-table/infinite-react'; import { useRef, useCallback } from 'react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const CustomEditor = () => { const { initialValue, confirmEdit, cancelEdit } = useInfiniteColumnEditor(); const domRef = useRef(null); const onKeyDown = useCallback((event: React.KeyboardEvent) => { const { key } = event; if (key === 'Enter' || key === 'Tab') { confirmEdit(domRef.current?.value); } else if (key === 'Escape') { cancelEdit(); } else { event.stopPropagation(); } }, []); return (
); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { components: { // reference to the custom editor component Editor: CustomEditor, }, defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="custom-editor-hooks-example" columns={columns} columnDefaultEditable /> ); } ``` ### useInfiniteColumnFilterEditor (`() => ({ column, value, setValue, className, filtered,... })`) > Used to write custom filter editors for columns. The return value of this hook is an object with the following properties: - `value` - the value that should be passed to the filter editor - `setValue(value)` - the functon you have to call to update the filtering for the current column - `column` - the current column - `operatorName`: `string` - the name of the operator currently being applied - `className` - a CSS class name to apply to the filter editor, for default styling - `filtered` - a boolean indicating whether the column is currently filtered or not - `disabled` - a boolean indicating whether the filter editor should be rendered as disabled or not - `filterTypeKey`: `string` - the key of the filter type - `filterType` - the filter type object for the current column - `filterTypes` - a reference to the [`filterTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes) object as configured in the `DataSource` **Example: Demo of a custom filter editor** The `canDesign` column is using a custom `bool` filter type with a custom filter editor. The checkbox has indeterminate state, which will match all values in the data source. ```ts import * as React from 'react'; import { InfiniteTable, InfiniteTablePropColumns, DataSource, components, useInfiniteColumnFilterEditor, } from '@infinite-table/infinite-react'; const { CheckBox } = components; type Developer = { id: number; firstName: string; canDesign: boolean; stack: string; hobby: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', canDesign: true, stack: 'frontend', hobby: 'gaming', }, { id: 2, firstName: 'Jane', canDesign: false, stack: 'backend', hobby: 'reading', }, { id: 3, firstName: 'Jack', canDesign: true, stack: 'frontend', hobby: 'gaming', }, { id: 4, firstName: 'Jill', canDesign: false, stack: 'backend', hobby: 'reading', }, { id: 5, firstName: 'Seb', canDesign: false, stack: 'backend', hobby: 'reading', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, canDesign: { field: 'canDesign', filterType: 'bool', renderValue: ({ value }) => (value ? 'Yes' : 'No'), }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, }; const domProps = { style: { height: '100%', }, }; function BoolFilterEditor() { const { value, setValue, className } = useInfiniteColumnFilterEditor(); return (
{ if (value === true) { // after the value was true, make it go to indeterminate state newValue = null; } if (value === null) { // from indeterminate, goto false newValue = false; } setValue(newValue); }} />
); } export default () => { return ( <> data={dataSource} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterTypes={{ bool: { defaultOperator: 'eq', emptyValues: [null], components: { FilterEditor: BoolFilterEditor, FilterOperatorSwitch: () => null, }, operators: [ { name: 'eq', label: 'Equals', fn: ({ currentValue, filterValue }) => currentValue === filterValue, }, ], }, }} > debugId="custom-filter-editor-hooks-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ### useInfiniteHeaderCell > Used inside [`columns.header`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) or [`column.components.HeaderCell`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.components.HeaderCell) ```ts import { useInfiniteHeaderCell } from '@infinite-table/infinite-react'; ``` For custom column cell components, see related [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell). When using this hook inside a [custom column header component](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.HeaderCell), make sure you get `domRef` from the hook result and pass it on to the final `JSX.Element` that is the DOM root of the component. ```tsx const CustomHeaderComponent = (props: React.HTMLProps) => { const { domRef, ...other } = useInfiniteHeaderCell(); return (
{props.children}
); }; ``` You should not pass the `domRef` along when using the hook inside the [`columns.header`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) function. **Example: Column with custom header & useInfiniteHeaderCell** ```tsx import { InfiniteTable, DataSource, useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const HobbyHeader: React.FC = function () { const { column } = useInfiniteHeaderCell(); return {column?.field} 🤷📸👨🏻‍🍳💃📚⛹️; }; const columns: InfiniteTablePropColumns = { id: { field: 'id', maxWidth: 80 }, stack: { field: 'stack', }, hobby: { field: 'hobby', components: { HeaderCell: HobbyHeader, }, }, }; export default function ColumnHeaderExampleWithHooks() { return ( <> primaryKey="id" data={dataSource}> debugId="column-header-hooks-example" columns={columns} columnDefaultWidth={200} /> ); } ``` --- # Infinite Table Props > Infinite Table Props Reference page with complete examples Canonical page: https://infinite-table.com/docs/reference/infinite-table-props In the API Reference below we'll use **`DATA_TYPE`** to refer to the TypeScript type that represents the data the component is bound to. ### debugId (`string`) > The unique id to identify this InfiniteTable instance in devtools If you have [Infinite Table DevTools extension](https://chromewebstore.google.com/detail/infinite-table-devtools-e/jpipjljbfffijmgiecljadbogfegejfa) installed, the current `` instance will be picked up by the devtools with this specific name. See [our blogpost on the devtools extension](https://infinite-table.com/blog/2025/05/12/the-first-devtools-for-a-datagrid.md) for more details. ### repeatWrappedGroupRows (`boolean|(rowInfo: InfiniteTableRowInfo) => boolean`) > When enabled, and [`wrapRowsHorizontally`](https://infinite-table.com/docs/reference/infinite-table-props.md#wrapRowsHorizontally) is also enabled, if there is [grouping configured](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) or if you're using tree data, the group/tree rows will be repeated at the top of each column set if the group/parent starts in the previous column set. See related [`wrapRowsHorizontally`](https://infinite-table.com/docs/reference/infinite-table-props.md#wrapRowsHorizontally). **Example: Horizontal Layout with repeated wrapped group rows** ```tsx import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 50 }, country: { field: 'country', header: 'Country' }, city: { field: 'city', header: 'City' }, firstName: { field: 'firstName', header: 'First Name' }, separator: { valueGetter: () => null, resizable: false, defaultWidth: 10, style: { background: 'var(--infinite-background)', }, }, }; export default function HorizontalLayout() { const [repeatWrappedGroupRows, setRepeatWrappedGroupRows] = React.useState(false); return ( <>
primaryKey="id" data={dataSource} defaultGroupBy={[ { field: 'country', }, { field: 'city', }, ]} > debugId="horizontal-layout-repeat-wrapped-groups-example" wrapRowsHorizontally repeatWrappedGroupRows={repeatWrappedGroupRows} columns={columns} columnDefaultWidth={100} columnDefaultSortable={false} /> ); } ``` **Example: Tree with horizontal Layout and repeated wrapped tree rows** In this example, parent nodes are repeated conditionally: only top-level parent nodes are repeated when wrapping happens. ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { renderTreeIcon: true, field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( { if (!rowInfo.isTreeNode) { return false; } return rowInfo.treeNesting === 0; }} /> ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [ { id: '300', name: 'empty.mp3', sizeInKB: 0, type: 'file', }, { id: '301', name: 'Hawaii Song.mp3', sizeInKB: 108, type: 'file', extension: 'mp3', }, { id: '302', name: 'Independence Day Song.mp3', sizeInKB: 108, type: 'file', extension: 'mp3', }, { id: '303', name: 'Pop Song.mp3', sizeInKB: 108, type: 'file', extension: 'mp3', }, ], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', }, { id: '311', name: 'Honolulu.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', }, { id: '312', name: 'New York.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### wrapRowsHorizontally (`boolean`) > Whether to wrap rows horizontally or not. Horizontal Layout is a very different approach to the normal grid layout and only useful in very advanced scenarios. When this is set to `true`, rows will be wrapped horizontally to fit the container. When horizontal layout is enabled in combination with grouping, you can also use the [`repeatWrappedGroupRows`](https://infinite-table.com/docs/reference/infinite-table-props.md#repeatWrappedGroupRows) property to repeat the group rows at the top of each column set - if the group starts in the previous column set. When horizontal layout is enabled, rows will wrap and the existing columns will be repeated for each row-wrapping section - we will call them column sets. So for example when the DataGrid is configured with 3 columns and the DataSource has 25 rows, but only 10 rows fit in the vertical viewport, you will end up with 3 column-sets: the first with 10 rows, the second with the next 10 rows, and the third with the remaining 5 rows. The same columns are repeated for each column-set. **Example: Horizontal Layout example** ```tsx import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 50 }, firstName: { field: 'firstName', header: 'First Name' }, age: { field: 'age', header: 'Age' }, }; export default function HorizontalLayout() { const [wrapRowsHorizontally, setWrapRowsHorizontally] = React.useState(false); return ( <>
primaryKey="id" data={dataSource}> debugId="horizontal-layout-example" wrapRowsHorizontally={wrapRowsHorizontally} columns={columns} columnDefaultWidth={100} columnDefaultSortable={false} /> ); } ``` In the column rendering functions (both for header and cell rendering), you will have access to the `horizontalLayoutPageIndex` property. This is the index of the current horizontal layout page (the current column-set). `horizontalLayoutPageIndex` can either be `null`, when horizontal layout is disabled, or a number >= 0, when horizontal layout is enabled. When using horizontal layout, columns can't be configured to have a flexible width. So don't specify [`columns.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) for any column when horizontal layout is enabled. **Example: Horizontal Layout example with column set index in header** ```tsx import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, DataSource, DataSourceData, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const getColumnHeaderFor = ( label: string, ): InfiniteTableColumn['header'] => { return ({ horizontalLayoutPageIndex, }: { horizontalLayoutPageIndex: number | null; }) => { return ( <> {label} {horizontalLayoutPageIndex != null ? `(${horizontalLayoutPageIndex + 1})` : ''} ); }; }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, header: getColumnHeaderFor('ID'), }, firstName: { field: 'firstName', header: getColumnHeaderFor('Name') }, age: { field: 'age', header: getColumnHeaderFor('Age') }, }; export default function HorizontalLayout() { const [wrapRowsHorizontally, setWrapRowsHorizontally] = React.useState(true); return ( <>
primaryKey="id" data={dataSource}> debugId="horizontal-layout-with-column-set-index-in-header-example" wrapRowsHorizontally={wrapRowsHorizontally} columns={columns} columnDefaultWidth={100} columnDefaultSortable={false} /> ); } ``` ### components.RowDetail > Component to use for rendering the row details section in the master-detail DataGrid. When specified, it makes InfiniteTable be a [master-detail DataGrid](https://infinite-table.com/docs/learn/master-detail/overview.md). For configuring the height of row details, see [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) See related [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer). **Example: Basic master detail DataGrid example** This example shows a master DataGrid with cities & countries. The details for each city shows a DataGrid with developers in that city. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, useMasterRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function RowDetail() { const rowInfo = useMasterRowInfo()!; console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-component-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } const components = { RowDetail, }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-component-example-2" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} components={components} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### scrollStopDelay (`number`) > The delay in milliseconds that the DataGrid waits until it considers scrolling to be stopped. Also used when lazy loading is to fetch the next batch of data. This also determines when the [`onScrollStop`](https://infinite-table.com/docs/reference/infinite-table-props.md#onScrollStop) callback prop is called. **Example: Scroll stop delay for lazy loading** ```ts import { InfiniteTable, DataSource, DataSourceData, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useMemo } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 100 }, salary: { field: 'salary', header: 'Salary' }, age: { field: 'age', header: 'Age' }, firstName: { field: 'firstName', header: 'First Name' }, preferredLanguage: { field: 'preferredLanguage', header: 'Preferred Language', }, lastName: { field: 'lastName', header: 'Last Name' }, country: { field: 'country', header: 'Country' }, city: { field: 'city', header: 'City' }, currency: { field: 'currency', header: 'Currency' }, stack: { field: 'stack', header: 'Stack' }, canDesign: { field: 'canDesign', header: 'Can Design' }, hobby: { field: 'hobby', header: 'Hobby' }, }; export default function App() { const lazyLoad = useMemo(() => ({ batchSize: 40 }), []); return ( data={dataSource} primaryKey="id" lazyLoad={lazyLoad} > debugId="scrollStopDelay-lazy-load-example" columns={columns} columnDefaultWidth={130} scrollStopDelay={50} /> ); } const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, lazyLoadStartIndex, lazyLoadBatchSize, groupKeys = [], sortInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const startLimit: string[] = []; if (lazyLoadBatchSize && lazyLoadBatchSize > 0) { const start = lazyLoadStartIndex || 0; startLimit.push(`start=${start}`); startLimit.push(`limit=${lazyLoadBatchSize}`); } const args = [ ...startLimit, pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers10k-sql?` + args, ).then((r) => r.json()); }; ``` ### headerOptions (`{alwaysReserveSpaceForSortIcon: boolean}`) > Various header configurations for the DataGrid. For now, it has the following properties: - [`headerOptions.alwaysReserveSpaceForSortIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#headerOptions.alwaysReserveSpaceForSortIcon) **Example** ```tsx import { InfiniteTable, DataSource, InfiniteTableColumn, components, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const { CheckBox } = components; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'Location: City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [reserveSpaceForSortIcon, setReserveSpaceForSortIcon] = useState(true); return ( <>
setReserveSpaceForSortIcon((prev) => !prev)} > Reserve space for sort icon
data={dataSource} primaryKey="id"> debugId="sortIcon-reserve-space-example" headerOptions={{ alwaysReserveSpaceForSortIcon: reserveSpaceForSortIcon, }} columns={columns} columnDefaultWidth={100} columnMinWidth={30} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### headerOptions.alwaysReserveSpaceForSortIcon (`boolean`) > Whether to reserve space in the column header for the sort icon or not. When this is set to `true`, the space for the sort icon is always reserved, even if the column is not currently sorted. **Example** ```tsx import { InfiniteTable, DataSource, InfiniteTableColumn, components, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const { CheckBox } = components; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'Location: City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [reserveSpaceForSortIcon, setReserveSpaceForSortIcon] = useState(true); return ( <>
setReserveSpaceForSortIcon((prev) => !prev)} > Reserve space for sort icon
data={dataSource} primaryKey="id"> debugId="sortIcon-reserve-space-example" headerOptions={{ alwaysReserveSpaceForSortIcon: reserveSpaceForSortIcon, }} columns={columns} columnDefaultWidth={100} columnMinWidth={30} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### rowDetailRenderer (`(rowInfo: InfiniteTableRowInfo) => ReactNode`) > When specified, it makes InfiniteTable be a [master-detail DataGrid](https://infinite-table.com/docs/learn/master-detail/overview.md). For configuring the height of row details, see [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight). See related [`components.RowDetail`](https://infinite-table.com/docs/reference/infinite-table-props.md#components.RowDetail). It's an alternative to using [`components.RowDetail`](https://infinite-table.com/docs/reference/infinite-table-props.md#components.RowDetail). This function is called with the [rowInfo](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) the user expands to see details for. Using this function, you can render another DataGrid or any other custom content. Make sure you have a column with the `renderRowDetailIcon: true` flag set. [`columns.renderRowDetailIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderRowDetailIcon) on a column makes the column display the row details expand icon. Without this flag, no column will have the expand icon, and the master-detail functionality will not work. To configure the height of the row details section, use the [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) prop. For rendering some row details as already expanded, see [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState). **Example: Basic master detail DataGrid example** This example shows a master DataGrid with cities & countries. The details for each city shows a DataGrid with developers in that city. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-example-2" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### showColumnFilters (`boolean`) > Whether to show the column filters or not (only applicable when the `` is configured with filtering - either with [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue) or [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue)). When the `` is configured with [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue), the column filters will be shown by default. Specify this prop as `false` to hide the column filters. **Example: Controling the visibility of column filters** ```tsx import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { field: 'salary', type: 'number', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency', defaultFilterable: false }, }; export default () => { const [showColumnFilters, setShowColumnFilters] = React.useState(true); return ( <>
data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" > debugId="column-filters-visibility-example" columnDefaultWidth={150} columnMinWidth={50} columns={columns} showColumnFilters={showColumnFilters} />
); }; ``` ### defaultRowDetailState (`RowDetailState`) > Specifies the default expanded/collapsed state of row details. For the controlled version, see [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState). If [`isRowDetailExpanded`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailExpanded) is specified, it has the last word in deciding if a row detail is expanded or not, so it overrides the `defaultRowDetailState`. **Example: Master detail DataGrid with some row details expanded by default** Some of the rows in the master DataGrid are expanded by default. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-default-expanded-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} />
); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-default-expanded-example-2" domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### isRowDetailExpanded (`(rowInfo: InfiniteTableRowInfo) => boolean`) > This function ultimately decides if a row detail is expanded or not. This function is meant for very advanced scenarios. For common use-cases, you'll probably use [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) and [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState). If `isRowDetailExpanded` is specified, it overrides [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState)/[`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState). ### isRowDetailEnabled (`(rowInfo: InfiniteTableRowInfo) => boolean`) > Decides on a per-row basis if the row details are enabled or not. See [Master Detail](https://infinite-table.com/docs/learn/master-detail/overview.md) for more information. This function is called with the [rowInfo](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) and should return a `boolean` value. It's useful when you don't want to show the row detail for some rows. **Example: Master detail DataGrid with some row not having details** All the odd rows don't have details. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-per-row-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } const isRowDetailEnabled = (rowInfo: InfiniteTableRowInfo) => { return rowInfo.indexInAll % 2 === 0; }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-per-row-example-2" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} isRowDetailEnabled={isRowDetailEnabled} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### rowDetailState (`RowDetailState`) > Specifies the expanded/collapsed state of row details. For the uncontrolled version, see [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState). When you use this controlled property, make sure you pair it with the [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange) callback to update it. If [`isRowDetailExpanded`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailExpanded) is specified, it has the final say in deciding if a row detail is expanded or not, so it overrides the `rowDetailState` and [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState). **Example: Master detail DataGrid with some row details expanded by default** Some of the rows in the master DataGrid are expanded by default. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, RowDetailStateObject, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-controlled-expanded-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } export default () => { const [rowDetailState, setRowDetailState] = React.useState< RowDetailStateObject >({ collapsedRows: true as const, expandedRows: [39, 54], }); return ( <>
Row detail state: {JSON.stringify(rowDetailState, null, 2)}
data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-controlled-expanded-example-2" domProps={domProps} onReady={({ api }) => { console.log(api.rowDetailApi); }} columnDefaultWidth={150} rowDetailState={rowDetailState} onRowDetailStateChange={(rowDetailState) => { setRowDetailState(rowDetailState.getState()); }} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### onRowDetailStateChange (`(rowDetailState: RowDetailState, {expandRow, collapseRow}) => void`) > Called when the expand/collapse state of row details changes. You can use this function prop to update the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop or simply to listen to changes in the row details state. This function is called with an instance of the [`RowDetailState`](https://infinite-table.com/docs/reference/type-definitions/index.md#RowDetailState). If you want to get the object behind it, simply call `rowDetailState.getState()`. Both the `RowDetailState` instance and the state object (literal) are valid values you can pass to the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState). The second parameter of this function is an object with `expandRow` and `collapseRow` properties, which contain the primary key of either the last expanded or the last collapsed row. For example, if the user is expanding row `3`, the object will be `{expandRow: 3, collapseRow: null}`. Next, if the user collapses row `5`, the object will be `{expandRow: null, collapseRow: 5}`. This makes it easy for you to know which action was taken and on which row. See related [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) and [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState). **Example: Master detail DataGrid with listener to the row expand/collapse state change** Some of the rows in the master DataGrid are expanded by default. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, RowDetailStateObject, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-controlled-expanded-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } export default () => { const [rowDetailState, setRowDetailState] = React.useState< RowDetailStateObject >({ collapsedRows: true as const, expandedRows: [39, 54], }); return ( <>
Row detail state: {JSON.stringify(rowDetailState, null, 2)}
data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-controlled-expanded-example-2" domProps={domProps} onReady={({ api }) => { console.log(api.rowDetailApi); }} columnDefaultWidth={150} rowDetailState={rowDetailState} onRowDetailStateChange={(rowDetailState) => { setRowDetailState(rowDetailState.getState()); }} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### rowDetailCache (`boolean|number`) > Controls the caching of detail DataGrids. By default, caching is disabled. It can be one of the following: - `false` - caching is disabled - this is the default - `true` - enables caching for all detail DataGrids - `number` - the maximum number of detail DataGrids to keep in the cache. When the limit is reached, the oldest detail DataGrid will be removed from the cache. **Example: Master detail DataGrid with caching for 5 detail DataGrids** This example will cache the last 5 detail DataGrids - meaning they won't reload when you expand them again. You can try collapsing a row and then expanding it again to see the caching in action - it won't reload the data. But when you open up a row that hasn't been opened before, it will load the data from the remote location. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-caching-with-default-expanded-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-caching-with-default-expanded-example-2" domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailCache={5} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then( (data: Developer[]) => new Promise((resolve) => { setTimeout(() => { resolve(data); }, 500); }), ); }; ``` ### rowDetailHeight (`number|CSSVar|(rowInfo)=>number`) > Controls the height of the row details section, in master-detail DataGrids. The default value is `300` pixels. This can be a number, a string (the name of a CSS variable - eg `--detail-height`), or a function. When a function is defined, it's called with the [rowInfo](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) object for the corresponding row. **Example: Master detail DataGrid with custom detail height** In this example we configure the height of row details to be 200px. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-custom-detail-height-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-custom-detail-height-example-2" domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### activeCellIndex (`[number,number] | null`) > Specifies the active cell for keyboard navigation. This is a controlled prop. See the [Keyboard Navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells.md) page for more details. See [`defaultActiveCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultActiveCellIndex) for the uncontrolled version of this prop and [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) for the keyboard navigation behavior. Use the [`onActiveCellIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveCellIndexChange) callback to be notified when the active cell changes. `null` is a valid value, and it means no cell is currently rendered as active. Especially useful for controlled scenarios, when you need ultimate control over the behavior of keyboard navigation. **Example: Controlled keyboard navigation for cells** This example starts with cell `[2,0]` already active. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForCells() { const [activeCellIndex, setActiveCellIndex] = React.useState< [number, number] >([2, 0]); return ( <>
Current active cell: {activeCellIndex[0]}, {activeCellIndex[1]}.
primaryKey="id" data={dataSource}> debugId="navigating-cells-controlled-example" activeCellIndex={activeCellIndex} onActiveCellIndexChange={setActiveCellIndex} columns={columns} /> ); } ``` ### activeRowIndex (`number | null`) > Specifies the active row for keyboard navigation. This is a controlled prop. See the [Keyboard Navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-rows.md) page for more details. See [`defaultActiveRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultActiveRowIndex) for the uncontrolled version of this prop and [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) for the keyboard navigation behavior. Use the [`onActiveRowIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) callback to be notified when the active row changes. `null` is a valid value, and it means no row is currently rendered as active. Especially useful for controlled scenarios, when you need ultimate control over the behavior of keyboard navigation. **Example: Controlled keyboard navigation for rows** This example starts with row at index `2` already active. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForRows() { const [activeRowIndex, setActiveRowIndex] = React.useState(2); return ( <>
Current active row: {activeRowIndex}.
primaryKey="id" data={dataSource}> debugId="navigating-rows-controlled-example" keyboardNavigation="row" activeRowIndex={activeRowIndex} onActiveRowIndexChange={setActiveRowIndex} columns={columns} /> ); } ``` ### autoSizeColumnsKey (`number|string|{key,includeHeader,columnsToSkip,columnsToResize}`) > Controls auto-sizing of columns. Here is a list of possible values for `autoSizeColumnsKey`: - `string` or `number` - when the value is changing, all columns will be auto-sized. - an object with a `key` property (of type `string` or `number`) - whenever the `key` changes, the columns will be auto-sized. Specifying an object for `autoSizeColumnsKey` gives you more control over which columns are auto-sized and if the size measurements include the header or not. When an object is used, the following properties are available: - `key` - mandatory property, which, when changed, triggers the update - `includeHeader` - optional boolean, - decides whether the header will be included in the auto-sizing calculations. If not specified, `true` is assumed. - `columnsToSkip` - a list of column ids to skip from auto-sizing. If this is used, all columns except those in the list will be auto-sized. - `columnsToResize` - the list of column ids to include in auto-sizing. If this is used, only columns in the list will be auto-sized. **Example: Auto-sizing columns** ```tsx import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function GroupByExample() { const [key, setKey] = React.useState(0); const [includeHeader, setIncludeHeader] = React.useState(false); const autoSizeColumnsKey = React.useMemo(() => { return { includeHeader, key, }; }, [key, includeHeader]); return ( <>
primaryKey="id" data={dataSource}> debugId="autoSizeColumnsKey-example" autoSizeColumnsKey={autoSizeColumnsKey} columns={columns} columnDefaultWidth={200} /> ); } ``` When auto-sizing takes place, [`onColumnSizingChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnSizingChange) is called with the new column sizes. If you use controlled [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing), make sure you update its value accordingly. When columns are auto-sized, keep in mind that only visible (rendered) rows are taken into account - so if you scroll new rows into view, auto-sizing columns may result in different column sizes. In the same logic, keep in mind that by default columns are also virtualized (controlled by [`virtualizeColumns`](https://infinite-table.com/docs/reference/infinite-table-props.md#virtualizeColumns)), not only rows, so only visible columns are auto-sized (in case you have more columns, the columns that are not currently visible do not change their sizes). ### columnDefaultEditable (`boolean`) > Specifies whether columns are editable by default. To enable editing globally, you can use this boolean prop on the `InfiniteTable` component. It will enable the editing on all columns. Or you can be more specific and choose to make individual columns editable via the [column.defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) prop. In addition to the props already in discussion, you can use the [`editable`](https://infinite-table.com/docs/reference/infinite-table-props.md#editable) prop on the `InfiniteTable` component. This overrides all other properties and when it is defined, is the only source of truth for whether something is editable or not. By default, double-clicking an editable cell will show the cell editor. You can prevent this by returning `{preventEdit: true}` from the [onCellDoubleClick](https://infinite-table.com/docs/reference/infinite-table-props.md#onCellDoubleClick) function prop. **Example** All columns are configured to not be editable, except the `salary` column. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, salary: { // the only editable column defaultEditable: true, defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { const shouldAcceptEdit = ({ value }: { value: any }) => { return parseInt(value, 10) == value; }; return ( <> primaryKey="id" data={dataSource}> debugId="global-should-accept-edit-example" columns={columns} columnDefaultEditable={false} shouldAcceptEdit={shouldAcceptEdit} /> ); } ``` ### columnDefaultSortable (`boolean`) > Specifies whether columns are sortable by default. This property is overriden by (in this order) the following props: - [`columnTypes.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultSortable) - [`column.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.defaultSortable) - [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) When specified, [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) overrides all other properties and is the only source of truth for whether something is sortable or not. This property does not apply for group columns, since for sorting, group columns generally depend on the columns they are grouping. In some cases, you can have group columns that group by fields that are not bound to actual columns, so for determining sorting for group columns, use one of the following props: - [`columnTypes.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultSortable) - [`column.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.defaultSortable) - [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) ### sortable (`boolean | ({column, columns, api, columnApi}) => boolean`) > This prop is the ultimate source of truth on whether (and which) columns are sortable. This property overrides all the following props: - [`columnDefaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultSortable) (this is the base value, overriden by all other props in this list, in this order) - [`columnTypes.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultSortable) - [`column.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.defaultSortable) The [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) prop is designed to be used for highly advanced scenarios, where you need to have ultimate control over which columns are sortable and which are not - in this case, you will want to declare [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) as a function, which returns `true/false` for every column. ### columnDefaultWidth (`number`) > Specifies the a default width for all columns. If a column is explicitly sized via [column.defaultWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth), [column.defaultFlex](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex), [`columnSizing.width`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.width) (or [`defaultColumnSizing.width`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing.width)), that will be used instead. Use [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth) to set a minimum width for all columns. Use [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth) to set a maximum width for all columns. [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth) and [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth) will be very useful once flex column sizing lands. **Example** ```ts files=["columnDefaultWidth-example.page.tsx","data.ts"] ``` ### columnHeaderHeight (`number`) > The height of the column header. This only refers to the height of the header label - so if you have another row in the column header, for filters, the filters will also have this height. Also, for column groups, each additional group will have this height. **Example** The column header height is set to `60` pixels. The column filters will also pick up this height. ```ts files=["columnHeaderHeight-example.page.tsx","data.ts"] ``` ### columnMaxWidth (`number`) > Specifies the maximum width for all columns. For specifying the minimum column width, see [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth). Maximum column width can be controlled more granularly via [`columnSizing.maxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.maxWidth), on a per column level. **Example** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; const defaultColumnSizing: InfiniteTablePropColumnSizing = { firstName: { flex: 1 }, country: { flex: 1 }, city: { flex: 1 }, salary: { flex: 2 }, }; export default function App() { return ( data={dataSource} primaryKey="id"> debugId="columnMaxWidth-example" columns={columns} columnMaxWidth={200} defaultColumnSizing={defaultColumnSizing} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### columnMinWidth (`number`) > Specifies the minimum width for all columns. For specifying the maximum column width, see [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth). Minimum column width can be controlled more granularly via [`columnSizing.minWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.minWidth) or [`columns.minWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.minWidth), on a per column level. **Example** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; const defaultColumnSizing: InfiniteTablePropColumnSizing = { city: { flex: 1 }, salary: { flex: 2 }, }; export default function App() { return ( data={dataSource} primaryKey="id"> debugId="columnMinWidth-example" columns={columns} columnMinWidth={300} defaultColumnSizing={defaultColumnSizing} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### columnOrder (`string[]|true`) > Defines the order in which columns are displayed in the component For uncontrolled usage, see [`defaultColumnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnOrder). When using this controlled prop, make sure you also listen to [`onColumnOrderChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnOrderChange) See [Column Order](https://infinite-table.com/docs/learn/columns/column-order.md) for more details on ordering columns both programatically and via drag & drop. The `columnOrder` array can contain identifiers that are not yet defined in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) Map or can contain duplicate ids. This is a feature, not a bug. We want to allow you to use the `columnOrder` in a flexible way so it can define the order of current and future columns. Displaying the same column twice is a perfectly valid use case. **Example: Column order** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, }; export default function App() { const [columnOrder, setColumnOrder] = useState([ 'firstName', 'country', 'team', 'company', 'department', 'companySize', ]); return ( <>

Current column order:{' '}

{columnOrder.join(', ')}.

Drag column headers to reorder.

data={dataSource} primaryKey="id"> debugId="columnOrder-example" columns={columns} columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} columnDefaultWidth={200} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; ``` This prop can either be an array of strings (column ids) or the boolean `true`. When `true`, all columns present in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) object will be displayed, in the iteration order of the object keys. **Example: Column order advanced example** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, }; export default function App() { const [columnOrder, setColumnOrder] = useState([ 'firstName', 'country', 'team', 'company', 'firstName', 'not existing column', 'companySize', ]); return ( <>

Current column order:{' '}

{JSON.stringify(columnOrder)}.

Note: if the column order contains columns that don't exist in the `columns` definition, they will be skipped.

data={dataSource} primaryKey="id"> debugId="columnOrder-advanced-example" columns={columns} columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} columnDefaultWidth={200} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; ``` Using [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) in combination with [`columnVisibility`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnVisibility) is very powerful - for example, you can have a specific column order even for columns which are not visible at a certain moment, so when they will be made visible, you'll know exactly where they will be displayed. ### columns (`Record>`) > Describes the columns available in the component. The following properties are available: - [field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) - [defaultWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth) - [defaultFlex](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) - [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) - [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) - [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) - [header](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) - [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) - [valueFormatter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) - ...etc **Example** ```ts files=["columns-example.page.tsx","data.ts"] ``` ### columns.className (`string | (param: InfiniteTableColumnStyleFnParams) => string`) > Controls styling via CSS classes for the column. Can be a `string` or a function returning a `string` (a valid className). If defined as a function, it accepts an object as a parameter (of type [`InfiniteTableColumnStylingFnParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnStylingFnParams)), which has the following properties: - `column` - the current column where the className is being applied - `data` - the data object for the current row. The type of this object is `DATA_TYPE | Partial | null`. For regular rows, it will be of type `DATA_TYPE`, while for group rows it will be `Partial`. For rows not yet loaded (because of batching being used), it will be `null`. - `rowInfo` - the information about the current row - see [Using RowInfo](https://infinite-table.com/docs/learn/rows/using-row-info.md) for more details. - `value` - the underlying value of the current cell - will generally be `data[column.field]`, if the column is bound to a `field` property - ... and more, see [`InfiniteTableColumnStylingFnParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnStylingFnParams) for details The `className` property can also be specified for [`columnTypes`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) **Example** ```ts files=["column-className-function-example.page.tsx","coloring.module.css"] ``` ### components > Components to override the default ones used by the DataGrid. The following components can be overridden: - `LoadMask` - see [`components.LoadMask`](https://infinite-table.com/docs/reference/infinite-table-props.md#components.LoadMask) - `CheckBox` - `Menu` - `MenuIcon` ### components.LoadMask > Allows customising the `LoadMask` displayed over the DataGrid when it's loading data. To better test this out, you can use the controlled [`loading`](https://infinite-table.com/docs/reference/datasource-props/index.md#loading) prop on the `` For more components that can be overriden, see [`components`](https://infinite-table.com/docs/reference/infinite-table-props.md#components) **Example: Custom LoadMask component** ```tsx import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; function LoadMask() { return (
Loading App ...
); } export default function App() { return ( loading data={employees} primaryKey="id"> debugId="load-mask-example" components={{ LoadMask, }} columnDefaultWidth={130} columns={columns} />
); } type Employee = { id: string | number; name: string; salary: number; department: string; company: string; }; const employees: Employee[] = [ { id: 1, name: 'Bob', salary: 10_000, department: 'IT', company: 'Bobsons', }, { id: 2, name: 'Alice', salary: 20_000, department: 'IT', company: 'Bobsons', }, ]; const columns: Record> = { id: { field: 'id', type: 'number', defaultWidth: 80, }, name: { field: 'name', }, salary: { field: 'salary', type: 'number' }, department: { field: 'department', header: 'Dep.' }, company: { field: 'company' }, }; ``` ### columns.renderTreeIcon (`boolean|(cellContext) => ReactNode`) > Renders the tree expand/collapse icon in the column cells. If you want default behavior, specify `true` and the default icon will be used. To render a custom icon, specify a function that returns a React node. The `cellContext` object param will contain all the information about the current cell. The `cellContext` object contains a `toggleCurrentTreeNode` function property, which can be used to toggle the node state when clicked. With the default value of `true`, an icon will be rendered only for parent nodes. If you want to render an icon for all nodes, specify a function (and differentiate between parent and leaf nodes), and it will be called regardless of whether the node is a parent or a leaf. You can also use [`columns.renderTreeIconForParentNode`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIconForParentNode) to specify to customize the tree icon rendering for parent nodes or [`columns.renderTreeIconForLeafNode`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIconForLeafNode) to customize the tree icon rendering for leaf nodes. **Example: Specifying a column to used as the tree icon** ```ts import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useMemo, useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const allColumns: Record> = { name: { field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeIcon, setTreeIcon] = useState('name'); const columns = useMemo(() => { const cols = { ...allColumns }; cols[treeIcon] = { ...cols[treeIcon], renderTreeIcon: true, }; return cols; }, [treeIcon]); return ( <>

Select the tree column

); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` **Example: Rendering a custom tree icon for both parent and leaf nodes** This example renders a custom tree icon and uses the `toggleCurrentTreeNode` function to toggle the node state when Clicked. `toggleCurrentTreeNode` is a property of the `cellContext` argument passed to the `renderTreeIcon` function. ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { CSSProperties } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; sizeInKB: number; children?: FileSystemNode[]; }; const renderTreeIcon: InfiniteTableColumn['renderTreeIcon'] = ({ rowInfo, toggleCurrentTreeNode, }) => { return rowInfo.isParentNode ? ( ) : ( ); }; const svgStyle: CSSProperties = { verticalAlign: 'middle', position: 'relative', top: '-1px', marginInline: '5px', }; const FileIcon = () => ( ); const FolderIcon = ({ onClick, open, }: { onClick: () => void; open: boolean; }) => { return ( {open ? ( ) : ( )} ); }; const columns: Record> = { name: { renderTreeIcon, field: 'name', header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### columns.renderRowDetailIcon (`boolean|(cellContext) => ReactNode`) > Renders the row detail expand/collapse icon in the column cell. Only used when [master-detail](https://infinite-table.com/docs/learn/master-detail/overview.md) is enabled. If this function is a prop, it can be used to customize the icon rendered for expanding/collapsing the row detail. See related [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) for configuring master-detail. **Example: Basic master detail DataGrid example** This example shows a master DataGrid with the ID column configured to show the row detail expand icon. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } export default () => { return ( <> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-example-2" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### columns.components > Specifies custom React components to use for column cells or header The column components object can have either of the two following properties: - [ColumnCell](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell) - a React component to use for rendering the column cells - [HeaderCell](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.HeaderCell) - a React component to use for rendering the column header - [Editor](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.Editor) - a React component to use for the editor, when editing is enabled for the column - [FilterOperatorSwitch](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.FilterOperatorSwitch) - a React component to use for the filter operator switch - clicking the operator pops up a menu with the available operators for that column filter. See [editing docs](https://infinite-table.com/docs/learn/editing/overview.md). ### columns.components.ColumnCell > Specifies a custom React component to use for column cells For column header see related [`columns.components.HeaderCell`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.HeaderCell). Inside a component used as a cell, you have to use [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) to retrieve information about the currently rendered cell. It's very important that you take ```tsx const { domRef } = useInfiniteColumnCell(); ``` the `domRef` from the [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) hook and pass it on to the root DOM element of your cell component. ```tsx
...
``` **If you don't do this, the column rendering will not work.** Also note that your React Component should be a functional component and have this signature ```tsx function CustomComponent(props: React.HTMLProps) { return ... } ``` that is, the `props` that the component is rendered with (is called with) are `HTMLProps` (more exactly `HTMLProps`) that you need to spread on the root DOM element of your component. If you want to customize anything, you can, for example, append a `className` or specify some extra styles. In order to access the cell-related information, you don't use the props, but you call the [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) hook. ```tsx const ExampleCellComponent: React.FunctionComponent< React.HTMLProps > = (props) => { const { domRef } = useInfiniteColumnCell(); return (
{props.children}
{emoji}
); }; ``` **Example: Custom components** ```tsx import { InfiniteTable, DataSource, useInfiniteColumnCell, useInfiniteHeaderCell, InfiniteTablePropColumnTypes, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const DefaultHeaderComponent: React.FunctionComponent< React.HTMLProps > = (props) => { const { column, domRef, columnSortInfo } = useInfiniteHeaderCell(); const style = { ...props.style, border: '1px solid #fefefe', }; let sortTool = ''; switch (columnSortInfo?.dir) { case undefined: sortTool = '👉'; break; case 1: sortTool = '👇'; break; case -1: sortTool = '☝🏽'; break; } return (
{/* here you would usually have: */} {/* {props.children} {sortTool} */} {/* but in this case we want to override the default sort tool as well (which is part of props.children) */} {column.field} {sortTool}
); }; const StackComponent: React.FunctionComponent< React.HTMLProps > = (props) => { const { value, domRef } = useInfiniteColumnCell(); const isFrontEnd = value === 'frontend'; const emoji = isFrontEnd ? '⚛️' : '💽'; const style = { padding: '5px 20px', border: `1px solid ${isFrontEnd ? 'red' : 'green'}`, ...props.style, }; return (
{props.children}
{emoji}
); }; const columnTypes: InfiniteTablePropColumnTypes = { default: { // override all columns to use these components components: { HeaderCell: DefaultHeaderComponent, }, }, }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderValue: ({ data }) => 'Stack: ' + data?.stack, components: { HeaderCell: DefaultHeaderComponent, ColumnCell: StackComponent, }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-components-example" columns={columns} columnTypes={columnTypes} /> ); } ``` ### columns.components.Editor > Specifies a custom React component to use for the editor, when [editing](https://infinite-table.com/docs/learn/editing/overview.md) is enabled for the column. The editor component should use the [`useInfiniteColumnEditor`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnEditor) hook to have access to cell-related information and to confirm, cancel or reject the edit. Here's the implementation for our default editor ```tsx export function InfiniteTableColumnEditor() { const { initialValue, setValue, confirmEdit, cancelEdit, readOnly } = useInfiniteColumnEditor(); const domRef = useRef(); const refCallback = React.useCallback((node: HTMLInputElement) => { domRef.current = node; if (node) { node.focus(); } }, []); const onKeyDown = useCallback((event: React.KeyboardEvent) => { const { key } = event; if (key === 'Enter' || key === 'Tab') { confirmEdit(); } else if (key === 'Escape') { cancelEdit(); } else { event.stopPropagation(); } }, []); return ( <> confirmEdit()} className={'...'} type={'text'} defaultValue={initialValue} onChange={useCallback((event: React.ChangeEvent) => { setValue(event.target.value); }, [])} /> ); } ``` **Example: Column with custom editor** Try editing the `salary` column - it has a custom editor ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, useInfiniteColumnEditor, } from '@infinite-table/infinite-react'; import { useRef, useCallback } from 'react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const CustomEditor = () => { const { initialValue, confirmEdit, cancelEdit } = useInfiniteColumnEditor(); const domRef = useRef(null); const onKeyDown = useCallback((event: React.KeyboardEvent) => { const { key } = event; if (key === 'Enter' || key === 'Tab') { confirmEdit(domRef.current?.value); } else if (key === 'Escape') { cancelEdit(); } else { event.stopPropagation(); } }, []); return (
); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { components: { // reference to the custom editor component Editor: CustomEditor, }, defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="custom-editor-hooks-example" columns={columns} columnDefaultEditable /> ); } ``` ### columns.components.HeaderCell > Specifies a custom React component to use for column headers For column cells see related [`columns.components.ColumnCell`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell). Inside a custom component used as a column header, you have to use [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) to retrieve information about the currently rendered header cell. It's very important that you take ```tsx const { domRef } = useInfiniteHeaderCell(); ``` the `domRef` from the [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) hook and pass it on to the root DOM element of your header component. ```tsx
...
``` **If you don't do this, the column header rendering will not work.** Also note that your React Component should be a functional component and have this signature ```tsx function CustomHeaderComponent(props: React.HTMLProps) { return ... } ``` that is, the `props` that the component is rendered with (is called with) are `HTMLProps` (more exactly `HTMLProps`) that you need to spread on the root DOM element of your component. If you want to customize anything, you can, for example, append a `className` or specify some extra styles. In order to access the column header-related information, you don't use the props, but you call the [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) hook. ```tsx const ExampleHeaderComponent: React.FunctionComponent< React.HTMLProps > = (props) => { const { domRef } = useInfiniteHeaderCell(); return (
{props.children}
{emoji}
); }; ``` **Example: Custom components** ```tsx import { InfiniteTable, DataSource, useInfiniteColumnCell, useInfiniteHeaderCell, InfiniteTablePropColumnTypes, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const DefaultHeaderComponent: React.FunctionComponent< React.HTMLProps > = (props) => { const { column, domRef, columnSortInfo } = useInfiniteHeaderCell(); const style = { ...props.style, border: '1px solid #fefefe', }; let sortTool = ''; switch (columnSortInfo?.dir) { case undefined: sortTool = '👉'; break; case 1: sortTool = '👇'; break; case -1: sortTool = '☝🏽'; break; } return (
{/* here you would usually have: */} {/* {props.children} {sortTool} */} {/* but in this case we want to override the default sort tool as well (which is part of props.children) */} {column.field} {sortTool}
); }; const StackComponent: React.FunctionComponent< React.HTMLProps > = (props) => { const { value, domRef } = useInfiniteColumnCell(); const isFrontEnd = value === 'frontend'; const emoji = isFrontEnd ? '⚛️' : '💽'; const style = { padding: '5px 20px', border: `1px solid ${isFrontEnd ? 'red' : 'green'}`, ...props.style, }; return (
{props.children}
{emoji}
); }; const columnTypes: InfiniteTablePropColumnTypes = { default: { // override all columns to use these components components: { HeaderCell: DefaultHeaderComponent, }, }, }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderValue: ({ data }) => 'Stack: ' + data?.stack, components: { HeaderCell: DefaultHeaderComponent, ColumnCell: StackComponent, }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-components-example" columns={columns} columnTypes={columnTypes} /> ); } ``` ### columns.contentFocusable (`boolean|(params) => boolean`) > Specifies if the column (or cell, if a function is used) renders content that will/should be focusable (via tab-navigation) **Example: Columns with cell content focusable** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const inputStyle = { background: 'white', color: 'black', padding: '2px 10px', }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', // this makes the column content focusable contentFocusable: true, renderValue: ({ value }) => ( ), }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage', // this makes the column content focusable contentFocusable: true, renderValue: ({ value }) => ( ), }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-contentFocusable-example" columns={columns} /> ); } ``` ### columns.cssEllipsis (`boolean`) > Specifies if the column should show ellipsis for content that is too long and does not fit the column width. For header ellipsis, see related [`headerCssEllipsis`](https://infinite-table.com/docs/reference/infinite-table-props.md#headerCssEllipsis). **Example: First name column(first) has cssEllipsis set to false** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', cssEllipsis: false, defaultWidth: 60, }, preferredLanguage: { field: 'preferredLanguage', defaultWidth: 100, headerCssEllipsis: false, cssEllipsis: true, }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function CssEllipsis() { return ( <> primaryKey="id" data={dataSource}> debugId="columns-cssEllipsis-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ### columns.dataType (`string`) > Specifies the type of the data for the column. For now, it's better to simply use [`columns.type`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type). If a column doesn't specify a [sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType), the `dataType` will be used instead to determine the type of sorting to use. If neither `sortType` nor `dataType` are specified, the [column.type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) will be used. ### columns.defaultDraggable (`boolean`) > Specifies whether the column is draggable by default (for reordering columns). This property overrides the global [`columnDefaultDraggable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultDraggable). ### draggableColumns (`boolean`) > Specifies whether columns are draggable (for reordering columns). This property overrides the global [`columnDefaultDraggable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultDraggable) and the column-level [`columns.defaultDraggable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultDraggable). ### columnDefaultDraggable (`boolean`) > Specifies whether columns are draggable by default (for reordering columns). This is overriden by [`columns.defaultDraggable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultDraggable) and [`draggableColumns`](https://infinite-table.com/docs/reference/infinite-table-props.md#draggableColumns). ### columns.defaultEditable (`boolean|(param)=>boolean|Promise`) > Controls if the column is editable or not. This overrides the global [`columnDefaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultEditable). This is overridden by the [`editable`](https://infinite-table.com/docs/reference/infinite-table-props.md#editable) prop. The value for this property can be either a `boolean` or a function. If it is a function, it will be called when an edit is triggered on the column. The function will be called with a single object that contains the following properties: - `value` - the current value of the cell (the value currently displayed, so after [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) have been applied) - `rawValue` - the current value of the cell, but before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `data` - the data object (of type `DATA_TYPE`) for the current row - `rowInfo` - the row info object that underlies the row - `column` - the current column on which editing is invoked - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSourceApi` - - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) The function can return a `boolean` value or a `Promise` that resolves to a `boolean` - this means you can asynchronously decide whether the cell is editable or not. Making [column.defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) a function gives you the ability to granularly control which cells are editable or not (even within the same column, based on the cell value or other values you have access to). By default, double-clicking an editable cell will show the cell editor. You can prevent this by returning `{preventEdit: true}` from the [onCellDoubleClick](https://infinite-table.com/docs/reference/infinite-table-props.md#onCellDoubleClick) function prop. **Example** Only the `salary` column is editable. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, salary: { // the only editable column defaultEditable: true, defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { const shouldAcceptEdit = ({ value }: { value: any }) => { return parseInt(value, 10) == value; }; return ( <> primaryKey="id" data={dataSource}> debugId="global-should-accept-edit-example" columns={columns} columnDefaultEditable={false} shouldAcceptEdit={shouldAcceptEdit} /> ); } ``` ### columns.defaultFlex (`number`) > Specifies a default flex for the column If you want more control on sizing, use controlled [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) (or uncontrolled [`defaultColumnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing)). See related [`columns.defaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth) ### columnGroupVisibility (`Record`) > Controls the visibility of column groups. By default, column groups are visible. ```tsx columnGroupVisibility={{ 'country': false, 'city': true, }} columns={{...}} /> ``` ### columns.defaultHiddenWhenGroupedBy (`'*'| true | keyof DATA_TYPE | { [keyof DATA_TYPE]: true }`) > Controls default column visibility when [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) is used. This property does not apply (work) when controlled [`columnVisibility`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnVisibility) is used, it only works with uncontrolled column visibility. The value for this property can be one of the following: - the `'*'` string - this means, the column is hidden whenever there are groups - so any groups. - a `string`, namely a field from the bound type of the `DataSource` (so type is `keyof DATA_TYPE`) - the column is hidden whenever there is grouping that includes the specified field. The grouping can contain any other fields, but if it includes the specified field, the column is hidden. - `true` - the column is hidden when there grouping that uses the field that the column is bound to. - `an object with keys` of type `keyof DATA_TYPE` and values being `true` - whenever the grouping includes any of the fields that are in the keys of this object, the column is hidden. **Example** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage', // hide whenever grouped by preferredLanguage defaultHiddenWhenGroupedBy: 'preferredLanguage', }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city', // hide whenever grouped by country or city defaultHiddenWhenGroupedBy: { country: true, city: true, }, }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', // hide whenever there is grouping defaultHiddenWhenGroupedBy: '*', }, currency: { field: 'currency' }, }; const columnSizing: InfiniteTablePropColumnSizing = { country: { width: 100 }, city: { flex: 1, minWidth: 100 }, salary: { flex: 2, maxWidth: 500 }, }; export default function App() { return ( data={dataSource} defaultGroupBy={[ { field: 'stack' }, { field: 'preferredLanguage' }, { field: 'country' }, ]} primaryKey="id" > debugId="columnDefaultHiddenWhenGroupedBy-example" columns={columns} columnDefaultWidth={250} columnSizing={columnSizing} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### columns.defaultWidth (`number`) > Specifies a default width for the column If you want more control on sizing, use controlled [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) (or uncontrolled [`defaultColumnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing)). See related [`columns.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) ### columns.field (`keyof DATA_TYPE`) > Binds the column to the specified data field. It should be a keyof `DATA_TYPE`. It can be the same or different to the column id. This is not used for referencing the column in various other props - the column key (column id) is used for that. If no [`columns.header`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) is specified, it will be used as the column header. **Example** ```ts files=["columns-example.page.tsx","data.ts"] ``` Group columns can also be bound to a field, like in the snippet below. **Example** In this example, the group column is bound to the `firstName` field, so this field will be rendered in non-group rows for this column. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTableProps, InfiniteTablePropColumns, DataSourceProps, } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', }, canDesign: { field: 'canDesign', renderGroupValue: ({ value }) => `Can design: ${value || ''}`, }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', renderSelectionCheckBox: true, defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { return ( data={dataSource} groupBy={defaultGroupBy} selectionMode="multi-row" primaryKey="id" > debugId="group-column-bound-to-field-example" columns={columns} domProps={domProps} groupColumn={groupColumn} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### columns.filterType (`string`) > Use this to configure the filter type for the column, when the `filterType` needs to be different from the column [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type). See related [`columns.type`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) If the type of filter you want to show does not match the column [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type), you can specify the filter with the [column.filterType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.filterType) property. Only use this when the type of the data differs from the type of the filter (eg: you have a numeric column, with a custom filter type). **Example: Custom column filterType for the salary column** In this example, the `salary` column has `type="number"` and `filterType="salary"`. This means the sort order defined for `type="number"` will be used while displaying a custom type of filter. ```ts import * as React from 'react'; import { DataSourceData, DataSource, InfiniteTable, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { defaultFilterable: true, field: 'salary', type: 'number', filterType: 'salary', }, firstName: { field: 'firstName', }, stack: { field: 'stack' }, currency: { field: 'currency', defaultFilterable: false }, }; function getIcon(icon: string) { return () => (
{icon}
); } const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" filterTypes={{ salary: { defaultOperator: 'gt', emptyValues: ['', null, undefined], operators: [ { name: 'gt', label: 'Salary Greater Than', components: { Icon: getIcon('>'), }, fn: ({ currentValue, filterValue }) => { return currentValue > filterValue; }, }, { name: 'gte', components: { Icon: getIcon('>='), }, label: 'Salary Greater Than or Equal', fn: ({ currentValue, filterValue }) => { return currentValue >= filterValue; }, }, { name: 'lt', components: { Icon: getIcon('<'), }, label: 'Salary Less Than', fn: ({ currentValue, filterValue }) => { return currentValue < filterValue; }, }, { name: 'lte', components: { Icon: getIcon('<='), }, label: 'Salary Less Than or Equal', fn: ({ currentValue, filterValue }) => { return currentValue <= filterValue; }, }, ], }, }} > debugId="column-filterType-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ### columns.getValueToEdit (`(params) => any|Promise`) > Allows customizing the value that will be passed to the cell editor when it is displayed (when editing starts). The function is called with an object that has the following properties: - `value` - the value of the cell (the value that is displayed in the cell before editing starts). This is the value resulting after [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) have been applied) - `rawValue` - the raw value of the cell, before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `data` - the current data object - `rowInfo` - the row info object that underlies the row - `column` - the current column on which editing is invoked - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSourceApi` - - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) This function can be async. Return a `Promise` to wait for the value to be resolved and then passed to the cell editor. See related [`columns.getValueToPersist`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.getValueToPersist) and [`columns.shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit). **Example** In this example, the `salary` for each row includes the currency string.

When editing starts, we want to remove the currency string and only show the numeric value in the editor - we do this via [`columns.getValueToEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.getValueToEdit).

```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="inline-editing-custom-edit-value-example" columns={columns} columnDefaultEditable /> ); } ``` ### columns.getValueToPersist (`(params) => any|Promise`) > Allows customizing the value that will be persisted when an edit has been accepted. The function is called with an object that has the following properties: - `initialValue` - the initial value of the cell (the value that was displayed in the cell before editing started). This is the value resulting after [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) have been applied) - `value` - the current value that was accepted as an edit and which came from the cell editor. - `rawValue` - the raw value of the cell, before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `data` - the current data object - `rowInfo` - the row info object that underlies the row - `column` - the current column on which editing is invoked - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSourceApi` - - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) This function can be async. Return a `Promise` to wait for the value to be resolved and then persisted. See related [`columns.getValueToEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.getValueToEdit) and [`columns.shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit). **Example** In this example, the `salary` for each row includes the currency string.

When an edit is accepted, we want the persisted value to include the currency string as well (like the original value did) - we do this via [`columns.getValueToPersist`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.getValueToPersist).

```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="inline-editing-custom-edit-value-example" columns={columns} columnDefaultEditable /> ); } ``` ### columns.renderHeader (`(param: InfiniteTableColumnHeaderParam) => ReactNode`) > A custom rendering function for the column header. Called with an object of type [`InfiniteTableColumnHeaderParam`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnHeaderParam). It's the equivalent of [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) but for the [column.header](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header). It gives you access to the column, along with information about sorting, filtering, grouping, etc. It is called with a single argument, of type [`InfiniteTableColumnHeaderParam`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnHeaderParam). ### columns.header (`React.ReactNode|({column, columnSortInfo, columnApi})=>React.ReactNode`) > Specifies the column header. Can be a static value or a function that returns a React node. If no `header` is specified for a column, the [field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) will be used instead. If a function is provided, it will be called with an argument with the following properties: - `column` - `columnSortInfo` - will allow you to render custom header based on the sort state of the column. - `columnApi` - [API](reference/column-api) for the current column. Can be useful if you customize the header and want to programatically trigger actions like sorting, show/hide column menu, etc. When we implement filtering, you'll also have access to the column filter. For styling the column header, you can use [headerStyle](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerStyle) or [headerClassName](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerClassName). For configuring the column header height, see the [`columnHeaderHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnHeaderHeight) prop. **Example** ```ts files=["columns-header-example.page.tsx","data.ts"] ``` In the `column.header` function you can use hooks or [render custom React components via column.components.HeaderCell](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.HeaderCell). To make it easier to access the param of the `header` function, we've exposed the [`useInfiniteHeaderCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteHeaderCell) - use it to gain access to the same object that is passed as an argument to the `header` function. **Example: Column with custom header that uses useInfiniteHeaderCell** ```ts import { InfiniteTable, DataSource, useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const HobbyHeader: React.FC = function () { const { column } = useInfiniteHeaderCell(); return {column?.field} 🤷📸👨🏻‍🍳💃📚⛹️; }; const columns: InfiniteTablePropColumns = { id: { field: 'id', maxWidth: 80 }, stack: { field: 'stack', }, hobby: { field: 'hobby', components: { HeaderCell: HobbyHeader, }, }, }; export default function ColumnHeaderExampleWithHooks() { return ( <> primaryKey="id" data={dataSource}> debugId="column-header-hooks-example" columns={columns} columnDefaultWidth={200} /> ); } ``` **Example: Custom header with button to trigger the column context menu** The `preferredLanguage` column has a custom header that shows a button for triggering the column context menu. In addition, the currency and preferredLanguage columns have a custom context menu icon. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { currency: { field: 'currency', // custom menu icon renderMenuIcon: () =>
🌎
, }, preferredLanguage: { field: 'preferredLanguage', defaultWidth: 350, header: ({ columnApi, renderLocation }) => { // if we're inside the column menu with all columns, return only the col name if (renderLocation === 'column-menu') { return 'Preferred Language'; } // but for the real column header // return this custom content return ( <> Preferred Language{' '} ); }, // custom menu icon renderMenuIcon: () =>
🌎
, }, salary: { field: 'salary', // hide the menu icon renderMenuIcon: false, }, country: { field: 'country', }, id: { field: 'id', defaultWidth: 80, renderMenuIcon: false }, firstName: { field: 'firstName', }, }; export default function ColumnContextMenuItems() { return ( <> primaryKey="id" data={dataSource}> debugId="getColumnMenuItems-example" columnHeaderHeight={70} columns={columns} getColumnMenuItems={(items, { column }) => { if (column.id === 'firstName') { // you can adjust the default items for a specific column items.splice(0, 0, { key: 'firstName', label: 'First name menu item', onAction: () => { console.log('Hey there!'); }, }); } items.push( { key: 'hello', label: 'Hello World', onAction: () => { alert('Hello World from column ' + column.id); }, }, { key: 'translate', label: 'Translate', menu: { items: [ { key: 'translateToEnglish', label: 'English', onAction: () => { console.log('Translate to English'); }, }, { key: 'translateToFrench', label: 'French', onAction: () => { console.log('Translate to French'); }, }, ], }, }, ); return items; }} /> ); } ``` ### columns.headerClassName (`string | (args) => string`) > Controls the css class name for the column header. Can be a string or a function returning a string. If defined as a function, it accepts an object as a parameter, which has the following properties: - `column` - the current column where the style is being applied - `columnSortInfo` - the sorting information for the column - `columnFilterValue` - the filtering information for the column - `dragging` - whether the current column is being dragged at the current time (during a column reorder) The `headerClassName` property can also be specified for [columnTypes](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.headerClassName). For styling with inline styles, see [`columns.headerStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerStyle). ### columns.headerCssEllipsis (`boolean`) > Specifies if the column should show ellipsis in the column header if the header is too long and does not fit the column width. If this property is not specified, the value of [`columns.cssEllipsis`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.cssEllipsis) will be used. For normal cell ellipsis, see related [`cssEllipsis`](https://infinite-table.com/docs/reference/infinite-table-props.md#cssEllipsis). **Example: Preferred Language column(second) has headerCssEllipsis set to false** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', cssEllipsis: false, defaultWidth: 60, }, preferredLanguage: { field: 'preferredLanguage', defaultWidth: 100, headerCssEllipsis: false, cssEllipsis: true, }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function CssEllipsis() { return ( <> primaryKey="id" data={dataSource}> debugId="columns-cssEllipsis-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ### columns.headerStyle (`CSSProperties | (args) => CSSProperties`) > Controls styling for the column header. Can be a style object or a function returning a style object. If defined as a function, it accepts an object as a parameter, which has the following properties: - `column` - the current column where the style is being applied - `columnSortInfo` - the sorting information for the column - `columnFilterValue` - the filtering information for the column - `dragging` - whether the current column is being dragged at the current time (during a column reorder) The `headerStyle` property can also be specified for [columnTypes](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.headerStyle). For styling with CSS, see [`columns.headerClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerClassName). For configuring the column header height, see the [`columnHeaderHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnHeaderHeight) prop. ### columns.maxWidth (`number`) > Configures the maximum width for the column. If not specified, [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth) will be used (defaults to `2000`). ### columns.minWidth (`number`) > Configures the minimum width for the column. If not specified, [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth) will be used (defaults to `30`). ### columns.render (`(cellContext) => Renderable`) > Customizes the rendering of the column. The argument passed to the function is an object of type [`InfiniteTableColumnCellContextType`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnCellContextType) See related [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue), [`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue) The difference between [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) is only for special columns (for now, only group columns are special columns, but more will come) when `InfiniteTable` renders additional content inside the column (eg: collapse/expand tool for group rows). The [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) function allows you to override the additional content. So if you specify this function, it's up to you to render whatever content, including the collapse/expand tool. Note that for customizing the collapse/expand tool, you can use specify `renderGroupIcon` function on the group column. To understand how the rendering pipeline works, head over to the page on [Column rendering](https://infinite-table.com/docs/learn/columns/column-rendering.md#rendering-pipeline). The [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) and [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) functions are called with an object that has the following properties: - data - the data object (of type `DATA_TYPE | Partial | null`) for the row. - rowInfo - very useful information about the current row. See [Using RowInfo](https://infinite-table.com/docs/learn/rows/using-row-info.md) for more details. - renderBag - read more about this in the docs for [Column rendering pipeline](https://infinite-table.com/docs/learn/columns/column-rendering.md#rendering-pipeline) **Example: Column with custom render** ```ts import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const defaultGroupBy: DataSourceGroupBy[] = [{ field: 'stack' }]; const groupColumn: InfiniteTablePropGroupColumn = { defaultWidth: 250, render: ({ rowInfo, toggleCurrentGroupRow }) => { if (rowInfo.isGroupRow) { const { collapsed } = rowInfo; const expandIcon = ( {collapsed ? ( <> ) : ( )} ); return (
toggleCurrentGroupRow()} > Grouped by {rowInfo.value} {expandIcon}
); } return null; }, }; export default function ColumnCustomRenderExample() { return ( <> primaryKey="id" data={dataSource} defaultGroupBy={defaultGroupBy} > debugId="column-render-example" groupColumn={groupColumn} columns={columns} columnDefaultWidth={200} /> ); } ``` In the `column.render` function you can use hooks or [render custom React components](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell). To make it easier to access the param of the `render` function, we've exposed the [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) - use it to gain access to the same object that is passed as an argument to the `render` function. **Example: Column with custom render that uses useInfiniteColumnCell** ```ts import { InfiniteTable, DataSource, useInfiniteColumnCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { HTMLProps } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; function CustomCell(_props: HTMLProps) { const { value, data } = useInfiniteColumnCell(); let emoji = '🤷'; switch (value) { case 'photography': emoji = '📸'; break; case 'cooking': emoji = '👨🏻‍🍳'; break; case 'dancing': emoji = '💃'; break; case 'reading': emoji = '📚'; break; case 'sports': emoji = '⛹️'; break; } const label = data?.stack === 'frontend' ? '⚛️' : ''; return ( {emoji} + {label} ); } const columns: InfiniteTablePropColumns = { id: { field: 'id', maxWidth: 80 }, firstName: { field: 'firstName' }, hobby: { field: 'hobby', // we're not using the arg of the render function directly // but CustomCell uses `useInfiniteColumnCell` to retrieve it instead render: () => , }, }; export default function ColumnRenderWithHooksExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-render-hooks-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ### columns.renderFilterIcon > Customizes the rendering of the filter icon for the column. **Example: Custom filter icons for salary and name columns** The `salary` column will show a bolded label when filtered. The `firstName` column will show a custom filter icon when filtered. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; const data: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', type: 'number', defaultWidth: 100, }, salary: { field: 'salary', type: 'number', header: ({ filtered }) => { return filtered ? Salary : 'Salary'; }, renderFilterIcon: () => { return null; }, }, firstName: { field: 'firstName', renderFilterIcon: ({ filtered }) => { return filtered ? '🔥' : ''; }, }, stack: { field: 'stack' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default () => { return ( <> data={data} primaryKey="id" defaultFilterValue={[]} filterDelay={0} filterMode="local" > debugId="column-filter-icon-example" domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={columns} /> ); }; ``` ### columns.renderGroupIcon (`(cellContext) => Renderable`) > Customizes the rendering of the collapse/expand group icon for group rows. The argument passed to the function is an object of type [`InfiniteTableColumnCellContextType`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnCellContextType) For actual content of group cells, see related [`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue) To understand how the rendering pipeline works, head over to the page on [Column rendering](https://infinite-table.com/docs/learn/columns/column-rendering.md#rendering-pipeline). **Example: Column with custom renderGroupIcon** ```tsx import { InfiniteTable, DataSource, DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderGroupValue: ({ rowInfo, value }) => { return ( <> {value} → {rowInfo.value} stuff ); }, renderLeafValue: ({ value, rowInfo }) => { return ( 🎇 {value} → {rowInfo.value} ); }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const groupBy: DataSourcePropGroupBy = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}, }, }, ]; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="column-renderGroupValueAndRenderLeafValue-example" columns={columns} /> ); } ``` ### columns.renderMenuIcon (`boolean|(cellContext)=> ReactNode`) > Allows customization of the context menu icon. Use this prop to customize the context icon for the current column. Specify `false` for no context menu icon. Use a function to render a custom icon. The function is called with an object that has the following properties: - `column` - `columnApi` - an API object for controlling the column programatically (toggle sort, toggle column context menu, etc) **Example: Custom menu icons and custom menu items** In this example, the currency and preferredLanguage columns have a custom icon for triggering the column context menu. In addition, the `preferredLanguage` column has a custom header that shows a button for triggering the column context menu. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { currency: { field: 'currency', // custom menu icon renderMenuIcon: () =>
🌎
, }, preferredLanguage: { field: 'preferredLanguage', defaultWidth: 350, header: ({ columnApi, renderLocation }) => { // if we're inside the column menu with all columns, return only the col name if (renderLocation === 'column-menu') { return 'Preferred Language'; } // but for the real column header // return this custom content return ( <> Preferred Language{' '} ); }, // custom menu icon renderMenuIcon: () =>
🌎
, }, salary: { field: 'salary', // hide the menu icon renderMenuIcon: false, }, country: { field: 'country', }, id: { field: 'id', defaultWidth: 80, renderMenuIcon: false }, firstName: { field: 'firstName', }, }; export default function ColumnContextMenuItems() { return ( <> primaryKey="id" data={dataSource}> debugId="getColumnMenuItems-example" columnHeaderHeight={70} columns={columns} getColumnMenuItems={(items, { column }) => { if (column.id === 'firstName') { // you can adjust the default items for a specific column items.splice(0, 0, { key: 'firstName', label: 'First name menu item', onAction: () => { console.log('Hey there!'); }, }); } items.push( { key: 'hello', label: 'Hello World', onAction: () => { alert('Hello World from column ' + column.id); }, }, { key: 'translate', label: 'Translate', menu: { items: [ { key: 'translateToEnglish', label: 'English', onAction: () => { console.log('Translate to English'); }, }, { key: 'translateToFrench', label: 'French', onAction: () => { console.log('Translate to French'); }, }, ], }, }, ); return items; }} /> ); } ``` ### columns.renderSelectionCheckBox (`boolean | ({ data, rowSelected: boolean | null, selectRow, deselectRow, ... })`) > Specifies that the current column will have a selection checkbox - if a function is provided, will be used to customizes the rendering of the checkbox rendered for selection. See related [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection). If `true` is provided, the default selection checkbox will be rendered. When a function is provided, it will be used for rendering the checkbox for selection. `rowSelected` property in the function parameter can be either `boolean` or `null`. The `null` value is used for groups with indeterminate state, meaning the group has some children selected, but not all of them. To understand how the rendering pipeline works, head over to the page on [Column rendering](https://infinite-table.com/docs/learn/columns/column-rendering.md#rendering-pipeline). **Example: Column with custom renderSelectionCheckBox** This example shows how you can use the default selection checkbox and decorate it. ```tsx import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTableProps, InfiniteTablePropColumns, DataSourceProps, } from '@infinite-table/infinite-react'; import * as React from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', renderGroupValue: ({ value }) => `Stack: ${value || ''}`, }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', renderGroupValue: ({ value }) => `Lang: ${value || ''}`, }, canDesign: { field: 'canDesign', renderGroupValue: ({ value }) => `Can design: ${value || ''}`, }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', renderSelectionCheckBox: ({ renderBag }) => { // render the default value and decorate it return [{renderBag.selectionCheckBox}]; }, defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { return ( data={dataSource} groupBy={defaultGroupBy} selectionMode="multi-row" primaryKey="id" > debugId="column-renderSelectionCheckBox-example" columns={columns} domProps={domProps} groupColumn={groupColumn} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### columns.renderGroupValue (`({ data, rowInfo, column, renderBag, rowIndex, ... })`) > Customizes the rendering of a group column content, but only for group rows. This prop is different from [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render), [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue), as it is only called for group rows. This function prop is called with a parameter - the `value` property of this parameter is not useful for group rows (of non-group columns), as it refers to the current data item, which is a group item, not a normal data item. Instead, use `rowInfo.value`, as that's the current group row value. See related [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon) for customizing the collapse/expand group icon. See related [`columns.renderLeafValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderLeafValue) for customizing the value for non-group rows in a group column. **Example: Column with custom renderGroupValue** ```tsx import { InfiniteTable, DataSource, DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderGroupValue: ({ rowInfo, value }) => { return ( <> {value} → {rowInfo.value} stuff ); }, renderLeafValue: ({ value, rowInfo }) => { return ( 🎇 {value} → {rowInfo.value} ); }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const groupBy: DataSourcePropGroupBy = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}, }, }, ]; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="column-renderGroupValueAndRenderLeafValue-example" columns={columns} /> ); } ``` ### columns.renderLeafValue (`({ data, rowInfo, column, renderBag, rowIndex, ... })`) > Customizes the rendering of the group column content, but only for non-group rows. See related [`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue) for customizing the value for group rows in a group column. **Example: Column with custom renderLeafValue** ```tsx import { InfiniteTable, DataSource, DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderGroupValue: ({ rowInfo, value }) => { return ( <> {value} → {rowInfo.value} stuff ); }, renderLeafValue: ({ value, rowInfo }) => { return ( 🎇 {value} → {rowInfo.value} ); }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const groupBy: DataSourcePropGroupBy = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}, }, }, ]; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="column-renderGroupValueAndRenderLeafValue-example" columns={columns} /> ); } ``` ### columns.renderValue (`(cellContext) => Renderable`) > Customizes the rendering of the column content. The argument passed to the function is an object of type [`InfiniteTableColumnCellContextType`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnCellContextType) See related [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render), [`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue) The difference between [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) and [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) is only for special columns (for now, only group columns are special columns, but more will come) when `InfiniteTable` renders additional content inside the column (eg: collapse/expand tool for group rows). The [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) function allows you to override the additional content. So if you specify this function, it's up to you to render whatever content, including the collapse/expand tool. Note that for customizing the collapse/expand tool, you can use specify `renderGroupIcon` function on the group column. To understand how the rendering pipeline works, head over to the page on [Column rendering](https://infinite-table.com/docs/learn/columns/column-rendering.md#rendering-pipeline). The [renderValue](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) and [render](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) functions are called with an object that has the following properties: - data - the data object (of type `DATA_TYPE | Partial | null`) for the row. - rowInfo - very useful information about the current row. See [Using RowInfo](https://infinite-table.com/docs/learn/rows/using-row-info.md) for more details. - renderBag - read more about this in the docs for [Column rendering pipeline](https://infinite-table.com/docs/learn/columns/column-rendering.md#rendering-pipeline) **Example: Column with custom renderValue** ```tsx import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, InfiniteTableColumnRenderValueParam, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderValue: ({ data, rowInfo }) => { if (rowInfo.isGroupRow) { return <>{rowInfo.value} stuff; } return 🎇 {data?.stack}; }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const defaultGroupBy: DataSourceGroupBy[] = [{ field: 'stack' }]; const groupColumn: InfiniteTablePropGroupColumn = { defaultWidth: 250, renderValue: ({ rowInfo, }: InfiniteTableColumnRenderValueParam) => { if (rowInfo.isGroupRow) { return ( <> Grouped by {rowInfo.value} ); } return null; }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource} defaultGroupBy={defaultGroupBy} > debugId="column-renderValue-example" groupColumn={groupColumn} columns={columns} columnDefaultWidth={200} /> ); } ``` In the `column.renderValue` function you can use hooks or [render custom React components](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell). To make it easier to access the param of the `renderValue` function, we've exposed the [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) - use it to gain access to the same object that is passed as an argument to the `renderValue` function. **Example: Using a sparkline component** ```tsx import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import { Sparklines, SparklinesLine } from 'react-sparklines'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; bugFixes: number[]; streetName: string; streetPrefix: string; streetNo: string; department: string; team: string; salary: number; currency: number; age: number; email: string; }; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, bugFixes: { field: 'bugFixes', header: 'Bug Fixes', defaultWidth: 300, renderValue: ({ value, data }) => { const color = data?.department === 'IT' || data?.department === 'Management' ? 'tomato' : '#253e56'; return ( ); }, }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees10k') .then((r) => r.json()) .then((data: Employee[]) => { return data.map((employee) => { return { ...employee, bugFixes: [...Array(10)].map(() => Math.round(Math.random() * 100)), }; }); }); }; export default function App() { return ( data={dataSource} primaryKey="id"> debugId="using-sparklines-example" columns={columns} columnDefaultWidth={150} /> ); } ``` ### columns.resizable (`boolean`) > Specifies if the current column is resizable or not. By default, all columns are resizable, since [`resizableColumns`](https://infinite-table.com/docs/reference/infinite-table-props.md#resizableColumns) defaults to `true`. ### columns.rowspan (`({ rowInfo, data, rowIndex, column }) => number`) > Specifies the rowspan for cells on the current column. The default rowspan for a column cell is 1. If you want to span multiple rows, return a value that is greater than 1. This function is called with an object that has the following properties: - column - the current column - data - the current data - rowInfo - information about the current row The `rowInfo` object contains information about grouping (if this row is a group row, the collapsed state, etc), parent groups, children of the current row (if it's a row group), etc. See [Using RowInfo](https://infinite-table.com/docs/learn/rows/using-row-info.md) for more details. **Example** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, DataSourceGroupBy, InfiniteTableGroupColumnBase, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage', }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city', }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, currency: { field: 'currency' }, }; const defaultGroupBy: DataSourceGroupBy[] = [ { field: 'stack' }, { field: 'preferredLanguage' }, { field: 'country', column: { rowspan: ({ rowInfo }) => { const rowspan = rowInfo.isGroupRow && rowInfo.groupNesting === 3 && !rowInfo.collapsed ? (rowInfo.deepRowInfoArray?.length || 0) + 1 : 1; return rowspan; }, } as InfiniteTableGroupColumnBase, }, ]; export default function App() { return ( data={dataSource} defaultGroupBy={defaultGroupBy} primaryKey="id" > debugId="column-rowspan-example" columns={columns} columnDefaultWidth={250} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### columns.shouldAcceptEdit (`(params) => boolean|Error|Promise`) > Function specified for the column, that determines whether to accept an edit or not. This function is called when the user wants to finish an edit. The function is used to decide whether an edit is accepted or rejected.

When the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) prop is specified, this is no longer called, and instead the global one is called.

If you define the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) and still want to use the column-level function, you can call the column-level function from the global one.

The function is called with an object that has the following properties: - `value` - the value that the user wants to persist via the cell editor - `initialValue` - the initial value of the cell (the value that was displayed before editing started). This is the value resulting after [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) have been applied) - `rawValue` - the initial value of the cell, but before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `data` - the current data object - `rowInfo` - the row info object that underlies the row - `column` - the current column on which editing is invoked - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSourceApi` - - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) **Example** Try editing the `salary` column. In the editor you can write whatever, but the column will only accept edits that are valid numbers. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80, defaultEditable: false }, salary: { defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, shouldAcceptEdit: ({ value }) => { return parseInt(value, 10) == value; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="inline-editing-custom-edit-value-example" columns={columns} columnDefaultEditable /> ); } ``` ### columns.sortable (`boolean`) > Specifies the sorting behavior for the current column. Overrides the global [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) prop. Use this column property in order to explicitly make the column sortable or not sortable. If not specified, the sortable prop from the column type ([`columnTypes.sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.sortable)) will be used. If that is not specified either, the global [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) prop will be used. ### columns.sortType (`string | string[]`) > Specifies the sort type for the column. See related [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) For local sorting, the sort order for a column is determined by the specified `sortType`. - if no `sortType` is specified, the [column.dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) will be used as the `sortType` - if no `sortType` or `dataType` is specified, it will default to the [`columns.type`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) value (if an array, the first item will be used). - if none of those are specified `"string"` is used The value of this prop (as specified, or as computed by the steps described above) should be a key from the [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) object. **Example: Custom sort by color - magenta will come first** ```ts import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export type CarSale = { id: number; make: string; model: string; year: number; sales: number; color: string; }; const carsales: CarSale[] = [ { make: 'Volkswagen', model: 'GTI', year: 2009, sales: 6, color: 'red', id: 0, }, { make: 'Honda', model: 'Element 2WD', year: 2009, sales: 739, color: 'red', id: 1, }, { make: 'Acura', model: 'RDX 4WD', year: 2008, sales: 2, color: 'magenta', id: 2, }, { make: 'Honda', model: 'Fit', year: 2009, sales: 211, color: 'blue', id: 3, }, { make: 'Mazda', model: '6', year: 2009, sales: 31, color: 'blue', id: 4, }, { make: 'Acura', model: 'TSX', year: 2009, sales: 14, color: 'yellow', id: 5, }, { make: 'Acura', model: 'TSX', year: 2010, sales: 14, color: 'red', id: 6, }, { make: 'Audi', model: 'A3', year: 2009, sales: 2, color: 'magenta', id: 7, }, ]; const columns: Record> = { color: { field: 'color', sortType: 'color' }, make: { field: 'make' }, model: { field: 'model' }, sales: { field: 'sales', sortType: 'number', }, year: { field: 'year', sortType: 'number', }, }; const newSortTypes = { color: (one: string, two: string) => { if (one === 'magenta') { // magenta comes first return -1; } if (two === 'magenta') { // magenta comes first return 1; } return one.localeCompare(two); }, }; export default function DataTestPage() { return ( <> data={carsales} primaryKey="id" defaultSortInfo={{ field: 'color', dir: 1, type: 'color', }} sortTypes={newSortTypes} > debugId="sortTypes-example" columns={columns} /> ); } ``` For group columns (and more specifically, when [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is `single-column`), the `sortType` should be a `string[]`, each item in the array corresponding to an item in [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) of the ``. This is especially useful when there are no corresponding columns for the `groupBy` fields. In this case, `InfiniteTable` can't know the type of sorting those fields will require, so you have to provide it yourself via the `column.sortType`. ### columns.style (`CSSProperties | (param: InfiniteTableColumnStyleFnParams) => CSSProperties`) > Controls styling for the column. Can be a style object or a function returning a style object. If defined as a function, it accepts an object as a parameter (of type [`InfiniteTableColumnStylingFnParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnStylingFnParams)), which has the following properties: - `column` - the current column where the style is being applied - `data` - the data object for the current row. The type of this object is `DATA_TYPE | Partial | null`. For regular rows, it will be of type `DATA_TYPE`, while for group rows it will be `Partial`. For rows not yet loaded (because of batching being used), it will be `null`. - `rowInfo` - the information about the current row - see [Using RowInfo](https://infinite-table.com/docs/learn/rows/using-row-info.md) for more details. - `value` - the underlying value of the current cell - will generally be `data[column.field]`, if the column is bound to a `field` property - ... and more, see [`InfiniteTableColumnStylingFnParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableColumnStylingFnParams) for details The `style` property can also be specified for [`columnTypes`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) **Example** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', style: { background: 'gray', color: 'white', }, }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number', style: ({ value }) => { return { color: value && value > 100_000 ? 'red' : 'currentColor', }; }, }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function GroupByExample() { return ( <> primaryKey="id" data={dataSource}> debugId="columns-style-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ### columns.type (`string | string[]`) > Specifies the column type - a column type is a set of properties that describes the column. Column types allow to easily apply the same properties to multiple columns. Specifying `type: "number"` for numeric columns will ensure correct number sorting function is used (when sorting is done client-side). This happens because [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) has a definition for the `number` sort type. For date columns (where the values in the columns are actual date objects) specify `type: "date"`. [Read more about date columns here](https://infinite-table.com/docs/learn/working-with-data/handling-dates.md#using-date-strings) See [`columnTypes`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) for more details on using column types. By default, all columns have the `default` column type applied. So, if you define the `default` column type, but don't specify any [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) for a column, the default column type properties will be applied to that column. When you want both the default type and another type to be applied, you can do so by specifying `type: ["default", "second-type"]`. When you dont want the default type to be applied, use `type: null`. If a column is filterable and does not explicitly specify a [filterType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.filterType), the `type` will also be used as the filter type. If a column is sortable and does not explicitly specify a [sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType), the `type` will also be used as the sort type. See the example below - `id` and `age` columns are `type='number'`. **Example** ```ts files=["columns-example.page.tsx","data.ts"] ``` ### columns.valueFormatter (`({ data?, isGroupRow, rowInfo, field?, rowSelected, rowActive, isGroupRow }) => Renderable`) > Customizes the value that will be rendered The `valueFormatter` prop is the next function called after the [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) during the [rendering pipeline](https://infinite-table.com/docs/learn/columns/column-rendering.md#rendering-pipeline). Unlike [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter), [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) can return any renderable value, like `JSX.Element`s. Unlike `valueGetter`, it is being called with an object that has both the `data` item (might be null or partial for group rows) and the `rowInfo` object, and some extra flags regarding the row state (selection, active, etc). Use the TS `isGroupRow` flag as discriminator to decide if `data` is available. If you want to further customize what's being rendered, see related [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter), [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue), [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render), [`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue), [`columns.renderLeafValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderLeafValue) and [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon). **Example: Column with custom valueFormatter** ```tsx import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, name: { header: 'Full Name', valueFormatter: ({ data, isGroupRow, value }) => { if (isGroupRow) { return {value}; } return (
            {data.firstName}, {data.lastName}
          
); }, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, }; export default function ColumnValueFormatterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-valueFormatter-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ### columns.valueGetter (`({ data, field? }) => string | number | boolean | null | undefined`) > Customizes the value that will be rendered The `valueGetter` prop is a function that takes a single argument - an object with `data` and `field` properties. It should return a plain JavaScript value (so not a `ReactNode` or `JSX.Element`) Note that the `data` property is of type `DATA_TYPE | Partial | null` and not simply `DATA_TYPE`, because there are cases when you can have grouping (so for group rows with aggregations `data` will be `Partial`) or when there are lazily loaded rows or group rows with no aggregations - for which `data` is still `null`. If you want to further customize what's being rendered, see related [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter), [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue), [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render), [`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue), [`columns.renderLeafValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderLeafValue) and [`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon). **Example: Column with custom valueGetter** ```tsx import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, name: { header: 'Full Name', valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-valueGetter-example" columns={columns} columnDefaultWidth={200} /> ); } ``` ### columnSizing (`Record`) > Defines the sizing of columns in the grid. This is a controlled property. For the uncontrolled version, see [`defaultColumnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing). It is an object that maps column ids to column sizing options. The values in the objects can contain the following properties: - [flex](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.flex) - use this for flexible columns. Behaves like the `flex` CSS property. - [width](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.width) - use this for fixed sized columns - [minWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.minWidth) - specifies the minimum width of the column. Useful for flexible columns or for restricting users resizing both fixed and flexible columns. - [maxWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.maxWidth) - specifies the maximum width of the column. Useful for flexible columns or for restricting users resizing both fixed and flexible columns. **Example: Controlled column sizing** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [columnSizing, setColumnSizing] = React.useState({ country: { width: 100 }, city: { flex: 1, minWidth: 100 }, salary: { flex: 2, maxWidth: 500 }, }); const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current column sizing:{' '}

{JSON.stringify(columnSizing, null, 2)}
Viewport reserved width: {viewportReservedWidth} -{' '}

data={dataSource} primaryKey="id"> debugId="columnSizing-example" columns={columns} columnDefaultWidth={50} columnSizing={columnSizing} onColumnSizingChange={setColumnSizing} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` For auto-sizing columns, see [`autoSizeColumnsKey`](https://infinite-table.com/docs/reference/infinite-table-props.md#autoSizeColumnsKey). ### columnSizing.flex (`number`) > Specifies the flex value for the column. See [using flexible column sizing section](https://infinite-table.com/docs/learn/columns/fixed-and-flexible-size.md#using-flexible-column-sizing) for more details. A column can either be flexible or fixed-width. For fixed columns, use [`columnSizing.width`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.width) if you're using [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) or [column.defaultWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth) for default-uncontrolled sizing. **Example: Controlled column sizing with flex columns** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [columnSizing, setColumnSizing] = React.useState({ country: { width: 100 }, city: { flex: 1, minWidth: 100 }, salary: { flex: 2, maxWidth: 500 }, }); const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current column sizing:{' '}

{JSON.stringify(columnSizing, null, 2)}
Viewport reserved width: {viewportReservedWidth} -{' '}

data={dataSource} primaryKey="id"> debugId="columnSizing-example" columns={columns} columnDefaultWidth={50} columnSizing={columnSizing} onColumnSizingChange={setColumnSizing} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### columnSizing.minWidth (`number`) > Specifies the minimum width for a column. Especially useful for flexible columns. See [Using flexible column sizing](https://infinite-table.com/docs/learn/columns/fixed-and-flexible-size.md#using-flexible-column-sizing) for more details on the flex algorithm. This can also be specified for all columns by specifying [`columnMinWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMinWidth). **Example: Controlled column sizing with minWidth for column** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [columnSizing, setColumnSizing] = React.useState({ country: { width: 100 }, city: { flex: 1, minWidth: 100 }, salary: { flex: 2, maxWidth: 500 }, }); const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current column sizing:{' '}

{JSON.stringify(columnSizing, null, 2)}
Viewport reserved width: {viewportReservedWidth} -{' '}

data={dataSource} primaryKey="id"> debugId="columnSizing-example" columns={columns} columnDefaultWidth={50} columnSizing={columnSizing} onColumnSizingChange={setColumnSizing} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### columnSizing.maxWidth (`number`) > Specifies the maximum width for a column. Especially useful for flexible columns. See [Using flexible column sizing](https://infinite-table.com/docs/learn/columns/fixed-and-flexible-size.md#using-flexible-column-sizing) for more details on the flex algorithm. This can also be specified for all columns by specifying [`columnMaxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnMaxWidth). **Example: Controlled column sizing with maxWidth for column** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [columnSizing, setColumnSizing] = React.useState({ country: { width: 100 }, city: { flex: 1, minWidth: 100 }, salary: { flex: 2, maxWidth: 500 }, }); const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current column sizing:{' '}

{JSON.stringify(columnSizing, null, 2)}
Viewport reserved width: {viewportReservedWidth} -{' '}

data={dataSource} primaryKey="id"> debugId="columnSizing-example" columns={columns} columnDefaultWidth={50} columnSizing={columnSizing} onColumnSizingChange={setColumnSizing} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### columnSizing.width (`number`) > Specifies the fixed width for the column. See [Using flexible column sizing](https://infinite-table.com/docs/learn/columns/fixed-and-flexible-size.md#using-flexible-column-sizing) for more details. A column can either be flexible or fixed. For flexible columns, use [`columnSizing.flex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.flex). **Example: Controlled column sizing with fixed column** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [columnSizing, setColumnSizing] = React.useState({ country: { width: 100 }, city: { flex: 1, minWidth: 100 }, salary: { flex: 2, maxWidth: 500 }, }); const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current column sizing:{' '}

{JSON.stringify(columnSizing, null, 2)}
Viewport reserved width: {viewportReservedWidth} -{' '}

data={dataSource} primaryKey="id"> debugId="columnSizing-example" columns={columns} columnDefaultWidth={50} columnSizing={columnSizing} onColumnSizingChange={setColumnSizing} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### keyboardShortcuts (`{key,handler,when}[]`) > An array that specifies the keyboard shortcuts for the DataGrid. See the [Keyboard Shortcuts](https://infinite-table.com/docs/learn/keyboard-navigation/keyboard-shortcuts.md) page for more details. **Example** Click on a cell and use the keyboard to navigate. Press `Shift+Enter` to show an alert with the current active cell position. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage', header: 'Language' }, country: { field: 'country', header: 'Country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardShortcuts() { return ( <> primaryKey="id" data={dataSource}> debugId="keyboard-shortcuts-initial-example" columns={columns} keyboardShortcuts={[ { key: 'Shift+Enter', when: (context) => !!context.getState().activeCellIndex, handler: (context) => { const { activeCellIndex } = context.getState(); const [rowIndex, columnIndex] = activeCellIndex!; alert( `Current active cell: row ${rowIndex}, column ${columnIndex}.`, ); }, }, { key: 'PageUp', handler: () => { console.log('PageUp key pressed.'); }, }, { key: 'PageDown', handler: () => { console.log('PageDown key pressed.'); }, }, ]} /> ); } ``` Infinite Table DataGrid comes with some predefined keyboard shorcuts. you can import from the `keyboardShortcuts` named export. ```ts import { keyboardShortcuts } from '@infinite-table/infinite-react' ``` #### Instant Edit ```ts {4,12} import { DataSource, InfiniteTable, keyboardShortcuts } from '@infinite-table/infinite-react'; function App() { return primaryKey="id" data={dataSource}> columns={columns} keyboardShortcuts={[ keyboardShortcuts.instantEdit ]} /> } ``` For now, the only predefined keyboard shorcut is `keyboardShortcuts.instantEdit`. This keyboard shorcut starts cell editing when any key is pressed on the active cell. This is the same behavior found in Excel/Google Sheets. **Example** Click on a cell and then start typing to edit the cell. ```ts import { InfiniteTable, DataSource, DataSourceData, keyboardShortcuts, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage', header: 'Language' }, country: { field: 'country', header: 'Country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id', defaultEditable: false }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardShortcuts() { return ( <> primaryKey="id" data={dataSource}> debugId="keyboard-shortcuts-instant-edit-example" columns={columns} columnDefaultEditable keyboardShortcuts={[keyboardShortcuts.instantEdit]} /> ); } ``` ### columnTypes (`Record`) > Specifies an object that maps column type ids to column types. Column types are used to apply the same configuration/properties to multiple columns. See related [`columns.type`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) By default, all columns have the `default` column type applied. So, if you define the `default` column type, but don't specify any [type](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type) for a column, the default column type properties will be applied to that column. The following properties are currently supported for defining a column type: - `align` - See [`columns.align`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.align) - `className` - See [`columns.className`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.className) - `components` - See [`columns.components`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components) - `cssEllipsis` - See [`columns.cssEllipsis`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.cssEllipsis) - `defaultEditable` - See [`columns.defaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) - `defaultFlex` - default flex value (uncontrolled) for the column(s) this column type will be applied to. See [`column.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.defaultFlex) - `defaultWidth` - default width (uncontrolled) for the column(s) this column type will be applied to. See [`column.defaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.defaultWidth) - `getValueToEdit` - See [`columns.getValueToEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.getValueToEdit) - `getValueToPersist` - See [`columns.getValueToPersist`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.getValueToPersist) - `headerAlign` - See [`columns.headerAlign`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerAlign) - `headerCssEllipsis` - See [`columns.headerCssEllipsis`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerCssEllipsis) - `headerStyle` - See [`columns.headerStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerStyle) - `header` - See [`columns.header`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.header) - `maxWidth` - minimum width for the column(s) this column type will be applied to. See [`column.maxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.maxWidth) - `minWidth` - minimum width for the column(s) this column type will be applied to. See [`column.minWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.minWidth) - `renderMenuIcon` - See [`columns.renderMenuIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderMenuIcon) - `renderSortIcon` - See [`columns.renderSortIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSortIcon) - `renderValue` - See [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) - `render` - render function for the column(s) this column type will be applied to. See [`column.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#column.render) - `shouldAcceptEdit` - See [`columns.shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) - `sortable` - See [`columns.sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortable) - `style` - See [`columns.style`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style) - `valueFormatter` - See [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) - `valueGetter` - See [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) - `verticalAlign` - See [`columns.verticalAlign`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.verticalAlign) When any of the properties defined in a column type are also defined in a column (or in column sizing/pinning,etc), the later take precedence so the properties in column type are not applied. The only exception to this rule is the [components](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components) property, which is merged from column types into the column. **Example: Using MUI X Date Picker with custom 'date' type columns** This is a basic example integrating with the [MUI X Date Picker](https://mui.com/x/react-date-pickers/date-picker/) - click any cell in the **Birth Date** or **Date Hired** columns to show the date picker. This example uses the [column types](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes) to give each date column the same editor and styling. ```ts import { InfiniteTable, DataSource, useInfiniteColumnEditor, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import { StyledEngineProvider } from '@mui/material/styles'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import * as _emotionStyled from '@emotion/styled'; import * as _emotionReact from '@emotion/react'; import * as React from 'react'; type Developer = { birthDate: Date; dateHired: Date; id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; }; const DATE_FORMAT = 'YYYY-MM-DD'; const DateEditor = () => { const { value, confirmEdit, cancelEdit } = useInfiniteColumnEditor(); const day = dayjs(value); return ( { if (day) { confirmEdit(day.toDate()); } }} /> ); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, birthDate: { field: 'birthDate', header: 'Birth Date', // we need to specify the type of the column as "date" type: 'date', }, dateHired: { field: 'dateHired', header: 'Date Hired', // we need to specify the type of the column as "date" type: 'date', }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const columnTypes = { date: { defaultEditable: true, components: { Editor: DateEditor, }, defaultWidth: 200, style: ({ inEdit }: { inEdit: boolean }) => { return inEdit ? { padding: 0 } : {}; }, renderValue: ({ value }: { value: Date }) => { return {dayjs(value).format(DATE_FORMAT)}; }, }, }; export default function LocalUncontrolledSingleSortingExample() { return ( <> primaryKey="id" data={dataSource}> debugId="column-types-date-editor-example" columnTypes={columnTypes} columns={columns} columnDefaultWidth={120} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', birthDate: new Date(1997, 0, 1), dateHired: new Date(2023, 0, 1), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', birthDate: new Date(1993, 3, 10), dateHired: new Date(2022, 5, 10), currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', birthDate: new Date(1997, 10, 30), dateHired: new Date(2021, 8, 29), currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', birthDate: new Date(1990, 5, 20), dateHired: new Date(2021, 8, 20), currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', birthDate: new Date(1990, 3, 20), dateHired: new Date(2023, 11, 12), currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', birthDate: new Date(2002, 3, 20), dateHired: new Date(2022, 2, 22), currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', birthDate: new Date(1992, 11, 12), dateHired: new Date(2022, 1, 12), currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', birthDate: new Date(1990, 9, 5), dateHired: new Date(2022, 1, 5), currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', birthDate: new Date(1990, 9, 15), dateHired: new Date(2022, 10, 1), currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', birthDate: new Date(1990, 4, 18), dateHired: new Date(2023, 3, 18), currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` ### columnTypes.components > See related [`columns.components`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components). ### columnTypes.defaultFlex (`number`) > Specifies a default flex value for the column type. Will be overriden in any column that already specifies a `defaultFlex` property. See related [`columnTypes.defaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultWidth), [`columns.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) and [`columns.defaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth) ### columnTypes.defaultSortable (`boolean`) > Specifies whether columns of this type are sortable. This prop overrides the component-level [`columnDefaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultSortable). This prop is overriden by [`columns.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultSortable) and [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable). ### columnTypes.headerClassName (`string | (args) => string`) > Controls styling for the column header for columns with this column type. Can be a string or a function returning a string. See docs at [`columns.headerClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerClassName). ### columns.align (`'start' | 'center' | 'end'`) > Controls the alignment of text in column cells and also the alignment of the column header. To only apply alignment to the column header, use [`columns.headerAlign`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerAlign). For vertical alignment, see [`columns.verticalAlign`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.verticalAlign). For css ellipsis, see [`columns.cssEllipsis`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.cssEllipsis). **Example: Column align example** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; export default function App() { const [align, setAlign] = React.useState<'start' | 'center' | 'end'>('start'); const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', align, }, }; return ( <>

Select the column align

data={dataSource} primaryKey="id"> debugId="column-align-example" columns={columns} columnDefaultWidth={250} headerOptions={{ alwaysReserveSpaceForSortIcon: false, }} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### columns.verticalAlign (`'start' | 'center' | 'end'`) > Controls the vertical alignment of text in column cells. For horizontal alignment, see [`columns.align`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.align). **Example: Column vertical align example** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; export default function App() { const [verticalAlign, setVerticalAlign] = React.useState< 'start' | 'center' | 'end' >('center'); const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', verticalAlign, }, }; return ( <>

Select the vertical align

data={dataSource} primaryKey="id"> debugId="column-vertical-align-example" columns={columns} columnDefaultWidth={250} rowHeight={80} headerOptions={{ alwaysReserveSpaceForSortIcon: false, }} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### columns.headerAlign (`'start' | 'center' | 'end'`) > Controls the alignment of the column header. See related [`columns.align`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.align) and [`columns.headerCssEllipsis`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerCssEllipsis). **Example: Column header align example** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; export default function App() { const [headerAlign, setHeaderAlign] = React.useState< 'start' | 'center' | 'end' >('start'); const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', headerAlign, }, }; return ( <>

Select the header align

data={dataSource} primaryKey="id"> debugId="column-header-align-example" columns={columns} columnDefaultWidth={250} headerOptions={{ alwaysReserveSpaceForSortIcon: false, }} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### columnTypes.headerStyle (`CSSProperties | (args) => CSSProperties`) > Controls styling for the column header for columns with this column type. Can be a style object or a function returning a style object. See docs at [`columns.headerStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.headerStyle). ### columnTypes.defaultWidth (`number`) > Specifies a default fixed width for the column type. Will be overriden in any column that already specifies a `defaultWidth` property. See related [`columnTypes.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultFlex), [`columns.defaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth) and [`columns.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) ### columnTypes.maxWidth (`number`) > Specifies a default maximum width for the column type. Will be overriden in any column that already specifies a `maxWidth` property. See related [`columnTypes.maxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.maxWidth) and [`columns.maxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.maxWidth) ### columnTypes.minWidth (`number`) > Specifies a default minimum width for the column type. Will be overriden in any column that already specifies a `minWidth` property. See related [`columnTypes.maxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.maxWidth) and [`columns.minWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.minWidth) ### defaultActiveCellIndex (`[number,number]`) > Specifies the active cell for keyboard navigation. This is an uncontrolled prop. See the [Keyboard Navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells.md) page for more details. See [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex) for the controlled version of this prop and [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) for the keyboard navigation behavior. **Example: Uncontrolled keyboard navigation for cells** This example starts with cell `[2,0]` already active. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForCells() { return ( <> primaryKey="id" data={dataSource}> debugId="navigating-cells-uncontrolled-example" defaultActiveCellIndex={[2, 0]} columns={columns} /> ); } ``` ### columnsTypes.sortable (`boolean`) > Specifies the sorting behavior for columns of this type. Overrides the global [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) prop, but is overriden by the column's own [sortable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortable) property. ### defaultActiveRowIndex (`number`) > Specifies the active row for keyboard navigation. This is an uncontrolled prop. See the [Keyboard Navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-rows.md) page for more details. See [`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex) for the controlled version of this prop and [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) for the keyboard navigation behavior. **Example: Uncontrolled keyboard navigation for rows** This example starts with row at index `2` already active. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const domProps = { style: { height: '90vh' } }; export default function KeyboardNavigationForRows() { return ( <> primaryKey="id" data={dataSource}> debugId="navigating-rows-uncontrolled-example" domProps={domProps} columns={columns} keyboardNavigation="row" defaultActiveRowIndex={2} /> ); } ``` ### defaultColumnOrder (`string[]|true`) > Defines the order in which columns are displayed in the component. For controlled usage, see [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder). When using this uncontrolled prop, you can also listen to [`onColumnOrderChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnOrderChange) to be notified of column order changes The `defaultColumnOrder` array can contain identifiers that are not yet defined in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) Map or can contain duplicate ids. This is a feature, not a bug. We want to allow you to use the `defaultColumnOrder` in a flexible way so it can define the order of current and future columns. Displaying the same column twice is a perfectly valid use case. See [Column Order](https://infinite-table.com/docs/learn/columns/column-order.md) for more details on ordering columns both programatically and via drag & drop. **Example: Uncontrolled column order** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; export const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', columnGroup: 'location', }, city: { field: 'city', header: 'City', columnGroup: 'address', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, department: { field: 'department', header: 'Department', }, team: { field: 'team', header: 'Team', }, company: { field: 'companyName', header: 'Company' }, companySize: { field: 'companySize', header: 'Company Size', }, }; const columnOrder = [ 'firstName', 'notfound', 'country', 'team', 'company', 'firstName', 'not existing', 'companySize', 'country', ]; export default function App() { return ( data={dataSource} primaryKey="id"> debugId="defaultColumnOrder-example" columns={columns} defaultColumnOrder={columnOrder} columnDefaultWidth={200} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; ``` ### defaultColumnSizing (`Record`) > Defines a default sizing of columns in the grid. This is an uncontrolled property. For the controlled version and more details, see [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing). It is an object that maps column ids to column sizing options. The values in the objects can contain the following properties: - [flex](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing.flex) - use this for flexible columns. Behaves like the `flex` CSS property. - [width](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing.width) - use this for fixed sized columns - [minWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing.minWidth) - specifies the minimum width of the column. Useful for flexible columns or for restricting users resizing both fixed and flexible columns. - [maxWidth](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnSizing.maxWidth) - specifies the maximum width of the column. Useful for flexible columns or for restricting users resizing both fixed and flexible columns. **Example: Uncontrolled column sizing** ```tsx import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; const defaultColumnSizing: InfiniteTablePropColumnSizing = { country: { width: 100 }, city: { flex: 1, maxWidth: 300 }, salary: { flex: 2 }, }; export default function App() { return ( data={dataSource} primaryKey="id"> debugId="defaultColumnSizing-example" columns={columns} columnDefaultWidth={50} defaultColumnSizing={defaultColumnSizing} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` For auto-sizing columns, see [`autoSizeColumnsKey`](https://infinite-table.com/docs/reference/infinite-table-props.md#autoSizeColumnsKey). ### defaultColumnSizing.flex (`number`) > Specifies the flex value for the column. See [`columnSizing.flex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.flex) for details. ### defaultColumnSizing.minWidth (`number`) > Specifies the minimum width for a column. Especially useful for flexible columns. See [`columnSizing.minWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.minWidth) for details. ### defaultColumnSizing.maxWidth (`number`) > Specifies the maximum width for a column. Especially useful for flexible columns. See [`columnSizing.maxWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.maxWidth) for details. ### defaultColumnSizing.width (`number`) > Specifies the fixed width for the column. See [`columnSizing.width`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing.width) for details. ### domProps (`React.HTMLProps`) > DOM properties to be applied to the component root element. For applying a className when the component is focused, see [`focusedClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#focusedClassName) For applying a className when the focus is within the component, see [`focusedWithinClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#focusedWithinClassName) **Example** ```ts files=["domprops-example.page.tsx","data.ts"] ``` ### editable (`(param) => boolean | Promise`) > Controls whether columns are editable or not. This overrides both the global [`columnDefaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultEditable) prop and the column's own [defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) property. This function prop will be called when an edit is triggered on the column. The function will be called with a single object that contains the following properties: - `value` - the current value of the cell (the value currently displayed, so after [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) have been applied) - `rawValue` - the current value of the cell, but before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `data` - the data object (of type `DATA_TYPE`) for the current row - `rowInfo` - the row info object that underlies the row - `column` - the current column on which editing is invoked - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSourceApi` - - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) The function can return a `boolean` value or a `Promise` that resolves to a `boolean` - this means you can asynchronously decide whether the cell is editable or not. By default, double-clicking an editable cell will show the cell editor. You can prevent this by returning `{preventEdit: true}` from the [onCellDoubleClick](https://infinite-table.com/docs/reference/infinite-table-props.md#onCellDoubleClick) function prop. ### focusedClassName (`string`) > CSS class name to be applied to the component root element when it has focus. For applying a className when the focus is within the component, see [`focusedWithinClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#focusedWithinClassName) For focus style, see [`focusedStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#focusedStyle). ### focusedWithinClassName (`string`) > CSS class name to be applied to the component root element when there is focus within (inside) the component. For applying a className when the component root element is focused, see [`focusedClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#focusedClassName) ### focusedStyle > Specifies the `style` to be applied to the component root element when it has focus. **Example: focusedStyle example** ```ts files=["focusedStyle-example.page.tsx","data.ts"] ``` ### focusedWithinStyle > Specifies the `style` to be applied to the component root element when there is focus within (inside) the component. To listen to focusWithin changes, listen to [`onFocusWithin`](https://infinite-table.com/docs/reference/infinite-table-props.md#onFocusWithin) and [`onBlurWithin`](https://infinite-table.com/docs/reference/infinite-table-props.md#onBlurWithin). **Example: focusedWithinStyle example - focus an input inside the table to see it in action** ```ts files=["focusedWithinStyle-example.page.tsx","data.ts"] ``` ### getCellContextMenuItems (`({data, column, rowInfo}) => MenuItem[] | null | { items: MenuItem[], columns: [{name}] } | Promise`) > Customises the context menu items for a cell. If you want to customize the context menu even when the user clicks outside any cell, but inside the table body, use [`getContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getContextMenuItems). The `getCellContextMenuItems` function can return one of the following: - `null` - no custom context menu will be displayed, the default context menu will be shown (default event behavior not prevented) - `[]` - an empty array - no custom context menu will be displayed, but the default context menu is not shown - the default event behavior is prevented - `Array` - an array of menu items to be displayed in the context menu - each `MenuItem` should have: - a unique `key` property, - a `label` property with the value to display in the menu cell - it's called `label` because this is the name of the default column in the context menu - an optional `onAction({ key, item, hideMenu: () => void })` callback function to handle the click action on the menu item. - an optional `onClick(event)` callback function to handle the click event on the menu item. - an optional `hideMenuOnAction: boolean` - if `true`, it will close the context menu when the menu item is clicked **Example: Using context menus** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { stack: { field: 'stack', header: 'Stack', }, firstName: { field: 'firstName', header: 'Name', }, age: { field: 'age', header: 'Age', }, hobby: { field: 'hobby', header: 'Hobby', }, preferredLanguage: { header: 'Language', field: 'preferredLanguage', }, }; export default function App() { return ( <> primaryKey="id" data={dataSource}> debugId="cell-basic-context-menu-example" columns={columns} getCellContextMenuItems={({ data, column }) => { return [ { key: 'hello', label: `Hello, ${data?.lastName} ${data?.firstName}`, onClick: () => { alert(`Hello, ${data?.lastName} ${data?.firstName}`); }, }, { key: 'col', label: `Current clicked column: ${column.header}`, }, { key: 'learn', label: `Learn`, menu: { items: [ { key: 'backend', label: 'Backend', onClick: () => { alert( `Learn Backend, ${data?.lastName} ${data?.firstName}`, ); }, }, { key: 'frontend', label: 'Frontend', onClick: () => { alert( `Learn Frontend, ${data?.lastName} ${data?.firstName}`, ); }, }, ], }, }, ]; }} /> ); } ``` This function can also return a `Promise` that resolves to one of the above types. This is useful for lazy loading the context menu items. When returning a `Promise`, the context menu will be shown after the promise resolves, and the default browser context menu is not shown. In addition, if you need to configure the context menu to have other columns rather than the default column (named `label`), you can do so by returning an object with `columns` and `items`: ```tsx const getCellContextMenuItems = () => { return { columns: [{ name: 'label' }, { name: 'icon' }], items: [ { label: 'Welcome', icon: '👋', key: 'hi', }, { label: 'Convert', icon: '🔁', key: 'convert', }, ], }; }; ``` **Example: Customising columns in the context menu** Right-click any cell in the table to see a context menu with multiple columns (`icon`, `label` and `description`). ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { stack: { field: 'stack', header: 'Stack', }, firstName: { field: 'firstName', header: 'Name', }, age: { field: 'age', header: 'Age', }, hobby: { field: 'hobby', header: 'Hobby', }, preferredLanguage: { header: 'Language', field: 'preferredLanguage', }, }; export default function App() { return ( <> primaryKey="id" data={dataSource}> debugId="cells-with-custom-columns-context-menu-example" columns={columns} columnDefaultEditable getCellContextMenuItems={({ data, column }) => { const columns = [ { name: 'icon' }, { name: 'label' }, { name: 'description' }, ]; return { columns, items: [ { key: 'hello', icon: '👋', label: `Hello, ${data?.lastName} ${data?.firstName}`, description: `This is a description for ${data?.lastName}`, }, { key: 'col', icon: '🙌', label: `Column: ${column.header}`, description: `Current clicked column: ${column.header}`, }, { key: 'learn', icon: '📚', label: `Learn`, description: `Learn more about ${data?.preferredLanguage}`, menu: { columns, items: [ { key: 'backend', label: 'Backend', icon: '👨‍💻', description: 'In the Backend', }, { key: 'frontend', label: 'Frontend', icon: '👨‍💻', description: 'In the Frontend', }, ], }, }, ], }; }} /> ); } ``` ### getContextMenuItems (`({event, data?, column?, rowInfo}, {api, dataSourceApi}) => MenuItem[] | null | { items: MenuItem[], columns: [{name}] } | Promise`) > Customises the context menu items for the whole table. If you want to customize the context menu only when the user clicks inside a cell, use [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems), which is probably what you're looking for. The first argument this function is called with has the same shape as the one for [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) but all cell-related properties could also be `undefined`. Also, the `event` is available as a property on this object. If this function returns null, the default context menu of the browser will be shown (default event behavior not prevented). **Example: Using context menus for the whole table** ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'Name', defaultWidth: 120, }, age: { field: 'age', header: 'Age', defaultWidth: 100, }, preferredLanguage: { header: 'Language', defaultWidth: 120, field: 'preferredLanguage', }, }; export default function App() { return ( <> primaryKey="id" data={dataSource}> debugId="table-basic-context-menu-example" columns={columns} getContextMenuItems={({ data, column }) => { if (!data) return [ { key: 'add', label: 'Add Item', onClick: () => { alert('Add Item'); }, }, ]; return [ { key: 'hello', label: `Hello, ${data?.lastName} ${data?.firstName}`, onClick: () => { alert(`Hello, ${data?.lastName} ${data?.firstName}`); }, }, { key: 'col', label: `Current clicked column: ${column?.header}`, }, { key: 'learn', label: `Learn`, menu: { items: [ { key: 'backend', label: 'Backend', onClick: () => { alert( `Learn Backend, ${data?.lastName} ${data?.firstName}`, ); }, }, { key: 'frontend', label: 'Frontend', onClick: () => { alert( `Learn Frontend, ${data?.lastName} ${data?.firstName}`, ); }, }, ], }, }, ]; }} /> ); } ``` This function can also return a `Promise` that resolves to one of the above types. This is useful for lazy loading the context menu items. When returning a `Promise`, the context menu will be shown after the promise resolves, and the default browser context menu is not shown. ### getColumnMenuItems (`(items, context) => MenuItem[]`) > Allows customization of the context menu items for a column. Use this function to customize the context menu for columns. The function is called with the following arguments: - `items` - the default menu items for the column - you can return this array as is to use the default menu items (same as not providing this function prop) or you can customize the array or return a new one altogether. - `context` - an object that gives you access to the column and the grid state - `context.column: InfiniteTableComputedColumn` - the current column for which the context menu is being shown - `context.api` - a reference to the [api](./reference/api) **Example: getColumnMenuItems example - custom menu item and icon** In this example, the currency and preferredLanguage columns have a custom icon for triggering the column context menu. In addition, the `preferredLanguage` column has a custom header that shows a button for triggering the column context menu. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { currency: { field: 'currency', // custom menu icon renderMenuIcon: () =>
🌎
, }, preferredLanguage: { field: 'preferredLanguage', defaultWidth: 350, header: ({ columnApi, renderLocation }) => { // if we're inside the column menu with all columns, return only the col name if (renderLocation === 'column-menu') { return 'Preferred Language'; } // but for the real column header // return this custom content return ( <> Preferred Language{' '} ); }, // custom menu icon renderMenuIcon: () =>
🌎
, }, salary: { field: 'salary', // hide the menu icon renderMenuIcon: false, }, country: { field: 'country', }, id: { field: 'id', defaultWidth: 80, renderMenuIcon: false }, firstName: { field: 'firstName', }, }; export default function ColumnContextMenuItems() { return ( <> primaryKey="id" data={dataSource}> debugId="getColumnMenuItems-example" columnHeaderHeight={70} columns={columns} getColumnMenuItems={(items, { column }) => { if (column.id === 'firstName') { // you can adjust the default items for a specific column items.splice(0, 0, { key: 'firstName', label: 'First name menu item', onAction: () => { console.log('Hey there!'); }, }); } items.push( { key: 'hello', label: 'Hello World', onAction: () => { alert('Hello World from column ' + column.id); }, }, { key: 'translate', label: 'Translate', menu: { items: [ { key: 'translateToEnglish', label: 'English', onAction: () => { console.log('Translate to English'); }, }, { key: 'translateToFrench', label: 'French', onAction: () => { console.log('Translate to French'); }, }, ], }, }, ); return items; }} /> ); } ``` ### groupColumn (`InfiniteTableColumn|(colInfo, toggleGroupRow) => InfiniteTableColumn`) > Allows you to define a custom configuration for one or multiple group columns. When this prop is defined, it gets merged onto any values specified in the [`groupBy.column`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy.column) property. If this is an object and no explicit [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is specified, the component is rendered as if you had [groupRenderStrategy="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy). If it's a function, it will be called with the following arguments: - `colInfo` - an object with the following properties: - `colInfo.groupCount` - the count of row groups - `colInfo.groupBy` - the array of row groups, used by the `DataSource` to do the grouping - `colInfo.groupRenderStrategy` - the current [render strategy for groups](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy). - `colInfo.groupByForColumn` - the grouping object (one of the items in `colInfo.groupBy`) corresponding to the current column. Only defined when `groupRenderStrategy` is `multi-column`. - `colInfo.groupIndexForColumn` - the index of `colInfo.groupByForColumn` in `colInfo.groupBy` - corresponding to the current column. Only defined when `groupRenderStrategy` is `multi-column`. - `toggleGroupRow(groupKeys: any[])` - a function you can use to toggle a group row. Pass an array of keys - the path to the group row you want to toggle. You can still use [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) as a function with single column group render strategy, but in this case, you have to be explicit and specify [groupRenderStrategy="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy). **Example: groupColumn used as an object** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTableColumn, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', style: { color: 'orange', }, renderValue: ({ value, rowInfo }) => rowInfo.isGroupRow ? null : `${value}.`, }, stack: { field: 'stack', style: { color: 'tomato', }, }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; const groupColumn: InfiniteTableColumn = { field: 'firstName', renderValue: ({ value }) => { return `First name: ${value}`; }, }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="group-column-custom-renderers-example" groupColumn={groupColumn} columns={columns} columnDefaultWidth={250} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` **Example: groupColumn used as a function** This example shows how to use [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) as a function that allows you to customize all generated group columns in a single place. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTableColumn, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', style: { color: 'orange', }, renderValue: ({ value, rowInfo }) => rowInfo.isGroupRow ? null : `${value}.`, }, stack: { field: 'stack', style: { color: 'tomato', }, }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; const groupColumn: InfiniteTableColumn = { field: 'firstName', renderValue: ({ value }) => { return `First name: ${value}`; }, }; export default function App() { return ( data={dataSource} primaryKey="id" groupBy={groupBy}> debugId="group-column-custom-renderers-example" groupColumn={groupColumn} columns={columns} columnDefaultWidth={250} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### groupRenderStrategy (`'single-column'|'multi-column'`) > Determines how grouping is rendered - whether a single or multiple columns are generated. **Example** ```ts files=["groupRenderStrategy-example.page.tsx","employee-columns.ts"] ``` ### hideColumnWhenGrouped (`boolean`) > Allows you to hide group columns bound to fields that are grouped by (fields mentioned in [groupBy.field](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy)). **Example** In this example, toggle the checkbox to see the `stack` and `preferredLanguage` columns hide/show as the value of `hideColumnWhenGrouped` changes. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage', }, stack: { field: 'stack', style: { color: 'tomato', }, }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; const groupColumn = { field: 'firstName' as keyof Developer, }; const domProps = { style: { flex: 1 }, }; export default function App() { const [hideColumnWhenGrouped, setHidden] = useState(true); return (
data={dataSource} primaryKey="id" groupBy={groupBy} > debugId="hideColumnWhenGrouped-example" groupColumn={groupColumn} columns={columns} hideColumnWhenGrouped={hideColumnWhenGrouped} columnDefaultWidth={250} domProps={domProps} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### hideEmptyGroupColumns (`boolean`) > Allows you to hide group columns which don't render any information (this happens when all previous groups are collapsed). **Example** ```ts files=["hideEmptyGroupColumns-example.page.tsx","employee-columns.ts"] ``` ### keyboardNavigation (`'cell'|'row'|false`) > Determines whether keyboard navigation is enabled. Available values: - `'cell'` - enables keyboard navigation for cells. This is the default. - `'row'` - enables keyboard navigation for rows. - `false` - disables keyboard navigation. For cell keyboard navigation, see [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex). For row keyboard navigation, see [`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex). **Example: Keyboard navigation** This example starts with cell `[2,0]` already active. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForCells() { return ( <> primaryKey="id" data={dataSource}> debugId="navigating-cells-uncontrolled-example" defaultActiveCellIndex={[2, 0]} columns={columns} /> ); } ``` **Example: Disabled Keyboard navigation** In this example the keyboard navigation is disabled. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForRows() { return ( <> primaryKey="id" data={dataSource}> debugId="navigation-disabled-example" keyboardNavigation={false} columns={columns} /> ); } ``` ### keyboardSelection (`boolean`) > Determines whether the keyboard can be used for selecting/deselecting rows/cells. By default [`keyboardSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardSelection) is enabled, so you can use the keyboard **spacebar** key to select multiple rows. Using the spacebar key is equivalent to doing a mouse click, so expect the combination of **spacebar** + `cmd`/`ctrl`/`shift` modifier keys to behave just like clicking + the same modifier keys. For specifying the selection mode, use [`selectionMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode) **Example: Toggling keyboard navigation** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, age: { field: 'age' }, id: { field: 'id' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, }; export default function App() { const [keyboardSelection, setKeyboardSelection] = useState(true); return ( <>
Keyboard selection is now{' '} {keyboardSelection ? 'enabled' : 'disabled'}.
data={dataSource} selectionMode="multi-row" primaryKey="id" > debugId="default-selection-mode-multi-row-keyboard-toggle-example" keyboardSelection={keyboardSelection} columns={columns} columnDefaultWidth={150} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### loadingText (`ReactNode`) > The text inside the load mask - displayed when [loading=true](https://infinite-table.com/docs/reference/datasource-props/index.md#loading). **Example: Customized loading text** ```ts import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; export default function App() { return ( loading data={employees} primaryKey="id"> debugId="loadingText-example" loadingText="Please wait ..." columnDefaultWidth={130} columns={columns} /> ); } type Employee = { id: string | number; name: string; salary: number; department: string; company: string; }; const employees: Employee[] = [ { id: 1, name: 'Bob', salary: 10_000, department: 'IT', company: 'Bobsons', }, { id: 2, name: 'Alice', salary: 20_000, department: 'IT', company: 'Bobsons', }, ]; const columns: Record> = { id: { field: 'id', type: 'number', defaultWidth: 80, }, name: { field: 'name', }, salary: { field: 'salary', type: 'number' }, department: { field: 'department', header: 'Dep.' }, company: { field: 'company' }, }; ``` ### multiSortBehavior (`'append'|'replace'`) > Specifies the behavior of the DataGrid when [multiple sorting](https://infinite-table.com/docs/learn/sorting/multiple-sorting.md) is configured. Defaults to `'replace'`. When `InfiniteTable` is configured with multiple sorting there are two supported behaviors: - `append` - when this behavior is used, clicking a column header adds that column to the alredy existing sort. If the column is already sorted, the sort direction is reversed. In order to remove a column from the sort, the user needs to click the column header in order to toggle sorting from ascending to descending and then to no sorting. - `replace` - the default behavior - a user clicking a column header removes any existing sorting and sets that column as sorted. In order to add a new column to the sort, the user needs to hold the `Ctrl/Cmd` key while clicking the column header. **Example** Try clicking the `age` column and then the `firstName` column. If the multi-sort behavior is `replace`, clicking the second column will remove the sort from the first column. In order for the sorting to be additive, even if the behavior is `replace`, use the `Ctrl`/`Cmd` key while clicking the column header. If the multi-sort behavior is `append`, clicking the second column will add it to the sort. ```ts import { InfiniteTable, DataSource, InfiniteTablePropMultiSortBehavior, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name' }, age: { field: 'age', header: 'Age' }, salary: { field: 'salary', header: 'Salary', type: 'number', }, country: { field: 'country', header: 'Country' }, preferredLanguage: { field: 'preferredLanguage' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function LocalUncontrolledSingleSortingExample() { const [multiSortBehavior, setMultiSortBehavior] = React.useState< 'append' | 'replace' >('replace'); return ( <>

Select the multi-sort behavior

primaryKey="id" data={dataSource} defaultSortInfo={[]} > debugId="local-multi-sorting-example-defaults-with-local-data" columns={columns} columnDefaultWidth={120} multiSortBehavior={multiSortBehavior} /> ); } const dataSource: Developer[] = [ { id: 0, firstName: 'Nya', country: 'India', city: 'Unnao', age: 24, currency: 'JPY', preferredLanguage: 'TypeScript', salary: 60000, hobby: 'sports', email: 'Nya44@gmail.com', }, { id: 1, firstName: 'Axel', country: 'Mexico', city: 'Cuitlahuac', age: 46, currency: 'USD', preferredLanguage: 'TypeScript', salary: 100000, hobby: 'sports', email: 'Axel93@hotmail.com', }, { id: 2, firstName: 'Gonzalo', country: 'United Arab Emirates', city: 'Fujairah', age: 24, currency: 'JPY', preferredLanguage: 'Go', salary: 120000, hobby: 'photography', email: 'Gonzalo_McGlynn34@gmail.com', }, { id: 3, firstName: 'Sherwood', country: 'Mexico', city: 'Tlacolula de Matamoros', age: 24, currency: 'CHF', preferredLanguage: 'Rust', salary: 99000, hobby: 'cooking', email: 'Sherwood_McLaughlin65@hotmail.com', }, { id: 4, firstName: 'Alexandre', country: 'France', city: 'Persan', age: 24, currency: 'EUR', preferredLanguage: 'Go', salary: 97000, hobby: 'reading', email: 'Alexandre_Harber@hotmail.com', }, { id: 5, firstName: 'Mariane', country: 'United States', city: 'Hays', age: 23, currency: 'EUR', preferredLanguage: 'TypeScript', salary: 58000, hobby: 'cooking', email: 'Mariane0@hotmail.com', }, { id: 6, firstName: 'Rosalind', country: 'Mexico', city: 'Nuevo Casas Grandes', age: 23, currency: 'AUD', preferredLanguage: 'JavaScript', salary: 198000, hobby: 'dancing', email: 'Rosalind69@gmail.com', }, { id: 7, firstName: 'Lolita', country: 'Sweden', city: 'Delsbo', age: 22, currency: 'JPY', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'cooking', email: 'Lolita.Hayes@hotmail.com', }, { id: 8, firstName: 'Tre', country: 'Germany', city: 'Bad Camberg', age: 23, currency: 'GBP', preferredLanguage: 'TypeScript', salary: 200000, hobby: 'sports', email: 'Tre28@gmail.com', }, { id: 9, firstName: 'Lurline', country: 'Canada', city: 'Raymore', age: 23, currency: 'EUR', preferredLanguage: 'Rust', salary: 58000, hobby: 'sports', email: 'Lurline_Deckow@gmail.com', }, ]; ``` ### onActiveCellIndexChange (`(activeCellIndex:[number,number])=>void`) > Callback triggered by cell navigation. See the [Keyboard Navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells.md) page for more details. See related [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex) and [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) for the keyboard navigation behavior. **Example: Controlled keyboard navigation (for cells) with callback** This example uses `onActiveCellIndexChange` to react to changes in the `activeCellIndex`. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForCells() { const [activeCellIndex, setActiveCellIndex] = React.useState< [number, number] >([2, 0]); return ( <>
Current active cell: {activeCellIndex[0]}, {activeCellIndex[1]}.
primaryKey="id" data={dataSource}> debugId="navigating-cells-controlled-example" activeCellIndex={activeCellIndex} onActiveCellIndexChange={setActiveCellIndex} columns={columns} /> ); } ``` ### onActiveRowIndexChange (`(activeRowIndex:number)=>void`) > Callback triggered by row navigation. See the [Keyboard Navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-rows.md) page for more details. See related [`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex) and [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) for the keyboard navigation behavior. **Example: Controlled keyboard navigation (for rows) with callback** This example uses `onActiveRowIndexChange` to react to changes in the `activeRowIndex`. ```ts import { InfiniteTable, DataSource, DataSourceData, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function KeyboardNavigationForRows() { const [activeRowIndex, setActiveRowIndex] = React.useState(2); return ( <>
Current active row: {activeRowIndex}.
primaryKey="id" data={dataSource}> debugId="navigating-rows-controlled-example" keyboardNavigation="row" activeRowIndex={activeRowIndex} onActiveRowIndexChange={setActiveRowIndex} columns={columns} /> ); } ``` ### onBlurWithin (`(event)=> void`) > Function that is called when a focused element is blurred within the component. For the corresponding focus event, see [`onFocusWithin`](https://infinite-table.com/docs/reference/infinite-table-props.md#onFocusWithin) This callback is fired when a focusable element inside the component is blurred, and the focus is no longer within the component. In other words, when you navigate focusable elements inside the table, this callback is not fired. **Example: Blur an input inside the table to see the callback fired** ```ts files=["onBlurWithin-example.page.tsx","data.ts"] ``` ### onCellDoubleClick (`({ colIndex, rowIndex, column, columnApi, api, dataSourceApi }, event) => void | {preventEdit?: boolean} `) > Callback function called when a cell has been double clicked. If the cell is editable, you can prevent going into edit mode by returning `{preventEdit: true}` from the function. ### onCellClick (`({ colIndex, rowIndex, column, columnApi, api, dataSourceApi }, event) => void`) > Callback function called when a cell has been clicked. The first argument of the function is an object that contains the following properties: - `rowIndex: number` - the index of the row that was clicked. - `colIndex: number` - the index of the column that was clicked. This index is the index in the array of visible columns. - `column: InfiniteTableComputedColumn` - the column that has been clicked - `columnApi: InfiniteTableColumnApi` - the [column API](https://infinite-table.com/docs/reference/column-api/index.md) - `api: InfiniteTableApi` - a reference to the [API](docs/reference/api) - `dataSourceApi: DataSourceApi` - a reference to the [Data Source API](https://infinite-table.com/docs/reference/datasource-api/index.md). Can be used to get the current data. The second argument is the original browser click event. ### onColumnOrderChange (`(columnOrder: string[])=>void`) > Called as a result of user changing the column order ### onColumnSizingChange (`(columnSizing)=>void`) > Called as a result of user doing a column resize. Use this callback to get updated information after a column resize is performed. This works well in combination with the controlled [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) prop (though you don't have to use controlled [`columnSizing`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnSizing) in order to use this callback). For more info on resizing columns, see [Column Sizing](https://infinite-table.com/docs/learn/columns/fixed-and-flexible-size.md). See related [`onViewportReservedWidthChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onViewportReservedWidthChange) **Example: Controlled column sizing example with onColumnSizingChange** ```ts import { InfiniteTable, DataSource, InfiniteTableColumnGroup, InfiniteTablePropColumnSizing, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { currency: { field: 'currency', columnGroup: 'finance', }, salary: { field: 'salary', columnGroup: 'finance', }, country: { field: 'country', columnGroup: 'regionalInfo', maxWidth: 400, }, preferredLanguage: { field: 'preferredLanguage', columnGroup: 'regionalInfo', }, id: { field: 'id' }, firstName: { field: 'firstName', }, stack: { field: 'stack', }, }; const columnGrous: Record = { regionalInfo: { header: 'Regional Info', }, finance: { header: 'Finance', columnGroup: 'regionalInfo', }, }; export default function ColumnValueGetterExample() { const [columnSizing, setColumnSizing] = useState({ salary: { maxWidth: 130, width: 80, }, currency: { maxWidth: 130, width: 80, }, }); return ( <>

Current column sizing:{' '}

{JSON.stringify(columnSizing, null, 2)}

primaryKey="id" data={dataSource}> debugId="onColumnSizingChange-example" columnSizing={columnSizing} onColumnSizingChange={setColumnSizing} columnGroups={columnGrous} columns={columns} columnDefaultWidth={100} /> ); } ``` ### onEditAccepted (`({value, initialValue, column, rowInfo, ...}) => void`) > Callback prop called when an edit is accepted In order to decide whether an edit should be accepted or not, you can use the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) prop or the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) alternative. When neither the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) nor the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) are defined, all edits are accepted by default. This callback is called with a single object that has the following properties: - `value` - the value that was accepted for the edit operation. - `initialValue` - the initial value of the cell (the value before editing started) - `rowInfo` - of type [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) - the row info object that underlies the row - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSouceApi` - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) - `column` - the column on which the edit was performed - `columnApi` - a reference to the [column API](https://infinite-table.com/docs/reference/column-api/index.md) See related [`onEditRejected`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditRejected) callback prop. ### onEditPersistSuccess (`({value, initialValue, column, rowInfo, ...})=>void`) > Callback prop called when an edit is persisted successfully Has the same signature as [`onEditAccepted`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditAccepted) ### onEditRejected (`({ value, initialValue, column, rowInfo, ... }) => void`) > Callback prop called when an edit is rejected In order to decide whether an edit should be accepted or rejected, you can use the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) prop or the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) alternative. When neither the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) nor the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) are defined, all edits are accepted by default. This callback prop has almost the same signature as the [`onEditAccepted`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditAccepted) callback prop. The only difference is that the argument passed to the function also contains an `error` property, with a reference to the error that caused the edit to be rejected. ### onFocusWithin (`(event)=> void`) > Function that is called when the table receives focus within the component. For the corresponding blur event, see [`onBlurWithin`](https://infinite-table.com/docs/reference/infinite-table-props.md#onBlurWithin) **Example: Focus an input inside the table to see the callback fired** ```ts files=["onFocusWithin-example.page.tsx","data.ts"] ``` ### onKeyDown (`({ api, dataSourceApi }, event) => void | InfiniteTablePropOnKeyDownResult`) > Callback function called when the `keydown` event occurs on the table. The first argument of the function is an object that contains the following properties: - `api: InfiniteTableApi` - a reference to the [API](docs/reference/api) - `dataSourceApi: DataSourceApi` - a reference to the [Data Source API](https://infinite-table.com/docs/reference/datasource-api/index.md). Can be used to get the current data. The second argument is the original browser `keydown` event. If you want to prevent some default behaviours, you can return an object with the following properties: - `preventEdit: boolean` - if true, the cell editor will not be shown when hitting the `Enter` key in an editable cell. - `preventEditStop: boolean` - if true, hitting the `Escape` key will not stop the edit. - `preventSelection: boolean` - if true, the ` ` and `Cmd+a` keys will not select cells/rows - `preventNavigation: boolean` - if true, keyboard navigation will be prevented when using `arrow` keys, `page up/down`, `home/end`, `enter`. For keyboard shortcuts, see [`keyboardShortcuts`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardShortcuts). ### onReady (`({api, dataSourceApi}) => void}`) > Callback prop that is being called when the table is ready. This is called only once with an object that has an `api` property, which is an instance of [`InfiniteTableApi`](https://infinite-table.com/docs/reference/api/index.md) and a `dataSourceApi` property, which is an instance of [`DataSourceApi`](https://infinite-table.com/docs/reference/datasource-api/index.md). The `ready` state for the table means it has been layout out and has measured its available size for laying out the columns. It will never be called again after the component is ready. ### onRenderRangeChange (`(range)=>void`) > Called whenever the render range changes, that is, additional rows or columns come into view. The first (and only) argument is an object with `{start, end}` where both `start` and `end` are arrays of `[rowIndex, colIndex]` pairs. So if you want to get the start and end indexes, you can do ```ts const [startRow, startCol] = renderRange.start; const [endRow, endCol] = renderRange.end; ``` This callback is not debounced or throttled, so it can be called multiple times in a short period of time, especially while scrolling. Make sure your function is fast, or attach a debounced function, in order to avoid performance issues. ```tsx import { debounce, InfiniteTable, DataSource } from '@infinite-table/infinite-react'; function App() { const onRenderRangeChange = useMemo(() => { return debounce((range) => { console.log(range.start, range.end); }, {wait: 100}); }, []); return primaryKey="id" data={/*data*/} > onRenderRangeChange={onRenderRangeChange} columns={/*columns*/} /> } ``` Unlike [`onScrollStop`](https://infinite-table.com/docs/reference/infinite-table-props.md#onScrollStop), this function is also called when the DataGrid is resized and also when initially rendered. ### onScrollStop (`({renderRange, viewportSize, scrollTop, scrollLeft})=>void`) > Triggered when the user has stopped scrolling (after [`scrollStopDelay`](https://infinite-table.com/docs/reference/infinite-table-props.md#scrollStopDelay) milliseconds). This is called when the user stops scrolling for a period of time - as configured by [`scrollStopDelay`](https://infinite-table.com/docs/reference/infinite-table-props.md#scrollStopDelay) (milliseconds). The function is called with an object that has the following properties: - `renderRange` - the render range of the viewport. This is an object with `{start, end}` where both `start` and `end` are arrays of `[rowIndex, colIndex]` pairs. So if you want to get the start and end indexes, you can do ```ts const [startRow, startCol] = renderRange.start; const [endRow, endCol] = renderRange.end; ``` - `viewportSize` - the size of the viewport - `{width, height}` - `scrollTop` - the scrollTop position of the viewport - `number` - `scrollLeft` - the scrollLeft position of the viewport - `number` Also see [`onScrollToTop`](https://infinite-table.com/docs/reference/infinite-table-props.md#onScrollToTop), [`onScrollToBottom`](https://infinite-table.com/docs/reference/infinite-table-props.md#onScrollToBottom) and [`onRenderRangeChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRenderRangeChange). **Example: onScrollStop is called with viewport info - scroll the grid and see the console** ```ts import { InfiniteTable, DataSource, InfiniteTableProps, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns, ScrollStopInfo, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; monthlyBonus: number; birthDate: Date; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, birthDate: { field: 'birthDate', type: 'number' }, }; export default function App() { const onScrollStop: InfiniteTableProps['onScrollStop'] = React.useCallback( ({ renderRange, viewportSize, scrollTop, scrollLeft, }: ScrollStopInfo) => { console.log({ renderRange, viewportSize, scrollTop, scrollLeft }); }, [], ); return ( <> primaryKey="id" data={dataSource}> debugId="onScrollStop-example" onScrollStop={onScrollStop} columns={columns} columnDefaultWidth={250} /> ); } ``` ### onScrollToBottom (`()=>void`) > Triggered when the user has scrolled to the bottom of the component. Also see [`onScrollToTop`](https://infinite-table.com/docs/reference/infinite-table-props.md#onScrollToTop) and [`onScrollStop`](https://infinite-table.com/docs/reference/infinite-table-props.md#onScrollStop). Also see [`onScrollToTop`](https://infinite-table.com/docs/reference/infinite-table-props.md#onScrollToTop) and [`onScrollStop`](https://infinite-table.com/docs/reference/infinite-table-props.md#onScrollStop). As an example usage, we're demoing live pagination, done in combination with the [react-query](https://tanstack.com/query/latest) library. If you want to scroll to the top of the table, you can use the [`scrollTopKey`](https://infinite-table.com/docs/reference/infinite-table-props.md#scrollTopKey) prop. **Example: Fetch new data on scroll to bottom** ```ts import '@infinite-table/infinite-react/index.css'; import { InfiniteTable, InfiniteTableColumn, DataSource, DataSourceSingleSortInfo, DataSourceDataParams, DataSourceLivePaginationCursorFn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useCallback } from 'react'; import { QueryClient, QueryClientProvider, useInfiniteQuery, keepPreviousData, } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, }); const emptyArray: Employee[] = []; export const columns: Record> = { id: { field: 'id' }, country: { field: 'country', }, city: { field: 'city' }, team: { field: 'team' }, department: { field: 'department' }, firstName: { field: 'firstName' }, lastName: { field: 'lastName' }, salary: { field: 'salary' }, age: { field: 'age' }, }; type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: number; department: string; team: string; salary: number; age: number; email: string; }; const PAGE_SIZE = 10; const dataSource = ({ sortInfo, livePaginationCursor = 0, }: { sortInfo: DataSourceSingleSortInfo | null; livePaginationCursor: number; }) => { return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${ sortInfo?.dir === 1 ? 'asc' : 'desc' }&_start=${livePaginationCursor}`, ) .then(async (r) => { const data = await r.json(); // we need the remote count, so we take it from headers const total = Number(r.headers.get('X-Total-Count')!); return { data, total }; }) .then(({ data, total }: { data: Employee[]; total: number }) => { const page = livePaginationCursor / PAGE_SIZE + 1; const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0); return { data, hasMore: total > PAGE_SIZE * page, page, prevPageCursor, nextPageCursor: prevPageCursor + data.length, }; }) .then( ( response, ): Promise<{ data: Employee[]; hasMore: boolean; page: number; nextPageCursor: number; prevPageCursor: number; }> => { return new Promise((resolve) => { setTimeout(() => { resolve(response); }, 150); }); }, ); }; const Example = () => { const [dataParams, setDataParams] = React.useState< Partial> >({ groupBy: [], sortInfo: undefined, livePaginationCursor: null, }); const { data, fetchNextPage: fetchNext, isFetchingNextPage, } = useInfiniteQuery({ initialPageParam: 0, queryKey: ['employees', dataParams.sortInfo, dataParams.groupBy], queryFn: ({ pageParam = 0 }) => { const params = { livePaginationCursor: pageParam, sortInfo: dataParams.sortInfo as DataSourceSingleSortInfo | null, }; return dataSource(params); }, placeholderData: keepPreviousData, getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0, getNextPageParam: (lastPage) => { const nextPageCursor = lastPage.hasMore ? lastPage.nextPageCursor : undefined; return nextPageCursor; }, select: (data) => { const flatData = data.pages.flatMap((x) => x.data); const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor; const result = { pages: flatData, pageParams: [nextPageCursor], }; return result; }, }); const onDataParamsChange = useCallback( (dataParams: DataSourceDataParams) => { const params = { groupBy: dataParams.groupBy, sortInfo: dataParams.sortInfo, livePaginationCursor: dataParams.livePaginationCursor, }; setDataParams(params); }, [], ); const [scrollTopId, setScrollTop] = React.useState(0); React.useEffect(() => { // when sorting changes, scroll to the top setScrollTop(Date.now()); }, [dataParams.sortInfo]); const fetchNextPage = () => { if (isFetchingNextPage) { return; } fetchNext(); }; React.useEffect(() => { fetchNextPage(); }, [dataParams.livePaginationCursor]); const livePaginationCursorFn: DataSourceLivePaginationCursorFn = useCallback(({ length }) => { return length; }, []); return ( primaryKey="id" // take the data from `data.pages`, // as returned from our react-query select function data={data?.pages || emptyArray} loading={isFetchingNextPage} onDataParamsChange={onDataParamsChange} livePagination livePaginationCursor={livePaginationCursorFn} > debugId="live-pagination-example" scrollTopKey={scrollTopId} columnDefaultWidth={200} columns={columns} /> ); }; function App() { return ( ); } export default App; ``` ### onViewportReservedWidthChange (`(reserved: number) => void`) > Callback to be notified of changes to [`viewportReservedWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#viewportReservedWidth) See [`viewportReservedWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#viewportReservedWidth) for details. See related [`onColumnSizingChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnSizingChange). When he user is performing a column resize (via drag & drop), [`onViewportReservedWidthChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onViewportReservedWidthChange) is called when the resize is finished (not the case for resizing with the **SHIFT** key pressed, when adjacent columns share the space between them since the reserved width is preserved). **Example: Using onViewportReservedWidth to respond to user column resizing** Resize a column to see `viewportReservedWidth` updated and then click the button to reset it to `0px` ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; const defaultColumnSizing: InfiniteTablePropColumnSizing = { country: { flex: 1 }, city: { flex: 1 }, salary: { flex: 2 }, }; export default function App() { const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current viewport reserved width: {viewportReservedWidth}px.

data={dataSource} primaryKey="id"> debugId="viewportReservedWidth-example" columns={columns} columnDefaultWidth={50} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} defaultColumnSizing={defaultColumnSizing} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### persistEdit (`(params) => any|Error|Promise`) > Custom function to persist an edit This allows edits that have been accepted (see [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit)) to be persisted to a remote (or local) location. This function is called with an object that has the following properties: - `value` - the value that was accepted for the edit operation. - `initialValue` - the initial value of the cell (the value that was displayed before editing started). This is the value resulting after [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) have been applied) - `rawValue` - the initial value of the cell, but before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `data` - the current data object - `rowInfo` - the row info object that underlies the row - `column` - the current column on which editing is invoked - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSourceApi` - - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) This function can be synchronous or asynchronous. For synchronous persisting, return an `Error` if the persisting fails, or any other value if all went well. For asynchronous persisting, you have to return a `Promise`. If the persisting fails, resolve the promise with an `Error` object or reject the promise. If the persisting succeeded, resolve the promise with any non-error value. ### pivotGrandTotalColumnPosition > Controls the position and visibility of pivot grand-total columns If specified as `false`, the pivot grand-total columns are not displayed. For normal pivot total columns, see [`pivotTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotTotalColumnPosition). Pivot total columns only display when [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) has two or more fields (`pivotBy.length > 1`). With a single pivot field, enabling [`pivotTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotTotalColumnPosition) has no effect — the totals would be the same as the already displayed values. The example below pivots by `stack` and `canDesign` so both pivot totals and grand totals are visible. **Example: Pivoting with pivotGrandTotalColumnPosition=start** ```ts import { InfiniteTable, DataSource, DataSourceGroupBy, DataSourcePivotBy, InfiniteTableColumnAggregator, DataSourcePropAggregationReducers, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, }; const defaultGroupBy: DataSourceGroupBy[] = [ { field: 'country', }, { field: 'city', }, ]; const defaultPivotBy: DataSourcePivotBy[] = [ { field: 'stack', }, { field: 'canDesign', columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer', }; }, }, ]; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => Math.round(arr.length ? sum / arr.length : 0), }; const aggregations: DataSourcePropAggregationReducers = { salary: { ...avgReducer, name: 'Salary (avg)', field: 'salary', }, age: { ...avgReducer, name: 'Age (avg)', field: 'age', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" defaultGroupBy={defaultGroupBy} defaultPivotBy={defaultPivotBy} aggregationReducers={aggregations} data={dataSource} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivot-grand-total-column-position-example" groupRenderStrategy="single-column" columns={columns} columnDefaultWidth={200} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} pivotTotalColumnPosition="end" pivotGrandTotalColumnPosition="start" /> ); }} ); } ``` ### pivotTotalColumnPosition > Controls the position and visibility of pivot total columns If specified as `false`, the pivot total columns are not displayed. For grand-total pivot columns, see [`pivotGrandTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotGrandTotalColumnPosition). Pivot total columns only make sense when pivoting by two or more pivot fields, and thus will only display if this is the case. You can however, display grand-total columns if you have a single pivot field (or even no pivot fields - so [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) is an empty array). In case there are no pivot fields, but [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) is an empty array, by default, a total column will be displayed for each aggregation (unless you specify [`pivotTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotTotalColumnPosition) as `false`). **Example: Pivoting with pivotTotalColumnPosition=start** ```ts import { InfiniteTable, DataSource, DataSourceGroupBy, DataSourcePivotBy, InfiniteTableColumnAggregator, DataSourcePropAggregationReducers, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, }; const defaultGroupBy: DataSourceGroupBy[] = [ { field: 'country', }, { field: 'city', }, ]; const defaultPivotBy: DataSourcePivotBy[] = [ { field: 'stack', }, { field: 'canDesign', columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer', }; }, }, ]; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => Math.round(arr.length ? sum / arr.length : 0), }; const aggregations: DataSourcePropAggregationReducers = { salary: { ...avgReducer, name: 'Salary (avg)', field: 'salary', }, age: { ...avgReducer, name: 'Age (avg)', field: 'age', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" defaultGroupBy={defaultGroupBy} defaultPivotBy={defaultPivotBy} aggregationReducers={aggregations} data={dataSource} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivot-total-column-position-example" groupRenderStrategy="single-column" columns={columns} columnDefaultWidth={200} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} pivotTotalColumnPosition="start" /> ); }} ); } ``` ### resizableColumns (`boolean`) > Controls if by default all columns are resizable or not. This property controls the behavior for all columns that don't have [`columns.resizable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.resizable) explicitly specified. **Example: Resizable columns example** For resizable columns, hover the mouse between column headers to grab & drag the resize handle. Hold SHIFT when grabbing in order to **share space on resize**. ```ts import { InfiniteTable, DataSource, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; export default function App() { const [resizableColumns, setResizableColumns] = useState(true); return ( <>

Columns are currently{' '} {resizableColumns ? 'resizable' : 'NOT RESIZABLE'}.

data={dataSource} primaryKey="id"> debugId="resizableColumns-example" resizableColumns={resizableColumns} columns={columns} columnDefaultWidth={100} columnMinWidth={30} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### rowHeight (`number|string`) > Specifies the height for rows. If a string is passed, it should be the name of a CSS variable, eg `--row-height` **Example: rowHeight as number** ```ts files=["rowHeight-number-example.page.tsx","data.ts"] ``` **Example: rowHeight from CSS variable name** ```ts files=["rowHeight-cssvar-example.page.tsx","data.ts"] ``` ### rowHoverClassName (`string?`) > Specifies the className to be applied to a row, when it is hovered. This property is static and cannot be a function, as applying the hover style does not trigger a re-render. Combined with the related [`rowClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowClassName) you will be able to achieve any dynamic use-case your app may need. In the example below, we applied `bg-orange-800!` (notice the Tailwind important `!` modifier) - because the default Infinite row styles target the `background` of the rows, and not the `background-color` as Tailwind CSS `bg-*` classes. Hence the important CSS modifier. ```tsx import * as React from 'react'; import { InfiniteTable, DataSource, type InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, salary: { field: 'salary', type: 'number' }, }; export default function App() { return ( primaryKey="id" data={dataSource}> debugId="row-hover-class-name-example" columns={columns} columnDefaultWidth={200} rowHoverClassName="bg-orange-800!" /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### rowClassName (`string|(params:InfiniteTableStylingFnParams) => string`) > Specifies the className to be applied to all rows or conditionally to certain rows. The `rowClassName` prop can be either a string or a function that returns a string. When used as a function, it's called with a param of type [`InfiniteTableStylingFnParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableStylingFnParams), just like the [`rowStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowStyle) function. ### rowStyle (`CSSProperties|(params:InfiniteTableStylingFnParams) => CSSProperties`) > Specifies the style object to be applied to all rows or conditionally to certain rows. The `rowStyle` prop can be either an object (typed as `React.CSSProperties`) or a function that is called with a param of type [`InfiniteTableStylingFnParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableStylingFnParams) ### `rowStyle` as a function When `rowStyle` is a function, it's called with a param of type [`InfiniteTableStylingFnParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableStylingFnParams) When Infinite Table calls `rowStyle`, the `data` property can be null - this is the case for grouped rows. The `rowInfo` object contains the following properties (see [type definition here](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo)): - `id` - the id of the current row - `data` - the data object - `indexInAll` - the index in the whole dataset - `indexInGroup` - the index of the row in the current group - `groupBy` - the fields used to group the `DataSource` - `isGroupRow` - whether the row is a group row - `collapsed` - for a group row, whether the group row is collapsed See [Using RowInfo](https://infinite-table.com/docs/learn/rows/using-row-info.md) for more details. You can either return a valid style object, or undefined. ```tsx const rowStyle: InfiniteTablePropRowStyle = ({ data, rowInfo, }: { data: Employee | null; rowInfo: InfiniteTableRowInfo; }) => { const salary = data ? data.salary : 0; if (salary > 150_000) { return { background: 'tomato' }; } if (rowInfo.indexInAll % 10 === 0) { return { background: 'lightblue', color: 'black' }; } }; ``` **Example: rowStyle example usage** ```ts files=["rowStyle-example.page.tsx","rowStyle-example-columns.ts"] ``` ### viewportReservedWidth (`number`) > Specifies the width of the space to be kept as blank - useful when there are flex columns. This number can even be negative. The flexbox algorithm also uses `viewportReservedWidth` to determine the width of the viewport to use for sizing columns - you can use `viewportReservedWidth=100` to always have a `100px` reserved area that won't be used for flexing columns. Or you can use a negative value, eg `-200` so the flexbox algorithm will use another `200px` (in addition to the available viewport area) for sizing flexible columns - this will result in a horizontal scrollbar being visible. For reacting to column resizing, you need to listen to [`onViewportReservedWidthChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onViewportReservedWidthChange) **Example: Using viewportReservedWidth to reserve whitespace when you have flexible columns** Resize a column to see `viewportReservedWidth` updated and then click the button to reset it to `0px` ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumnSizing, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; export const columns: Record> = { firstName: { field: 'firstName', header: 'First Name', }, country: { field: 'country', header: 'Country', }, city: { field: 'city', header: 'City', }, salary: { field: 'salary', type: 'number', header: 'Salary', }, }; const defaultColumnSizing: InfiniteTablePropColumnSizing = { country: { flex: 1 }, city: { flex: 1 }, salary: { flex: 2 }, }; export default function App() { const [viewportReservedWidth, setViewportReservedWidth] = useState(0); return ( <>

Current viewport reserved width: {viewportReservedWidth}px.

data={dataSource} primaryKey="id"> debugId="viewportReservedWidth-example" columns={columns} columnDefaultWidth={50} viewportReservedWidth={viewportReservedWidth} onViewportReservedWidthChange={setViewportReservedWidth} defaultColumnSizing={defaultColumnSizing} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/employees100') .then((r) => r.json()) .then((data: Employee[]) => data); }; export type Employee = { id: number; companyName: string; companySize: string; firstName: string; lastName: string; country: string; countryCode: string; city: string; streetName: string; streetNo: string; department: string; team: string; salary: number; age: number; email: string; }; ``` ### shouldAcceptEdit (`(params) => boolean|Error|Promise`) > Function used to validate edits for all columns. This function is called when the user wants to finish an edit - it is used to decide whether an edit is accepted or rejected, for all columns.

This overrides the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) prop.

If you define the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) and still want to use the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit), you can call the column-level function from this global one.

The function is called with an object that has the following properties: - `value` - the value that the user wants to persist via the cell editor - `initialValue` - the initial value of the cell (the value that was displayed before editing started). This is the value resulting after [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) and [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) have been applied) - `rawValue` - the initial value of the cell, but before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `data` - the current data object - `rowInfo` - the row info object that underlies the row - `column` - the current column on which editing is invoked - `api` - a reference to the [InfiniteTable API](https://infinite-table.com/docs/reference/api/index.md) - `dataSourceApi` - - a reference to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) **Example** Edit the `salary` column. Only valid numbers are persisted. ```ts import { InfiniteTable, DataSource, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; currency: string; stack: string; hobby: string; salary: string; }; const dataSource: Developer[] = [ { id: 1, firstName: 'John', currency: 'USD', stack: 'frontend', hobby: 'gaming', salary: 'USD 1000', }, { id: 2, firstName: 'Jane', currency: 'EUR', stack: 'backend', hobby: 'reading', salary: 'EUR 2000', }, { id: 3, firstName: 'Jack', currency: 'GBP', stack: 'frontend', hobby: 'gaming', salary: 'GBP 3000', }, { id: 4, firstName: 'Jill', currency: 'USD', stack: 'backend', hobby: 'reading', salary: 'USD 4000', }, ]; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, salary: { // the only editable column defaultEditable: true, defaultWidth: 320, field: 'salary', header: 'Salary - edit accepts numbers only', style: { color: 'tomato' }, getValueToEdit: ({ value }) => { return parseInt(value.substr(4), 10); }, getValueToPersist: ({ value, data }) => { return `${data!.currency} ${parseInt(value, 10)}`; }, }, firstName: { field: 'firstName', header: 'Name', }, currency: { field: 'currency', header: 'Currency', }, }; export default function InlineEditingExample() { const shouldAcceptEdit = ({ value }: { value: any }) => { return parseInt(value, 10) == value; }; return ( <> primaryKey="id" data={dataSource}> debugId="global-should-accept-edit-example" columns={columns} columnDefaultEditable={false} shouldAcceptEdit={shouldAcceptEdit} /> ); } ``` ### scrollTopKey (`number|string`) > Determines scrolling the table to the top. Use this property to declaratively tell the `InfiniteTable` component to scroll to the top. Whenever a new value is provided for this property, it will scroll to the top. **Example: Declaratively scrolling to the top of the table** ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, age: { field: 'age', type: 'number' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency', type: 'number' }, }; export default function GroupByExample() { const [scrollTopKey, setScrollTopKey] = React.useState(0); return ( <> primaryKey="id" data={dataSource}> debugId="scrollTopKey-example" scrollTopKey={scrollTopKey} columns={columns} columnDefaultWidth={200} /> ); } ``` ### virtualizeColumns (`boolean`) > Configures whether columns are virtualized or not By default, columns are virtualized in order to improve performance. --- # Infinite Table Keyboard Navigation API Canonical page: https://infinite-table.com/docs/reference/keyboard-navigation-api/ Available starting with version `6.1.1`. See the [Keyboard Navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells.md) page for more details. ```tsx title="Configuring the keyboard navigation to be 'cell'" // can be "cell" (default), "row" or false ``` You can retrieve the keyboard navigation api by reading it from the `api.keyboardNavigationApi` property. ```tsx {4} const onReady = ({api}: {api:InfiniteTableApi}) => { // do something with it api.keyboardNavigationApi.gotoCell({direction: 'top'}) } columns={[...]} onReady={onReady} /> ``` See the [Infinite Table API page](https://infinite-table.com/docs/reference/api/index.md) for the main API. See the [Infinite Table Cell Selection API page](https://infinite-table.com/docs/reference/cell-selection-api/index.md) for the row selection API. See the [Infinite Table Row Selection API page](https://infinite-table.com/docs/reference/row-selection-api/index.md) for the row selection API. See the [Infinite Table Column API page](https://infinite-table.com/docs/reference/column-api/index.md) for the column API. ### setKeyboardNavigation (`(keyboardNavigation: 'cell'|'row'|false) => void`) > Sets the keyboard navigation mode. See [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) The sole argument is of the same type as the [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) See the [Keyboard Navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells.md) page for more details. If you are using controlled [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex) or [`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex), make sure you update the values by using the [`onActiveCellIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveCellIndexChange) and [`onActiveRowIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) callbacks respectively. ### setActiveCellIndex (`(activeCellIndex: [number, number]) => void`) > Sets the value for [`defaultActiveCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultActiveCellIndex)/[`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex) See related [`gotoCell`](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md#gotoCell) If you are using controlled [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex) make sure you update the controlled value by using the [`onActiveCellIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveCellIndexChange) callback prop. ### setActiveRowIndex (`(activeRowIndex: number) => void`) > Sets the value for [`defaultActiveRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultActiveRowIndex)/[`activeRowIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRowIndex) If you are using controlled [`activeRow`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeRow), make sure you update the values by using the [`onActiveRowIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange) callback prop. ### gotoNextRow (`()=> number | false`) > Changes the active row index to the next row. See related [`gotoPreviousRow`](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md#gotoPreviousRow), [`setActiveRowIndex`](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md#setActiveRowIndex) Returns `false` if the action was not successful (eg: already at the last row), otherwise the new active row index. This sets the value for [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex) ### gotoPreviousRow (`()=> number | false`) > Changes the active row index to the prev row. See related [`gotoNextRow`](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md#gotoNextRow), [`setActiveRowIndex`](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md#setActiveRowIndex) Returns `false` if the action was not successful (eg: already at the first row), otherwise the new active row index. This sets the value for [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex) ### gotoCell (`({direction: 'top' | 'bottom' | 'left' | 'right' }) => [number, number] | false`) > Changes the active cell index, by navigating to the specified direction (equivalent to pressing the arrow keys). See related [`setActiveCellIndex`](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md#setActiveCellIndex) **Example: Using KeyboardNavigationApi.gotoCell** ```tsx import { InfiniteTable, DataSource, DataSourceData, InfiniteTableKeyboardNavigationApi, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, canDesign: { field: 'canDesign' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; export default function App() { const [keyboardNavigationApi, setKeyboardNavigationApi] = useState< InfiniteTableKeyboardNavigationApi | undefined >(undefined); return ( <> primaryKey="id" data={dataSource}>
debugId="goto-cell-example" // keyboardNavigation="cell" is the default, so no need to specify it columns={columns} defaultActiveCellIndex={[0, 0]} onReady={({ api }) => { setKeyboardNavigationApi(api.keyboardNavigationApi); }} /> ); } ``` --- # Infinite Table Row Details API Canonical page: https://infinite-table.com/docs/reference/row-detail-api/ This API can be used when [master-detail](https://infinite-table.com/docs/learn/master-detail/overview.md) is configured in the DataGrid. You can retrieve the row details api by reading it from the `api.rowDetailApi` property. ```tsx {4} const onReady = ({api}: {api:InfiniteTableApi}) => { // do something with it api.rowDetailApi.collapseAllDetails() } columns={[...]} onReady={onReady} /> ``` See the [Infinite Table API page](https://infinite-table.com/docs/reference/api/index.md) for the main API. See the [Infinite Table Cell Selection API page](https://infinite-table.com/docs/reference/cell-selection-api/index.md) for the row selection API. See the [Infinite Table Row Selection API page](https://infinite-table.com/docs/reference/row-selection-api/index.md) for the row selection API. See the [Infinite Table Column API page](https://infinite-table.com/docs/reference/column-api/index.md) for the column API. ### collapseAllDetails (`() => void`) > Collapses all row details. **Example: Master detail DataGrid with collapse all button** Some of the rows in the master DataGrid are expanded by default. You can collapse them via the Row Detail API. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, RowDetailStateObject, InfiniteTableApi, RowDetailState, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-api-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } export default () => { const [rowDetailState, setRowDetailState] = React.useState< RowDetailStateObject >({ collapsedRows: true as const, expandedRows: [39, 54], }); const onRowDetailStateChange = React.useCallback( (rowDetailState: RowDetailState) => { setRowDetailState(rowDetailState.getState()); }, [], ); const [api, setApi] = React.useState | null>(null); return ( <>
Row detail state: {JSON.stringify(rowDetailState, null, 2)}
data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-api-example-2" domProps={domProps} onReady={({ api }) => { setApi(api); }} columnDefaultWidth={150} rowDetailState={rowDetailState} onRowDetailStateChange={onRowDetailStateChange} columnMinWidth={50} columns={masterColumns} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### expandAllDetails (`() => void`) > Expands all row details. **Example: Master detail DataGrid with expand all button** Click the `Expand All` button to expand all row details. ```ts import * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, RowDetailStateObject, InfiniteTableApi, RowDetailState, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; city: string; currency: string; country: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; salary: number; }; type City = { id: number; name: string; country: string; }; const masterColumns: InfiniteTablePropColumns = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns = { firstName: { field: 'firstName', header: 'First Name', }, salary: { field: 'salary', type: 'number', }, stack: { field: 'stack' }, currency: { field: 'currency' }, city: { field: 'city' }, }; const domProps = { style: { height: '100%', }, }; const shouldReloadData = { sortInfo: true, filterValue: true, }; function renderDetail(rowInfo: InfiniteTableRowInfo) { console.log('rendering detail for master row', rowInfo.id); return ( data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > debugId="master-detail-api-example" columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> ); } export default () => { const [rowDetailState, setRowDetailState] = React.useState< RowDetailStateObject >({ collapsedRows: true as const, expandedRows: [39, 54], }); const onRowDetailStateChange = React.useCallback( (rowDetailState: RowDetailState) => { setRowDetailState(rowDetailState.getState()); }, [], ); const [api, setApi] = React.useState | null>(null); return ( <>
Row detail state: {JSON.stringify(rowDetailState, null, 2)}
data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > debugId="master-detail-api-example-2" domProps={domProps} onReady={({ api }) => { setApi(api); }} columnDefaultWidth={150} rowDetailState={rowDetailState} onRowDetailStateChange={onRowDetailStateChange} columnMinWidth={50} columns={masterColumns} rowDetailRenderer={renderDetail} /> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData = () => { const cityNames = new Set(); const result: City[] = []; return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`) .then((response) => response.json()) .then((response) => { response.data.forEach((data: Developer) => { if (cityNames.has(data.city)) { return; } cityNames.add(data.city); result.push({ name: data.city, country: data.country, id: result.length, }); }); return result; }); }; const detailDataSource: DataSourceData = ({ filterValue, sortInfo, masterRowInfo, }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } if (!filterValue) { filterValue = []; } if (masterRowInfo) { // filter by master country and city filterValue = [ { field: 'city', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.name, }, }, { field: 'country', filter: { operator: 'eq', type: 'string', value: masterRowInfo.data.country, }, }, ...filterValue, ]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, filterValue ? 'filterBy=' + JSON.stringify( filterValue.map(({ field, filter }) => { return { field: field, operator: filter.operator, value: filter.type === 'number' ? Number(filter.value) : filter.value, }; }), ) : null, ] .filter(Boolean) .join('&'); return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ### isRowDetailCollapsed (`(rowId: any)=> boolean`) > Checks if the row detail is collapsed for the row with the specified primary key. ### isRowDetailExpanded (`(rowId: any)=> boolean`) > Checks if the row detail is expanded for the row with the specified primary key. ### collapseRowDetail (`(rowId: any) => void`) > Collapses the detail for the row with the specified primary key. ### expandRowDetail (`(rowId: any)=> boolean`) > Expands the detail for the row with the specified primary key. ### toggleRowDetail (`(rowId: any)=> boolean`) > Toggles the expand/collapse state of the row detail, for the row with the specified primary key. --- # Infinite Table Row Selection API Canonical page: https://infinite-table.com/docs/reference/row-selection-api/ ```tsx title="Configuring the selection mode to be 'multi-row'" // can be "single-row", "multi-row", "multi-cell" or false ``` To enable multi-row selection, you need to specify [selectionMode="multi-row"](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode) on the `` component. You can retrieve the row selection api by reading it from the `api.rowSelectionApi` property. ```tsx {4} const onReady = ({api}: {api:InfiniteTableApi}) => { // do something with it api.rowSelectionApi.selectGroupRow(['USA']) } columns={[...]} onReady={onReady} /> ``` See the [Infinite Table API page](https://infinite-table.com/docs/reference/api/index.md) for the main API. See the [Infinite Table Cell Selection API page](https://infinite-table.com/docs/reference/cell-selection-api/index.md) for the cell selection API. See the [Infinite Table Column API page](https://infinite-table.com/docs/reference/column-api/index.md) for the column API. See the [Infinite Table Row Detail API page](https://infinite-table.com/docs/reference/row-detail-api/index.md) for the row detail API (when master-detail is configured). ### allRowsSelected (`boolean`) > Boolean getter to report whether all the rows are selected or not ### deselectGroupRow (`(groupKeys: any[]) => void`) > Deselects the group row that is identified by the given group keys. Only makes sense when the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy). Calling this method triggers a call to [DataSource.onRowSelectionChange](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange). For selecting a group row, see related [selectGroupRow](#selectGroupRow). Most often, you don't need to use this imperative way of selecting group rows. Simply update the [DataSource.rowSelection](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) to exclude the group row from the selection. ### deselectRow (`(primaryKey: any, groupKeys?: any[]) => boolean`) > Deselects the specified row. Optionally provide the group keys, if you have access to them. See note from [isRowSelected](#isRowSelected) for whether you need to provide the `groupKeys` or not. Calling this method triggers a call to [DataSource.onRowSelectionChange](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange). For selecting the row, see related [selectRow](#selectRow). Most often, you don't need to use this imperative way of deselecting rows. Simply update the [DataSource.rowSelection](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) to remove the row you want from the selection. ### deselectAll (`() => void`) > Deselects all the rows in the DataSource. Calling this method triggers a call to [DataSource.onRowSelectionChange](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange). For selecting all rows, see related [selectAll](#selectAll). Most often, you don't need to use this imperative way of deselecting all rows. Simply update the [DataSource.rowSelection](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) (when multiple row selection is enabled) to a value of ```tsx { defaultSelection: false, selectedRows: []} ``` ### getGroupRowSelectionState (`(groupKeys: any[], rowSelection?: DataSourceRowSelection) => true|false|null`) > Returns the state of a group row - only applicable when the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) The returned values can be: - `true` - the group row and all its children are selected, at any level of nesting - `false` - the group row and all its children are deselected, at any level of nesting - `null` - the group row has some (not all) children selected, at any level of nesting Baiscally, `true` means the group row and all children are selected, `false` means the group row is not selected and doesn't have any selected children, while `null` is the indeterminate state, where just some (but not all) of the children of the group are selected. If you provide a the value of a `rowSelection`, it will be used as the source of truth for selection. If no value for `rowSelection` is provided, it will use the current row selection. If you don't provide a value for the `rowSelection` and are calling this method in the [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange) callback prop, you might be one step behind the selection. In such a case, make sure you pass to this function the value you receive in the [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange) callback. ### getSelectedPrimaryKeys (`(rowSelection?: DataSourceRowSelection) => (string|number)[]`) > Retrieves the ids (primary keys) of the selected rows, when the selection contains group keys instead of primary keys (so when [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) is `true` and the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy)). If you provide a the value of a `rowSelection`, it will be used as the source of truth for retrieving the row ids. If no value for `rowSelection` is provided, it will use the current row selection. This will not work properly when the `DataSource` is configured with [lazy loading](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad), since it cannot give you primary keys of rows not yet loaded. If you don't provide a value for the `rowSelection` and are calling this method in the [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange) callback prop, you might be one step behind the selection. In such a case, make sure you pass to this function the value you receive in the [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange) callback. **Example: Using getSelectedPrimaryKeys in multi row checkbox selection with grouping** This example shows how you can use getSelectedPrimaryKeys with multiple row selection to retrieve the actual ids of the selected rows. ```ts import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTableProps, InfiniteTableApi, InfiniteTablePropColumns, DataSourceProps, DataSourcePropRowSelection_MultiRow, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useCallback, useRef, useEffect, useState } from 'react'; const columns: InfiniteTablePropColumns = { country: { field: 'country', }, firstName: { field: 'firstName', defaultHiddenWhenGroupedBy: '*', }, stack: { field: 'stack', renderGroupValue: ({ value }) => `Stack: ${value || ''}`, }, age: { field: 'age' }, id: { field: 'id' }, preferredLanguage: { field: 'preferredLanguage', renderGroupValue: ({ value }) => `Lang: ${value || ''}`, }, canDesign: { field: 'canDesign', renderGroupValue: ({ value }) => `Can design: ${value || ''}`, }, }; const defaultGroupBy: DataSourceProps['groupBy'] = [ { field: 'canDesign', }, { field: 'stack', }, { field: 'preferredLanguage', }, ]; const groupColumn: InfiniteTableProps['groupColumn'] = { field: 'firstName', renderSelectionCheckBox: true, defaultWidth: 300, }; const domProps = { style: { flex: 1, minHeight: 500, }, }; export default function App() { const apiRef = useRef | null>(null); const [rowSelection, setRowSelection] = useState({ selectedRows: [ ['yes', 'backend', 'TypeScript'], ['yes', 'backend', 'Go'], 16, 26, 30, ['yes', 'frontend'], ], deselectedRows: [4, 2], defaultSelection: false, }); const [selectedIds, setSelectedIds] = useState([]); const onReady = useCallback( ({ api }: { api: InfiniteTableApi }) => { apiRef.current = api; setSelectedIds( api.rowSelectionApi.getSelectedPrimaryKeys(rowSelection) as string[], ); }, [], ); useEffect(() => { if (!apiRef.current) { return; } setSelectedIds( apiRef.current.rowSelectionApi.getSelectedPrimaryKeys( rowSelection, ) as string[], ); }, [rowSelection]); return (
Current row selection:
 {JSON.stringify(rowSelection, null, 2)}.
Current selected ids: {selectedIds.join(', ')}
data={dataSource} groupBy={defaultGroupBy} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} useGroupKeysForMultiRowSelection primaryKey="id" > debugId="controlled-multi-row-selection-example-with-group-keys" onReady={onReady} columns={columns} domProps={domProps} groupColumn={groupColumn} columnDefaultWidth={150} />
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### isRowSelected (`(primaryKey: any, groupKeys?: any[]) => boolean`) > Checks if a row specified by its primary key is selected or not. Optionally provide the group keys, if you have access to them. The group keys are not mandatory, and they are useful only when the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy). Even if you don't pass them, the component will try to retrieve them from its internal state - note though that in lazy-load scenarios, not all rows/groups may have been loaded, so in this case, you have to make sure you provide the `groupKeys` when calling this method. ### isRowDeselected (`(primaryKey: any, groupKeys?: any[]) => boolean`) > Checks if a row specified by its primary key is deselected or not. Optionally provide the group keys, if you have access to them. See note from [isRowSelected](#isRowSelected) ### selectAll (`() => void`) > Selects all the rows in the DataSource. Calling this method triggers a call to [DataSource.onRowSelectionChange](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange). For deselecting all rows, see related [deselectAll](#deselectAll). Most often, you don't need to use this imperative way of selecting rows. Simply update the [DataSource.rowSelection](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) (when multiple row selection is enabled) to a value of ```tsx { defaultSelection: true, deselectedRows: []} ``` ### selectGroupRow (`(groupKeys: any[]) => void`) > Selects the group row that is identified by the given group keys. Only makes sense when the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy). Calling this method triggers a call to [DataSource.onRowSelectionChange](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange). For deselecting a group row, see related [deselectGroupRow](#deselectGroupRow). Most often, you don't need to use this imperative way of selecting group rows. Simply update the [DataSource.rowSelection](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) to include the group row you want to select. ### selectRow (`(primaryKey: any, groupKeys?: any[]) => boolean`) > Selects the specified row. Optionally provide the group keys, if you have access to them. See note from [isRowSelected](#isRowSelected) for whether you need to provide the `groupKeys` or not. Calling this method triggers a call to [DataSource.onRowSelectionChange](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange). For deselecting the row, see related [deselectRow](#deselectRow). Most often, you don't need to use this imperative way of selecting rows. Simply update the [DataSource.rowSelection](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) to include the row you want in the selection. ### toggleGroupRowSelection (`(groupKeys: any[]) => void`) > Toggles the selection of the group row that is identified by the given group keys. Only makes sense when the DataSource is [grouped](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy). Calling this method triggers a call to [DataSource.onRowSelectionChange](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange). For deselecting a group row, see related [deselectGroupRow](#deselectGroupRow). For selecting a group row, see related [selectGroupRow](#selectGroupRow). Most often, you don't need to use this imperative way of selecting group rows. Simply update the [DataSource.rowSelection](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) to include the group row you want to select. ### toggleRowSelection (`(primaryKey: any, groupKeys?: any[]) => boolean`) > Toggles the selection of the specified row. Optionally provide the group keys, if you have access to them. See note from [isRowSelected](#isRowSelected) for whether you need to provide the `groupKeys` or not. Calling this method triggers a call to [DataSource.onRowSelectionChange](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange). For deselecting the row, see related [deselectRow](#deselectRow). For selecting the row, see related [selectRow](#selectRow). For toggling the selection for a group row, see related [toggleGroupRowSelection](#toggleGroupRowSelection). Most often, you don't need to use this imperative way of selecting rows. Simply update the [DataSource.rowSelection](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) to include or exclude the given row. --- # Tree API Canonical page: https://infinite-table.com/docs/reference/tree-api/ When rendering the `TreeDataSource` component, you can get access to the Tree API by reading it from the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) [`treeApi`](https://infinite-table.com/docs/reference/datasource-api/index.md#treeApi) property. ```tsx {3} onReady={(api: DataSourceApi) => { api.treeApi // <---- // treeApi is accessible here // you may want to store a reference to it in a ref or somewhere in your app state }} /> ``` For updating tree nodes, see the following methods: - [`updateDataByNodePath`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataByNodePath) - [`removeDataByNodePath`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByNodePath) ### expandAll (`() => void`) > Expands all the nodes in the tree. See related [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) prop. **Example: Expanding all nodes** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### selectAll (`() => void`) > Selects all the nodes in the tree. See related [`deselectAll`](https://infinite-table.com/docs/reference/tree-api/index.md#deselectAll) prop. This works if the tree has selection enabled. See [tree selection](https://infinite-table.com/docs/learn/tree-grid/tree-selection.md) for more details. **Example: Selecting all nodes via Tree API** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### selectNode (`(nodePath: NodePath) => void`) > Selects the node with the given node path. See related [`deselectNode`](https://infinite-table.com/docs/reference/tree-api/index.md#deselectNode) and [`toggleNodeSelection`](https://infinite-table.com/docs/reference/tree-api/index.md#toggleNodeSelection) methods. **Example: Selecting a node via Tree API** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeRowIndex, setActiveRowIndex] = useState(0); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### deselectNode (`(nodePath: NodePath) => void`) > Deselects the node with the given node path. See related [`selectNode`](https://infinite-table.com/docs/reference/tree-api/index.md#selectNode) and [`toggleNodeSelection`](https://infinite-table.com/docs/reference/tree-api/index.md#toggleNodeSelection) methods. **Example: Deselecting a node via Tree API** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeRowIndex, setActiveRowIndex] = useState(0); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### toggleNodeSelection (`(nodePath: NodePath) => void`) > Toggles the selection state of the node with the given node path. See related [`selectNode`](https://infinite-table.com/docs/reference/tree-api/index.md#selectNode) and [`deselectNode`](https://infinite-table.com/docs/reference/tree-api/index.md#deselectNode) methods. **Example: Toggling a node's selection state via Tree API** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeRowIndex, setActiveRowIndex] = useState(0); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### deselectAll (`() => void`) > Deselects all the nodes in the tree. See related [`selectAll`](https://infinite-table.com/docs/reference/tree-api/index.md#selectAll) prop. This works if the tree has selection enabled. See [tree selection](https://infinite-table.com/docs/learn/tree-grid/tree-selection.md) for more details. **Example: Deselecting all nodes via Tree API** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### collapseAll (`() => void`) > Collapses all the nodes in the tree. See related [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) prop. **Example: Collapsing all nodes** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### isNodeExpanded (`(nodePath: NodePath) => boolean`) > Returns `true` if the node is expanded, `false` otherwise. **Example: Checking if a node is expanded** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeRowIndex, setActiveRowIndex] = useState(0); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ### toggleNode (`(nodePath: NodePath, options?: {force?: boolean}) => void`) > Toggles the node with the give node path. If the node at the given path is expanded, it will be collapsed and vice versa. See related [`expandNode`](https://infinite-table.com/docs/reference/tree-api/index.md#expandNode) and [`collapseNode`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseNode) methods. ### expandNode (`(nodePath: NodePath, options?: {force?: boolean}) => void`) > Expands the node with the given node path. See related [`collapseNode`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseNode) and [`toggleNode`](https://infinite-table.com/docs/reference/tree-api/index.md#toggleNode) methods. Expands the node. Does not affect other child nodes. **Example: Expanding a node** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeRowIndex, setActiveRowIndex] = useState(0); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` If `options.force` is `true`, the node will be expanded even if [`isNodeReadOnly`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeReadOnly) is `true` for the given node. ### collapseNode (`(nodePath: NodePath, options?: {force?: boolean}) => void`) > Collapses the node with the given node path. See related [`expandNode`](https://infinite-table.com/docs/reference/tree-api/index.md#expandNode) and [`toggleNode`](https://infinite-table.com/docs/reference/tree-api/index.md#toggleNode) methods. Collapses the node. Does not affect other child nodes. **Example: Collapsing a node** ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); const [activeRowIndex, setActiveRowIndex] = useState(0); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', extension: 'txt', mimeType: 'text/plain', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` If `options.force` is `true`, the node will be collapsed even if [`isNodeReadOnly`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeReadOnly) is `true` for the given node. --- # Infinite Table Type Definitions > TypeScript type definitions for Infinite Table Canonical page: https://infinite-table.com/docs/reference/type-definitions/ These are the public type definitions for `InfiniteTable` and related components, that you can import with named imports from the `@infinite-table/infinite-react` package. ```tsx title="Importing the type for rowInfo" import type { InfiniteTableRowInfo } from '@infinite-table/infinite-react'; ``` The types of all properties in the `InfiniteTable` and `DataSource` components respect the following naming convention: `Prop` So, for example, the type for [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) is [`DataSourcePropGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePropGroupBy) ### DataSourceState > Represents the state of the whole `` component. You can grab a reference to the `` component state via the [`useDataSourceState`](https://infinite-table.com/docs/reference/hooks/index.md#useDataSourceState) hook that Infinite exposes. Available properties: - `dataArray` - array of [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) ### TreeSelectionValue > Represents the selection state of the tree nodes. See [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection) for more details. ```ts import type { TreeSelectionValue } from '@infinite-table/infinite-react'; ``` The selection value is an object with the following properties: - `defaultSelection`: `boolean` - whether the tree nodes are selected by default or not. - `selectedPaths?`: `NodePath[]` - the paths of the selected nodes. Mandatory if `defaultSelection` is `false`. - `deselectedPaths`: `NodePath[]` - the paths of the deselected nodes. Mandatory if `defaultSelection` is `true`. ```tsx title="Example of tree selection value" const treeSelection: TreeSelectionValue = { defaultSelection: false, selectedPaths: [['1'], ['2', '20']], deselectedPaths: [['1','10']], }; // node ['1'] will be selected but indeterminate // since ['1','10'] is in the deselectedPaths // node ['2','20'] will be fully selected ``` ### TreeExpandStateValue > Represents the expand/collapse state of the tree nodes. See [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState) for more details. ```ts import type { TreeExpandStateValue } from '@infinite-table/infinite-react'; ``` You can specify the expand/collapse state of the tree nodes in two ways: 1. With node paths (recommended) When using node paths, the object should have the following properties: - `defaultExpanded`: `boolean` - whether the tree nodes are expanded by default or not. - `collapsedPaths`: `string[]` - when `defaultExpanded` is `true`, this is a mandatory prop. - `expandedPaths`: `string[]` - when `defaultExpanded` is `false`, this is a mandatory prop. ```tsx title="Example of treeExpandState with node paths" const treeExpandState = { defaultExpanded: true, collapsedPaths: [ ['1', '10'], ['2', '20'], ['5'] ], expandedPaths: [ ['1', '4'], ['5','nested node in 5'], ], }; ``` 2. With node ids When using node ids, the object should have the following properties: - `defaultExpanded`: `boolean` - whether the tree nodes are expanded by default or not. - `collapsedIds`: `string[]` - when `defaultExpanded` is `true`, this is a mandatory prop. - `expandedIds`: `string[]` - when `defaultExpanded` is `false`, this is a mandatory prop. ```tsx title="Example of treeExpandState with node ids" const treeExpandState = { defaultExpanded: true, collapsedIds: ['1', '2', '5'], expandedIds: ['10', '20', 'nested node in 5'], }; ``` ### RowDetailState > Represents the collapse/expand state of row details - when [master-detail is configured](https://infinite-table.com/docs/learn/master-detail/overview.md). Also see [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) for the most important property in the master-detail configuration. This class can be instantiated and the value passed to the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop (or its uncontrolled variant, [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState)). ```tsx title="Passing an instance of RowDetailState to the InfiniteTable" const rowDetailState = new RowDetailState({ collapsedRows: true, expandedRows: [2, 3, 4], }); rowDetailState={rowDetailState} />; ``` ```tsx title="Passing an object literal to the InfiniteTable" rowDetailState={{ collapsedRows: true, expandedRows: [2, 3, 4], }} /> ``` The instance is only useful if you want to interrogate the object, with methods like `areAllCollapsed()`, `areAllExpanded()`, `isRowDetailsExpanded(rowId)` and so on. When using the [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange) callback, it's called with an instance of this class - if you want to use the object literal, make sure you call `rowDetailState.getState()` to get the plain object. The [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) and [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) accept both an object literal and an instance of this class. The object literal has the following properties: - `collapsedRows`: `boolean | any[]` - if `true`, all row details are collapsed. If an array, it contains the row ids of the rows that are collapsed. - `expandedRows`: `boolean | any[]` - if `true`, all row details are expanded. If an array, it contains the row ids of the rows that are expanded. You can create an instance using the object literal notation and you can get the object literal from the instance using the `getState` method: ```tsx const rowDetailState = new RowDetailState({ collapsedRows: true, expandedRows: [2, 3, 4], }); const clone = new RowDetailState(rowDetailState); const state = rowDetailState.getState(); ``` You can mark rows as expanded/collapsed even after creating the instance: ```tsx const rowDetailState = new RowDetailState({ collapsedRows: true, expandedRows: [2, 3, 4], }); rowDetailState.expandRowDetails(5); rowDetailState.collapseRowDetails(2); // now you can pass this instance back to the InfiniteTable component ``` ### DataSourceDataParams > The type for the object passed into the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function prop of the `DataSource` component. When the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function is called, it will be called with an object of this type. The following properties are available on this object: - `sortInfo?` - [`DataSourcePropSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePropSortInfo) - the current sort info for the grid. - `groupBy?` - an array of [`DataSourceGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceGroupBy) - the current group by for the grid. - `pivotBy?` - an array of [`DataSourcePivotBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePivotBy) - the current pivot by for the grid. - `filterValue?` - an array of [`DataSourceFilterValueItem`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceFilterValueItem) - the current filter value for the grid. - `masterRowInfo?` - [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) - only available if the DataSource is a detail DataSource - meaning there is a master DataGrid, and the DataSource is used to load the detail DataGrid. ### DataSourceFilterValueItem > The type for the items in the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) array prop of the `DataSource` component. ### GroupRowsState > Describes the collapse/expand state for group rows, when [grouping is used](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md). This is a class, and instances of it can be used as a value for the [`groupRowsState`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRowsState)/[`defaultGroupRowsState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultGroupRowsState) props. It's the sole argument available in the [`onGroupRowsStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onGroupRowsStateChange) callback. It gives you the following additional utility methods: - `getState()` - `areAllExpanded()` - `areAllCollapsed()` - `expandAll()` - `collapseAll()` - `isGroupRowExpanded(keys: any[][])` - `isGroupRowCollapsed(keys: any[][])` - `expandGroupRow(keys: any[][])` - `collapseGroupRow(keys: any[][])` - `toggleGroupRow(keys: any[][])` To create an instance, pass a plain object that describes the [`groupRowsState`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRowsState)/[`defaultGroupRowsState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultGroupRowsState) value: ```tsx const state = new GroupRowsState({ expandedRows: true, collapsedRows: [ ['Europe'] ['Europe','France'], ['Italy'] ] }) console.log(state.getState()) // will log the above object that was used // as the sole argument for the constructor ``` When you call those methods, be aware you're not updating the React state! So you'll have to clone the object, call the method on the clone and then update the React state - in the code below, notice the `onClick` code for the `Expand all`/`Collapse all` buttons. **Example: Using the expandAll/collapseAll methods with cloning the GroupRowsState instance** ```ts import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}, }, }, { field: 'stack', }, ]; const columns: InfiniteTablePropColumns = { country: { field: 'country', // specifying a style here for the column // note: it will also be "picked up" by the group column // if you're grouping by the 'country' field style: { color: 'tomato', }, }, firstName: { field: 'firstName' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, }; export default function App() { const [groupRowsState, setGroupRowsState] = React.useState< GroupRowsState >(() => { const groupRowsState = new GroupRowsState({ collapsedRows: true, expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']], }); return groupRowsState; }); return ( <> data={dataSource} primaryKey="id" groupBy={groupBy} groupRowsState={groupRowsState} onGroupRowsStateChange={setGroupRowsState} > debugId="using-group-rows-state-controlled-example" columns={columns} /> ); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; email: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; ``` ### DataSourcePivotBy > Describes a pivot value for the grid. This is the type for the items in the [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) array prop of the `DataSource` component. The most important property in this type is the `field` - which will be `keyof DATA_TYPE` - the field to pivot by. Another important property in this type is the `column`. It will be used to configure the generated pivot columns: - if it's an object literal, it will be applied to all generated columns - if it's a function, it will be called for each generated column, and the return value will be used to configure the column. ```tsx const pivotBy: DataSourcePivotBy[] = [ { field: 'country' }, { field: 'canDesign', column: ({ column: pivotCol }) => { const lastKey = pivotCol.pivotGroupKeys[pivotCol.pivotGroupKeys.length - 1]; return { header: lastKey === 'yes' ? '💅 Designer' : '💻 Non-designer', }; }, }, ]; ``` ### InfiniteTableColumnHeaderParam > Represents runtime information passed to rendering and styling functions called when rendering the column headers This object is passed to [`headerClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#headerClassName), [`headerStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#headerStyle), [`header`](https://infinite-table.com/docs/reference/infinite-table-props.md#header) and [`renderHeader`](https://infinite-table.com/docs/reference/infinite-table-props.md#renderHeader) functions. It is an object with the following properties: - `column` - see [`InfiniteTableComputedColumn`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableComputedColumn) for details - `columnSortInfo` - the current sort info for the column. it will be an object of type [`DataSourceSingleSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceSingleSortInfo) or `null`. - `filtered: boolean` - if the column is currently filtered or not - `api` - [`InfiniteTableApi`](https://infinite-table.com/docs/reference/api/index.md) - the api object. - `columnApi` - [`InfiniteTableColumnApi`](https://infinite-table.com/docs/reference/column-api/index.md) - the column api object. - `renderBag` - an object with various JSX values, the default elements rendered by the Infinite Table for the column header. It contains the following properties: - `header` - the default column header text - `sortIcon` - the default sort icon - `menuIcon` - the default column menu icon - `filterIcon` - the default column filter icon - `selectionCheckBox` - the default column selection checkbox ```tsx title="Example column.renderHeader function" const renderHeader = ({ renderBag }) => { return ( ({renderBag.header}) {renderBag.sortIcon} ); }; const columns = { salary: { field: 'salary', type: 'number', renderHeader, }, }; ``` ### InfiniteTableColumnStylingFnParams > Represents runtime information passed to many styling functions called when rendering the column cells This object is passed at runtime during the rendering of column cells. It is an object with the following properties: - `column` - see [`InfiniteTableComputedColumn`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableComputedColumn) for details - `rowInfo` - see [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) for details - `data` - the data object for the current row. The type of this object is `DATA_TYPE | Partial | null`. For regular rows, it will be of type `DATA_TYPE`, while for group rows it will be `Partial`. For rows not yet loaded (because of batching being used), it will be `null`. - `value` - the underlying value of the current cell - will generally be `data[column.field]`, if the column is bound to a `field` property - `inEdit`: `boolean` - `editError`: `Error` - `rowSelected`: `boolean | null;` - `rowActive`: `boolean | null` - `rowHasSelectedCells`: `boolean` - if the current row has selected cells or not The following functions all have this as first argument: - [`columns.style`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style) - [`columns.className`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.className) ### InfiniteTableStylingFnParams > Represents runtime information passed to many styling functions called when rendering rows/cells This object is passed at runtime during the rendering of grid rows/cells. It is an object with the following properties: - `rowInfo` - see [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) for details - `rowIndex`: `number` - the index of the row - `rowHasSelectedCells`: `boolean` - if the current row has selected cells or not The following functions all have this as first argument: - [`rowStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowStyle) - [`rowClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowClassName) - [`rowProps`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowProps) ### DataSourceSingleSortInfo > Represents information on a specific sort. Contains info about the field to sort by, the sort direction, and the sort type. This is the referenced by the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop. Basically the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) prop can be either an array of [`DataSourceSingleSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceSingleSortInfo) objects, or a single [`DataSourceSingleSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceSingleSortInfo) object (or null). These are the type properties: - `dir`: `1 | -1` - 1 means ascending sort order; -1 means descending sort order. - `field?`: `keyof DATA_TYPE` - the field to sort by. - `id?`: `string` - an id for the sort info. When a column is not bound to a `field`, use the column id as the `id` property of the sort info, if you need to specify a default sort order by that column. Note that columns have a [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter), which will be used when doing local sorting and the column is not bound to an exact field. - `type?`: `string` - the sort type to apply. See [`sortType`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortType) for more details. For example, you can use `"string"` or `"number"` or `"date"` ### DataSourcePropSortInfo > The type of the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) DataSource prop. Valid types for this prop are: - `null` - [`DataSourceSingleSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceSingleSortInfo) - [`DataSourceSingleSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceSingleSortInfo)[] ### DataSourcePropGroupBy > The type of the [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) prop. Basically this type is an array of [`DataSourceGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceGroupBy). ### InfiniteTableComputedColumn > This represents an enhanced column definition for a column. A computed column is basically a column with more information computed at runtime, based on everything Infinite Table can aggregate about it. This type also includes the properties of the `InfinteTableColumn` type: [`columns.id`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.id), [`columns.field`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field), [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter), etc. Additional type properties: - `id`: `string` - the id of the column. This is the same as the [`columns.id`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.id) prop. - `computedEditable`: `boolean| Function` - whether this column is ediable or not. See [`columns.defaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) for more details. - `computedWidth`: `number` - the actual calculated width of the column (in pixels) that will be used for rendering. This is computed based on the [`columns.defaultWidth`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultWidth), [`columns.defaultFlex`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFlex) and other min/max constraints. - `computedPinned`: `false | "start" | "end"` - `computedSortInfo`: [`DataSourceSingleSortInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceSingleSortInfo) or null - the sort info for this column. - `computedSorted`: `boolean` - whether this column is currently sorted or not. - `computedSortedAsc`: `boolean` - whether this column is currently sorted ascending or not. - `computedSortedDesc`: `boolean` - whether this column is currently sorted descending or not. - `computedFiltered`: `boolean` - whether this column is currently filtered or not. - ... and more (docs coming soon) ### InfiniteTableColumnCellContextType > The type for the parameter of [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) (and related rendering functions) and also for the object you get back when you call [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) These are the type properties: - `isGroupRow`: `boolean` - whether the current row is a group row or not. - `data`: `DATA_TYPE` | `Partial` | `null` - the data object for the current row. Because the DataSource can be grouped, the `data` object can be either the original data object, or a partial data object (containing the aggregated values - in case of a group row), or null. You can use `isGroupRow` to discriminate between these cases. If `isGroupRow` is `false`, then `data` is of type `DATA_TYPE`. - `rowInfo`: [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo). See that type for more details. - `rawValue`: `string` | `number` | other - the raw value for the cell - as computed from the [column field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) or [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) function. - `value`: `Renderable` - the current value to render for the cell. This is based on the `rawValue`, but if a [column valueFormatter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) exists, it will be the result of that. - `column`: [`InfiniteTableComputedColumn`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableComputedColumn) - the (computed) column definition for the current cell. - `columnsMap`: a map collection of [`InfiniteTableComputedColumn`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableComputedColumn) objects, keyed by column id. - `fieldsToColumn`: a map collection of [`InfiniteTableComputedColumn`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableComputedColumn) objects, keyed by the column field. If a column is not bound to a field, it will not be included in this map. - `align`: the computed value of the [align](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.align) prop for the current cell. This will be `"start"`, `"center"` or `"end"`. - `api`: [`InfiniteTableApi`](https://infinite-table.com/docs/reference/api/index.md) - the api object. - `rowInfo`: [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) - the row info for the current row. - `rowIndex`: `number` - the index of the current row. - `renderBag`: See [column rendering](https://infinite-table.com/docs/learn/columns/column-rendering.md#rendering-pipeline) for more details. - `toggleCurrentGroupRow`: `() => void` - a function that can be used to toggle the current row, if it's a group row. - `toggleCurrentTreeNode`: `() => void` - a function that can be used to toggle the expand/collapse state of the current tree node (only available when rendering [a tree grid](https://infinite-table.com/docs/learn/tree-grid/overview.md)). - `rootGroupBy`: [`DataSourceGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceGroupBy) - the group by specified in the [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) prop of the `DataSource`. - `groupByForColumn`: available for group columns. When [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is `"multi-column"`, this will be a single [`DataSourcePropGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePropGroupBy), for each of the generated group columns. When [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is `"single-column"`, this will be an array of [`DataSourcePropGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePropGroupBy) objects - it will be available only in the single group column that will be generated. ### InfiniteColumnEditorContextType > The type for the object you get back when you call [`useInfiniteColumnEditor`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnEditor) These are the type properties: - `api`: [`InfiniteTableApi`](https://infinite-table.com/docs/reference/api/index.md) - the api object. - `initialValue`: `any` - the initial value for the editor. - `value`: `any` - the current value for the editor. Initially will be the same as `initialValue`. If you use this value, then your editor is "controlled", so make sure that when the editor is changed, you call the `setValue` function with the new value. - `setValue`: `(value: any) => void` - should be called to update the value in the cell editor. Calling this does not complete the edit. - `confirmEdit`: a reference to [InfiniteTableApi.confirmEdit](https://infinite-table.com/docs/reference/api/index.md#confirmEdit). If you have called `setValue` while editing (meaning your editor was controlled), you don't have to pass any parameters to this function. - the last value of the editor will be used. If your editor is uncontrolled and you haven't called `setValue`, you need to call `confirmEdit` with the value that you want to confirm for the edit. - `cancelEdit`: a reference to [InfiniteTableApi.cancelEdit](https://infinite-table.com/docs/reference/api/index.md#cancelEdit). Call this to cancel the edit and close the editor. Doesn't require any parameters. - `rejectEdit`: a reference to [InfiniteTableApi.rejectEdit](https://infinite-table.com/docs/reference/api/index.md#rejectEdit). Call this to reject the edit and close the editor. You can pass an `Error` object when calling this function to specify the reason for the rejection. - `readOnly`: `boolean` - whether the cell is read-only or not. Inside the [`useInfiniteColumnEditor`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnEditor) hook, you can still call [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) to get access to the cell-related information. ### DataSourceGroupBy > The type for the objects in the [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) array. See related [`DataSourcePropGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePropGroupBy) The type is generic, and the generic type parameter is the type of the data in the grid. In this documentation, either `DATA_TYPE` or `T` will be used to refer to the generic type parameter. These are the type properties: - `field` - `keyof DATA_TYPE`. The field to group by. - `column`: `Partial` - `toKey?`: `(value: any, data: DATA_TYPE) => any` - a function that can be used to decide the bucket where each data object from the data set will be placed. If not provided, the `field` value will be used. ### InfiniteTableRowInfo > Type for `rowInfo` object representing rows in the table. See [Using RowInfo](https://infinite-table.com/docs/learn/rows/using-row-info.md) for more details. The type is generic, and the generic type parameter is the type of the data in the grid. In this documentation, either `DATA_TYPE` or `T` will be used to refer to the generic type parameter. Many methods in Infinite Table are called with `rowInfo` objects that are typed to [`InfiniteTableRowInfo`](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo). (see [`columns.style`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.style), [`rowStyle`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowStyle), [`rowClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowClassName), [`persistEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#persistEdit), [`onEditAccepted`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditAccepted) and many others) This is a discriminated type, based on the `dataSourceHasGrouping` boolean property and the `isGroupRow` boolean property. This means that the type of the object will change based on the value of those properties. ```ts export type InfiniteTableRowInfo = // dataSourceHasGrouping = false, isGroupRow = false | InfiniteTable_NoGrouping_RowInfoNormal; // dataSourceHasGrouping = true, isGroupRow = false | InfiniteTable_HasGrouping_RowInfoNormal // dataSourceHasGrouping = true, isGroupRow = true | InfiniteTable_HasGrouping_RowInfoGroup // tree scenarios - leaf node | InfiniteTable_Tree_RowInfoLeafNode // tree scenarios - parent node | InfiniteTable_Tree_RowInfoParentNode; ``` The common properties of the type (in all discriminated cases) are: - `id` - the primary key of the row, as retrieved using the [`idProperty`](https://infinite-table.com/docs/reference/datasource-props/index.md#idProperty) prop. - `indexInAll` - the index in all currently visible rows. - `rowSelected` - whether the row is selected or not - `boolean | null`. - `rowDisabled` - whether the row is disabled or not - `boolean`. ### InfiniteTable_NoGrouping_RowInfoNormal This type has `dataSourceHasGrouping` set to `false` and `isGroupRow` set to `false`. Additional properties to the ones already mentioned above: - `data` - the data for the underlying row, of type `DATA_TYPE`. - `isGroupRow` - `false` - `isTreeNode` - `false` - `dataSourceHasGrouping` - `false` - `selfLoaded` - `boolean` - useful when lazy loading is configured. ### InfiniteTable_HasGrouping_RowInfoNormal This type has `dataSourceHasGrouping` set to `true` and `isGroupRow` set to `false`. So we're in a scenario where grouping is configured via [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy), but the current row is not a group row. Additional properties this type exposes: - `data` - the data for the underlying row, of type `DATA_TYPE`. - `dataSourceHasGrouping` - `true` - `isGroupRow` - `false` - `isTreeNode` - `false` - `indexInGroup` - type: `number`. The index of the row in its parent group. - `groupKeys` - type: `any[]`, but usually it's actually `string[]`. For normal rows, the group keys will have all the keys starting from the topmost parent down to the last group row in the hierarchy (the direct parent of the current row). - `groupBy` - type `(keyof T)[]`. Has the same structure as groupKeys, but it will contain the fields used to group the rows. - `rootGroupBy` - type `(keyof T)[]`. The groupBy value of the DataSource component, mapped to the `groupBy.field` - `parents` - a list of `rowInfo` objects that are the parents of the current row. - `indexInParentGroups[]` - type: `number[]`. See below for an example - `groupCount` - type: `number`. The count of leaf rows that the current group (in this case, the parent group) contains - `groupNesting` - type `number`. The nesting of the parent group. - `collapsed` - type `boolean`. - `selfLoaded` - type: `boolean`. Useful in lazy-loading scenarios, when there is batching present. If you're not in such a scenario, the value will be `false`. ### InfiniteTable_HasGrouping_RowInfoGroup This type has `dataSourceHasGrouping` set to `true` and `isGroupRow` set to `true`. So we're in a scenario where grouping is configured via [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) and the current row is a group row. Additional properties this type exposes: - `data` - the data for the underlying row, of type `Partial | null`. If there are [aggregations configured](https://infinite-table.com/docs/learn/grouping-and-pivoting/group-aggregations.md), then `data` will be an object that contains those aggregated values (so the shape of the object will be `Partial`). When no aggregations, `data` will be `null` - `dataSourceHasGrouping` - `true` - `isGroupRow` - `true` - `isTreeNode` - `false` - `error` - type: `string?`. If there was an error while loading the group (when the group row is expanded), this will contain the error message. If the group row was loaded with the `cache: true` flag sent in the server response, the error will remain on the `rowInfo` object even when you collapse the group row, otherwise, if `cache: true` was not present, the `error` property will be removed on collapse. - `indexInGroup` - type: `number`. The index of the row in the its parent group. - `deepRowInfoArray` - an array of `rowInfo` objects. This array contains all the (uncollapsed, so visible) row infos under this group, at any level of nesting, in the order in which they are visible in the table. - `reducerResults` - type `Record`. The result of the [aggregation reducers](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) for each field in the [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) prop. - `groupCount` - type: `number`. The count of leaf rows that the current group (in this case, the parent group) contains - `groupData` - type: `DATA_TYPE[]`. The array of the data of all leaf nodes (normal nodes) that are inside this group. ### InfiniteTable_Tree_RowInfoBase The base type for row nodes when using the `` component. - `nodePath`: `any[]` - the path for the current row info - `isTreeNode`: `true` - `isParentNode`: `boolean` - `indexInParent`: `number` - `treeNesting`: `number` - the nesting level of the current node. ### InfiniteTable_Tree_RowInfoParentNode The type used for parent nodes in tree scenarios. In addition to the properties already available via `InfiniteTable_Tree_RowInfoBase`, it adds the following properties: - `isParentNode` - `true` - `isTreeNode` - `true` - `totalLeafNodesCount` - `number` - `collapsedLeafNodesCount` - `number` ### InfiniteTable_Tree_RowInfoLeafNode The type used for leaf nodes in tree scenarios. In addition to the properties already available via `InfiniteTable_Tree_RowInfoBase`, it adds the following properties: - `isParentNode` - `false` - `isTreeNode` - `true` --- # Releases > All releases | Infinite Table DataGrid for React Canonical page: https://infinite-table.com/docs/releases/ ## 9.0.0-canary - 15.07.2026 First Vue release ## 8.0.5 — 06.07.2026 Fix [column drag issue](https://github.com/infinite-table/infinite-react/issues/288) ## 8.0.4 — 08.04.2026 Add `useDataSourceApi`, `useInfiniteTableApi` and `useInfiniteColumnApi` hooks ## 8.0.3 — 03.04.2026 Improvements to the drag and drop implementation. ## 8.0.0 Perf improvements by refactoring the usage of React context. Dramatically improve scrolling performance in horizontal layout scenarios, by avoiding React `flushSync` operations. ## 7.5.1 Minor fixes ## 7.5.0 Improve perf for data updates by 35% in certain scenarios. Previously, updating a row would re-render the whole visible viewport. Starting with this version, only the updated row will re-render. Improve scrolling performance generally, by a lot, by doing async updates. For scrolling in a horizontal layout configuration, the scrolling flushes cells synchronously. ## 7.4.2 Improve perf by leveraging React async flushing whenever possible. ## 7.4.1 Minor enhancement on the `debug` export ## 7.4.0 Add custom tracks in Chrome DevTools performance profiler. Read [our blogpost on custom tracks in Chrome DevTools performance profiler](https://infinite-table.com/blog/2025/10/20/debugging-your-datagrid-performance-with-custom-tracks-in-chrome-devtools-performance-profiler.md) ## 7.3.6 Add `InfiniteTable.Body.rowHoverClassName` to allow customizing the CSS `className` applied to cells on hover. ## 7.3.3 Add CSS layer `infinite-table` to all CSS styles we provide in our CSS files. ## 7.3.2 Add support for `className` in `colTypes`. ## 7.3.1 Improve TreeSelectionState standalone usage by adding config for strict mode. ## 7.3.0 Fix DataGrid virtualization issues in React 18 and above, caused by batched updates Improve tree selection when there is an external filter Starting with this version, the minimum React version is `18`. ## 7.2.4 Minor bugfixes ## 7.2.3 Fix listening to logs from the `debug` fn that Infinite exports, so we only listen to own logs. This avoids errors circular stringification errors of log messages. ## 7.2.2 Improve `TreeApi` with `getSelectedLeafNodePaths` method and other related methods. ## 7.2.1 Fix usage of React context, to be backwards compatible to React 18. This prevented the component from rendering in React 18 (affected version was 7.2.0). ## 7.2.0 Release GroupingToolbar - a new way to interact with your grouping. Fix error that appeared when column pinning was used and no visible columns were available. ## 7.1.0 Support for React 19 - update source-code, fix tests and update typings to work with React 19 ## 7.0.1 Bugfix related to devtools Uncaught TypeError: Cannot read properties of undefined (reading 'startsWith') ## 7.0.0 First version that supports Infinite Table devtools. ## 6.2.11 Improve performance on heavy scrolling by avoiding CSS vars for scroll pos. ## 6.2.10 Recompute DataSource repeat wrapped group rows when `wrapRowsHorizontally` changes. ## 6.2.9 Fix edge case where `repeatWrappedGroupRows` didn't work in trees for collapsed non-leaf nodes. When a non-leaf node was collapsed, if it was the first node in a column set, it didn't repeat its parents correctly. Version `6.2.9` fixes this. ## 6.2.8 Minor bugfix. ## 6.2.6 Consolidate themes ## 6.2.2 Update shadcn theme to work well with the latest tailwind 4 colors. ## 6.2.0 Starting with this release, the CSS for each theme (other than the `"default"`) is not included in the root CSS file (`@infinite-table/infinite-react/index.css`) and has to be imported explicitly: ```ts import '@infinite-table/infinite-react/theme/shadcn.css' import '@infinite-table/infinite-react/theme/balsam.css' import '@infinite-table/infinite-react/theme/minimalist.css' import '@infinite-table/infinite-react/theme/ocean.css' ``` ## 6.1.1 Fix performance regressions introduced in 6.1.0. Add [Keyboard Navigation API](https://infinite-table.com/docs/reference/keyboard-navigation-api/index.md) ## 6.1.0 This release includes a refactor of the core virtualization algorithm, which should result some performance improvements in certain scenarios. @milestone id="142" ## 6.1.0-canary.0 Fix virtualization issues. ## 6.0.20 ## 6.0.19 @milestone id="141" ## 6.0.18 @milestone id="140" ## 6.0.16 @milestone id="139" ## 6.0.15 @milestone id="138" ## 6.0.13 @milestone id="136" ## 6.0.12 @milestone id="135" ## 6.0.10 @milestone id="134" ## 6.0.9 @milestone id="133" ## 6.0.8 @milestone id="132" ## 6.0.5 @milestone id="131" ## 6.0.0 @milestone id="130" ## 5.0.5 @milestone id="129" ## 5.0.4 @milestone id="128" ## 5.0.1 @milestone id="127" ## 5.0.0 Add support for horizontal layout. ## 4.4.1 ## 4.4.0 @milestone id="125" ## 4.3.7 @milestone id="124" ## 4.3.2 @milestone id="123" ## 4.3.0 Fix major lazy loading bugs and issues. ## 4.2.0 Replace `sortMode` with [shouldReloadData.sortInfo](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) Replace `groupMode` with [shouldReloadData.groupBy](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.groupBy) @milestone id="122" ## 4.1.0 @milestone id="121" ## 4.0.0 @milestone id="120" ## 3.3.4 @milestone id="119" ## 3.3.3 @milestone id="118" ## 3.3.0 @milestone id="117" ## 3.2.11 @milestone id="116" ## 3.2.6 @milestone id="115" ## 3.2.5 @milestone id="114" ## 3.2.3 @milestone id="112" ## 3.2.0 @milestone id="111" ## 3.1.5 @milestone id="109" ## 3.1.1 @milestone id="107" ## 3.1.0 @milestone id="106" ## 3.0.15 @milestone id="105" ## 3.0.12 @milestone id="104" ## 3.0.10 @milestone id="102" ## 3.0.9 @milestone id="101" ## 3.0.7 @milestone id="100" ## 3.0.4 @milestone id="99" ## 3.0.3 @milestone id="98" ## 3.0.1 @milestone id="97" ## 3.0.0 @milestone id="96" ## 2.0.8 @milestone id="95" ## 2.0.4 🚀 @milestone id="93" ## 2.0.3 🚀 @milestone id="92" ## 2.0.2 🚀 @milestone id="91" ## 2.0.0 🚀 This release, although a major one, does not introduce new major functionality, but rather improves on existing features and more specifically adds support for sorting group columns. #### Improved group column sorting Version `2.0.0` allows you to make group columns sortable, even when they are configured with `groupBy` fields that are not actually bound to columns. ```tsx groupBy={[ // those fields are not bound to actual columns {field: 'team'}, {field: 'age' }, ]}> groupColumn={{ sortType: ['string', 'number'], // <--- allows you to have // the group column sortable }} ... /> ``` ### Updated column sortable behavior We've also introduced a few new props and renamed `column.sortable` to [`columns.defaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultSortable). Also, the behavior for the [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) prop has changed. The new [`columnDefaultSortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultSortable) is now what [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) used to be, while the [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) prop overrides any sorting flags and is the ultimate source of truth for column sorting. @milestone id="90" ## 1.3.23 🚀 @milestone id="89" ## 1.3.22 🚀 @milestone id="88" ## 1.3.21 🚀 @milestone id="87" ## 1.3.20 🚀 @milestone id="86" ## 1.3.17 🚀 @milestone id="85" ## 1.3.15 🚀 @milestone id="84" ## 1.3.12 🚀 @milestone id="83" ## 1.3.8 🚀 @milestone id="82" ## 1.3.7 🚀 @milestone id="81" ## 1.3.6 🚀 @milestone id="80" ## 1.3.4 🚀 @milestone id="79" ## 1.3.2 🚀 @milestone id="78" ## 1.3.0 🚀 @milestone id="77" ## 1.2.5 🚀 @milestone id="76" ## 1.2.4 🚀 @milestone id="75" ## 1.2.3 🚀 @milestone id="74" ## 1.2.2 🚀 @milestone id="73" ## 1.2.1 🚀 @milestone id="72" ## 1.2.0 🚀 @milestone id="71" ## 1.1.0 🚀 @milestone id="70" ## 1.0.0 🚀 @milestone id="69" ## 0.9.0 🚀 @milestone id="67" ## 0.8.1 🚀 @milestone id="66" ## 0.8.0 🚀 @milestone id="65" ## 0.7.3 🚀 @milestone id="64" ## 0.7.1 🚀 @milestone id="64" ## 0.7.0 🚀 @milestone id="63" ## 0.6.4 🚀 @milestone id="62" ## 0.6.3 🚀 @milestone id="61" ## 0.6.2 🚀 @milestone id="60" ## 0.6.1 🚀 @milestone id="59" ## 0.6.0 🚀 @milestone id="58" ## 0.4.12 🚀 @milestone id="56" ## 0.4.10 🚀 @milestone id="54" ## 0.4.9 🚀 @milestone id="53" ## 0.4.8 🚀 @milestone id="52" ## 0.4.7 🚀 @milestone id="51" ## 0.4.6 🚀 @milestone id="50" ## 0.4.5 🚀 @milestone id="49" ## 0.4.4 🚀 @milestone id="48" ## 0.4.3 🚀 @milestone id="47" ## 0.4.1 🚀 @milestone id="45" ## 0.4.0 🚀 @milestone id="44" ## 0.3.22 🚀 @milestone id="43" ## 0.3.21 🚀 @milestone id="42" ## 0.3.20 🚀 @milestone id="41" ## 0.3.19 🚀 @milestone id="40" ## 0.3.17 🚀 @milestone id="39" ## 0.3.16 🚀 @milestone id="38" ## 0.3.15 🚀 @milestone id="37" ## 0.3.14 🚀 @milestone id="36" ## 0.3.13 🚀 @milestone id="35" ## 0.3.12 🚀 @milestone id="34" ## 0.3.11 🚀 @milestone id="33" ## 0.3.10 🚀 @milestone id="32" ## 0.3.7 🚀 @milestone id="31" ## 0.3.6 🚀 @milestone id="30" ## 0.3.4 🚀 @milestone id="29" ## 0.3.3 🚀 @milestone id="28" ## 0.3.2 🚀 @milestone id="27" ## 0.3.1 🚀 @milestone id="26" Rename `rowInfo.flatRowInfoArray` to `rowInfo.deepRowInfoArray` ## 0.3.0 🚀 @milestone id="25" ## 0.3.0-canary.0 🚀 New virtualization engine implemented for better performance. ## 0.2.20 🚀 @milestone id="24" ## 0.2.18 🚀 @milestone id="22" ## 0.2.17 🚀 @milestone id="21" ## 0.2.16 🚀 @milestone id="20" ## 0.2.15 🚀 @milestone id="19" ## 0.2.14 🚀 @milestone id="18" ## 0.2.13 🚀 @milestone id="17" ## 0.2.12 🚀 @milestone id="16" ## 0.2.11 🚀 @milestone id="15" ## 0.2.10 🚀 @milestone id="14" ## 0.2.9 🚀 @milestone id="13" ## 0.2.8 🚀 @milestone id="12" ## 0.2.7 🚀 @milestone id="11" ## 0.2.6 🚀 @milestone id="9" ## 0.2.5 🚀 @milestone id="8" ## 0.2.4 🚀 @milestone id="7" ## 0.2.0 🚀 - Implement initial support for [server-side pivoting](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md) ## 0.1.0 🚀 This release introduces several breaking changes: - `DataSource.groupRowsBy` has been renamed to [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) - `InfiniteTable.columnAggregations` has been removed and you have to use [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) @milestone id="5" ## 0.0.10 🚀 @milestone id="4" ## 0.0.9 🚀 @milestone id="3" ## 0.0.7 🚀 @milestone id="2" ## 0.0.5 🚀 @milestone id="1" --- # Version 1 Canonical page: https://infinite-table.com/docs/releases/v1 ## 1.0.0 🚀 @milestone id="60" --- # Building a file explorer TreeGrid in React > Use Infinite Table TreeGrid and TreeDataSource to render nested data with expand state, selection, and custom tree icons. Published: 2026-07-13 Author: radu Tags: tree-grid, tree-data, react-datagrid Canonical page: https://infinite-table.com/blog/2026/07/13/building-a-file-explorer-treegrid-in-react An important use-case for Infinite Table is handling tree data. There's a log of scenarios where you need handling hierarchical data: product catalogs with categories, file managers, CRMs, permissions screens often need to show resources nested under resources. Infinite Table's [TreeGrid docs](https://infinite-table.com/docs/learn/tree-grid/overview.md) cover this shape of data with two dedicated components: `` and ``. They give tree data its own types and state model while keeping the rest of the DataGrid the same - same columns, styling, sizing, and virtualization patterns you already use in the regular DataGrid. ## Start with nested data For tree data, use `` instead of ``, and `` instead of ``. ```tsx {1,2} ``` The `nodesKey` prop tells the data source where child nodes live on each data item. In the docs, the examples use a file-system shape where folders contain a `children` array and files are leaf nodes. ```tsx {6,12} const dataSource = [ { id: '1', name: 'Documents', type: 'folder', children: [ { id: '10', name: 'Report.docx', type: 'file', }, ], }, ]; ``` Once the data has a nested structure, choose the column that should render the expand/collapse affordance by setting [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon). ```tsx {4} const columns = { name: { field: 'name', renderTreeIcon: true, }, }; ``` With this setup, you're already good to go and have an interactive tree grid. **Example: Basic TreeGrid example** This example is reused from the TreeGrid docs. Expand and collapse folders to inspect the nested file-system data. ```tsx import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', renderTreeIcon: true, header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', mimeType: 'text/plain', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ## Treat nodes by path, not only by id The TreeGrid docs use the term `node` for a rendered item/row in the tree, and `node path` for the route from the root node to the current node. The top-level of your `TreeDataSource` doesn't need to be only one item - you can have multiple roots that are siblings to each other. ```tsx title="Node path example" const data = [ { id: '1', // path: ['1'] name: 'Documents', children: [ { id: '10', // path: ['1', '10'] name: 'Private', children: [ { id: '100', // path: ['1', '10', '100'] name: 'Report.docx', }, ], }, ], }, { id: '2', // path: ['2'] name: 'Media', children: [ { id: '20', // path: ['2','20'] name: 'FamilyTrip.mpeg' } ] } ]; ``` Paths are important because tree UIs often need to preserve state at a specific location in the hierarchy. The same node id could be meaningful in different branches, while a node path describes the exact branch the user interacted with. ## Restore expand and collapse state By default, the TreeGrid renders all nodes expanded. For product UIs, you will often want a more intentional starting point: expand the current project, collapse archived folders, or restore the state a user saved in a previous session. Use [`defaultTreeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeExpandState) for an initial uncontrolled value: ```tsx const defaultTreeExpandState = { defaultExpanded: true, collapsedPaths: [ ['1', '10'], ['3', '31'], ], expandedPaths: [['3']], }; ; ``` For fully controlled state, use [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState) together with [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange). The same docs page also shows the imperative Tree API methods such as [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) and [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll). **Example: Tree expand and collapse state** This demo starts with a custom expand state and exposes buttons that call the Tree API to expand or collapse all nodes. ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; const defaultTreeExpandState: TreeExpandStateValue = { defaultExpanded: true, collapsedPaths: [ ['1', '10'], ['3', '31'], ], expandedPaths: [['3']], }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ## Add tree-aware selection Selection works at the tree level too. Configure [`defaultTreeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeSelection) or [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection) on ``, and render a checkbox in the tree column with [`columns.renderSelectionCheckBox`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox). ```tsx {2,7} const defaultTreeSelection = { defaultSelection: true, deselectedPaths: [ ['1', '10'], ['3', '31'], ], selectedPaths: [['3']], }; const columns = { name: { field: 'name', renderTreeIcon: true, renderSelectionCheckBox: true, }, }; ``` This include/exclude shape scales well for large trees because you can describe "everything is selected except these branches" or "nothing is selected except these branches" without enumerating every leaf node. **Example: Tree selection with checkboxes** Select and deselect branches, then use the buttons above the grid to call the Tree API for all nodes. ```tsx import { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, TreeSelectionValue, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; const defaultTreeSelection: TreeSelectionValue = { defaultSelection: true, deselectedPaths: [ ['1', '10'], ['3', '31'], ], selectedPaths: [['3']], }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState | null>(); return ( <>
); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); }; ``` ## Customize the tree icon The default expand/collapse icon is enough for many apps, but file explorers, permission editors, and navigation builders usually need stronger visual signals. The [tree icon docs](https://infinite-table.com/docs/learn/tree-grid/tree-icon-rendering.md) show two useful levels of customization: - set `--infinite-expand-collapse-icon-color` to recolor the default icon - provide a function to [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) when you want custom icons for both parent and leaf nodes When `renderTreeIcon` is a function, it receives tree-specific row information. Check `rowInfo.isParentNode` before reading parent-only state such as `rowInfo.nodeExpanded`. ```tsx title="Rendering a custom tree icon" const renderTreeIcon = ({ rowInfo, toggleCurrentTreeNode }) => { if (!rowInfo.isParentNode) { return ; } return ( ); }; ``` ## When to reach for TreeGrid Use TreeGrid when the hierarchy is part of the data model: - file-system or document-library UIs - organization charts and team member lists - product catalogs with nested categories - permission editors with resources and sub-resources - project plans with parent tasks and child tasks Use regular row grouping when the hierarchy is derived from flat data. Use TreeGrid when the hierarchy already exists in the records you load. Start with the [TreeGrid overview](https://infinite-table.com/docs/learn/tree-grid/overview.md), then continue with [expand/collapse state](https://infinite-table.com/docs/learn/tree-grid/tree-expand-and-collapse-state.md), [tree selection](https://infinite-table.com/docs/learn/tree-grid/tree-selection.md), and [custom tree icons](https://infinite-table.com/docs/learn/tree-grid/tree-icon-rendering.md) depending on the interaction your app needs. --- # Building pivoted React DataGrids with generated columns > Turn grouped data into cross-tab reports with Infinite Table pivoting, generated columns, custom pivot headers, totals, and server-side pivot loading. Published: 2026-07-10 Author: radu Tags: pivoting, grouping, analytics Canonical page: https://infinite-table.com/blog/2026/07/10/building-pivoted-react-datagrids-with-generated-columns Pivot tables start as a reporting request and quickly become a UI architecture problem. You need to group rows, aggregate values, create columns from data values, keep those columns typed, and still leave room to customize headers, widths, formatting, totals, and server-side loading. Infinite Table's pivoting feature keeps that work inside the `DataSource` pipeline so your React grid can render a report-style layout without hardcoding every possible result column. The docs cover this in the [pivoting guide](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md). This article walks through the key idea — define pivoting at the data level, then render the generated columns in `` — and maps it to real product scenarios. ## Pivoting belongs in the DataSource In Infinite Table, the `DataSource` owns grouping, pivoting, and aggregation because those features reshape the data before the grid renders it. ```tsx const groupBy = [{ field: 'department' }, { field: 'country' }]; const pivotBy = [{ field: 'team' }]; groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={aggregationReducers} > {({ pivotColumns, pivotColumnGroups }) => { return ( columns={columns} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} /> ); }}
; ``` The `children` render prop of the `DataSource` is the handoff for a pivot DataGrid. The `DataSource` looks at [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) and [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers), groups and aggregates the data, then gives the table the generated `pivotColumns` and `pivotColumnGroups`. Without that split, you would scan the dataset yourself, discover every pivot value, construct matching columns, wire column groups, and keep the result in sync as grouping or pivoting changes. ## Map a business question to pivot config A useful way to design a pivot view is to write the question first, then fill in three lists: - **Rows** (`groupBy`) — what you compare down the page: region, department, product line - **Columns** (`pivotBy`) — what becomes generated headers: month, channel, plan tier - **Values** (`aggregationReducers`) — what you measure: sum of revenue, avg salary, ticket count If the column headers depend on values in the data — countries that appear this quarter, warehouses that currently hold stock, channels that received tickets — you want generated pivot columns, not a static column list. ## Scenario 1: People analytics — language × stack, pivoted by country Imagine a developers analytics grid. Rows are grouped by `preferredLanguage` and `stack`, then pivoted by `country` and whether a developer can design. Instead of one salary column, the grid shows aggregated salary for each country/design combination. That is the classic “compare compensation across geography while keeping role structure on the rows” report — the same shape as headcount or average salary by department and location. The demo below groups developers, pivots the salary aggregation into generated columns, and starts with groups collapsed so the cross-tab is easy to scan. **Example: Pivoting with generated columns** Expand a group to see aggregated salary values distributed across generated pivot columns. Full walkthrough: [pivoting overview](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md). ```tsx import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourcePropAggregationReducers, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data) .then( (data) => new Promise((resolve) => { setTimeout(() => resolve(data), 1000); }), ); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, field: 'salary', reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const reducers: DataSourcePropAggregationReducers = { salary: avgReducer, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function GroupByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={reducers} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivoting-example" columns={columns} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={200} pivotTotalColumnPosition="end" /> ); }}
); } ``` In product terms, the same shape answers questions like: - Average salary by technology stack, broken out by country - Headcount by department, broken out by office - Utilization by team, broken out by project phase ## Scenario 2: Support ops — volume by priority × channel Support and customer-success teams often need a matrix, not a flat ticket list: - **Rows:** priority (`critical`, `high`, `normal`) or product area - **Columns:** channel (`email`, `chat`, `phone`, `portal`) - **Values:** ticket count, average time-to-first-response, reopen rate ```tsx const groupBy = [{ field: 'priority' }, { field: 'productArea' }]; const pivotBy = [{ field: 'channel' }]; const aggregationReducers = { ticketCount: { name: 'Tickets', initialValue: 0, reducer: (acc) => acc + 1, }, avgFirstResponseMins: { name: 'Avg first response (min)', field: 'firstResponseMins', initialValue: 0, reducer: (acc, value) => acc + value, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }, }; ``` As new channels appear in the feed, Infinite Table generates matching pivot columns. You do not ship a release every time marketing adds “WhatsApp” as a support channel. The same pattern works for sales pipeline stages by region, or incidents by severity and service. ## Scenario 3: Commerce — revenue by product line × region (with totals) Finance and merchandising reports usually need both the matrix and the summary columns: - **Rows:** product category → SKU family - **Columns:** region → sales channel - **Values:** sum of revenue, sum of units That is where [`pivotTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotTotalColumnPosition) and [`pivotGrandTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotGrandTotalColumnPosition) matter: - Pivot totals summarize inside a pivot group (for example, all channels under EMEA) - Grand totals summarize across the whole pivot surface for each aggregation Pivot total columns only appear when you pivot by two or more fields (`pivotBy.length > 1`). With a single pivot field, enabling [`pivotTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotTotalColumnPosition) has no effect — those totals would duplicate the values already shown. The example below uses two pivot levels (`stack` and `canDesign`) so both kinds of totals are visible. ```tsx const pivotBy = [{ field: 'stack' }, { field: 'canDesign' }]; columns={columns} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} pivotTotalColumnPosition="end" pivotGrandTotalColumnPosition="start" /> ``` **Example: Pivot totals and grand-total columns** Pivot total columns sit at the end of each stack group. Grand-total columns are placed at the start of the grid. ```tsx import { InfiniteTable, DataSource, DataSourceGroupBy, DataSourcePivotBy, InfiniteTableColumnAggregator, DataSourcePropAggregationReducers, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { id: { field: 'id', defaultWidth: 80 }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, }; const defaultGroupBy: DataSourceGroupBy[] = [ { field: 'country', }, { field: 'city', }, ]; const defaultPivotBy: DataSourcePivotBy[] = [ { field: 'stack', }, { field: 'canDesign', columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer', }; }, }, ]; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => Math.round(arr.length ? sum / arr.length : 0), }; const aggregations: DataSourcePropAggregationReducers = { salary: { ...avgReducer, name: 'Salary (avg)', field: 'salary', }, age: { ...avgReducer, name: 'Age (avg)', field: 'age', }, }; export default function ColumnValueGetterExample() { return ( <> primaryKey="id" defaultGroupBy={defaultGroupBy} defaultPivotBy={defaultPivotBy} aggregationReducers={aggregations} data={dataSource} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivot-grand-total-column-position-example" groupRenderStrategy="single-column" columns={columns} columnDefaultWidth={200} pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} pivotTotalColumnPosition="end" pivotGrandTotalColumnPosition="start" /> ); }}
); } ``` For a commerce grid, that reads as: revenue for each region/channel cell, a subtotal per region, and a grand total for the selected filters — spreadsheet expectations, without building spreadsheet UI yourself. ## Scenario 4: Inventory — warehouse × status, readable headers Generated columns still need to feel like part of your app. Infinite Table gives you a few customization layers. ### Inherit formatting from source columns If an aggregation reducer is bound to a field that already has a column, the generated pivot column inherits that column configuration — number formatting, styles, default width, and related behavior. ```tsx const columns: InfiniteTablePropColumns = { salary: { field: 'salary', type: 'number', style: { color: 'red' }, }, }; const aggregationReducers = { avgSalary: { field: 'salary', reducer: 'avg', }, }; ``` **Example: Generated pivot columns inherit column config** Aggregations bound to `salary` / `age` pick up the original column styling. See also [customizing pivot columns](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/customizing-pivot-columns.md). ```tsx import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropAggregationReducers, InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const columnAggregations: DataSourcePropAggregationReducers = { avgSalary: { field: 'salary', name: 'Average salary', ...avgReducer, }, avgAge: { field: 'age', ...avgReducer, pivotColumn: { defaultWidth: 500, inheritFromColumn: 'firstName', }, }, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName', style: { fontWeight: 'bold', }, renderValue: ({ value }) => <>{value}!, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', header: 'Salary', style: { color: 'red' }, }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function PivotByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={columnAggregations} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivot-column-inherit-example" columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={180} /> ); }}
); } ``` ### Rename raw values into business labels Inventory status codes (`in_stock`, `reserved`, `quarantine`) or boolean flags rarely belong in a header as-is. Use [`pivotBy.column`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy.column) as an object for every generated column at that pivot level, or as a function that receives the generated column metadata. ```tsx const pivotBy: DataSourcePivotBy[] = [ { field: 'country' }, { field: 'canDesign', column: ({ column }) => { const lastKey = column.pivotGroupKeys[column.pivotGroupKeys.length - 1]; return { header: lastKey === 'yes' ? 'Designer' : 'Non-designer', }; }, }, ]; ``` That callback turns raw pivot keys into labels operators recognize, while column generation stays automatic. **Example: Custom headers on generated pivot columns** The `canDesign` pivot values are rewritten to Designer / Non-designer headers. Try collapsing and expanding groups to see the column groups stay aligned. ```tsx import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { DataSourcePropAggregationReducers, InfiniteTableColumnAggregator, InfiniteTablePropColumns, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; const avgReducer: InfiniteTableColumnAggregator = { initialValue: 0, field: 'salary', reducer: (acc, sum) => acc + sum, done: (sum, arr) => (arr.length ? sum / arr.length : 0), }; const columnAggregations: DataSourcePropAggregationReducers = { salary: avgReducer, }; const columns: InfiniteTablePropColumns = { id: { field: 'id' }, firstName: { field: 'firstName' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, country: { field: 'country' }, canDesign: { field: 'canDesign' }, hobby: { field: 'hobby' }, city: { field: 'city' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number' }, currency: { field: 'currency' }, }; const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function PivotByExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'preferredLanguage', }, { field: 'stack' }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'country' }, { field: 'canDesign', column: ({ column: pivotCol }) => { const lastKey = pivotCol.pivotGroupKeys[pivotCol.pivotGroupKeys.length - 1]; return { header: lastKey === 'yes' ? '💅 Designer' : '💻 Non-designer', }; }, }, ], [], ); return ( <> primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={columnAggregations} defaultGroupRowsState={groupRowsState} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="pivoting-customize-column-example" columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={180} /> ); }} ); } ``` A warehouse dashboard might look like: ```tsx const groupBy = [{ field: 'skuFamily' }, { field: 'sku' }]; const pivotBy = [ { field: 'warehouse' }, { field: 'status', column: ({ column }) => { const status = column.pivotGroupKeys[column.pivotGroupKeys.length - 1]; const labels: Record = { in_stock: 'Available', reserved: 'Reserved', quarantine: 'Quarantine', }; return { header: labels[status] ?? String(status), defaultWidth: 120, }; }, }, ]; const aggregationReducers = { units: { name: 'Units', field: 'quantity', initialValue: 0, reducer: (acc, value) => acc + value, }, }; ``` New warehouses in the dataset become new column groups without a frontend schema change. ## Scenario 5: SaaS metrics — plans and regions at scale Product analytics often outgrows client-side reshaping: millions of subscription events, weekly cohorts, plan × region matrices. For that case, keep the same Infinite Table UI model and move the pivot computation to the server. Enable [`lazyLoad`](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad) and provide a function for [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) that returns already-pivoted groups. The grid still generates `pivotColumns` / `pivotColumnGroups` from [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) and [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers), but leaf rows are not loaded — pivoting works on aggregated group payloads. ```tsx const groupBy = [{ field: 'country' }, { field: 'stack' }]; const pivotBy = [ { field: 'preferredLanguage' }, { field: 'canDesign' }, ]; const aggregationReducers = { salary: { name: 'Salary (avg)', field: 'salary', reducer: 'avg' }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg' }, }; const dataSource = ({ groupBy, pivotBy, groupKeys, aggregationReducers }) => { // Fetch a Promise that resolves to { data, totalCount, pivot, ... } // See the pivoting docs for the full response shape. }; ; ``` Typical SaaS mappings: - **MRR by segment and plan** — `groupBy`: `segment` → `accountTier`; `pivotBy`: `plan` → `billingInterval`; aggregations: `sum(mrr)`, `count(accounts)` - **Activation by acquisition channel** — `groupBy`: `week` → `cohort`; `pivotBy`: `channel`; aggregation: `avg(activationRate)` - **Churn by region and product** — `groupBy`: `region`; `pivotBy`: `product` → `plan`; aggregation: `sum(churnedMrr)` **Example: Server-side pivoting** Grouping and pivot aggregations are loaded remotely. Expand countries to fetch nested groups with pivot values already computed on the server. ```tsx import { InfiniteTable, DataSource, GroupRowsState, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns, InfiniteTablePropColumnPinning, DataSourceData, DataSourcePropAggregationReducers, DataSourceGroupBy, DataSourcePivotBy, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const DATA_SOURCE_SIZE = '10k'; const dataSource: DataSourceData = ({ pivotBy, aggregationReducers, groupBy, groupKeys = [], }) => { const args = [ pivotBy ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) : null, `groupKeys=${JSON.stringify(groupKeys)}`, groupBy ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) : null, aggregationReducers ? 'reducers=' + JSON.stringify( Object.keys(aggregationReducers).map((key) => ({ field: aggregationReducers[key].field, id: key, name: aggregationReducers[key].reducer, })), ) : null, ] .filter(Boolean) .join('&'); return fetch( process.env.NEXT_PUBLIC_BASE_URL + `/developers${DATA_SOURCE_SIZE}-sql?` + args, ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const aggregationReducers: DataSourcePropAggregationReducers = { salary: { // the aggregation name will be used as the column header name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; const columns: InfiniteTablePropColumns = { preferredLanguage: { field: 'preferredLanguage' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, country: { field: 'country' }, firstName: { field: 'firstName' }, stack: { field: 'stack' }, id: { field: 'id' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; // make the row labels column (id: 'labels') be pinned const defaultColumnPinning: InfiniteTablePropColumnPinning = { // make the generated group columns pinned to start 'group-by-country': 'start', 'group-by-stack': 'start', }; // make all rows collapsed by default const groupRowsState = new GroupRowsState({ expandedRows: [], collapsedRows: true, }); export default function RemotePivotExample() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', column: { // give the group column for the country prop a custom id id: 'group-by-country', }, }, { field: 'stack', column: { // give the group column for the stack prop a custom id id: 'group-by-stack', }, }, ], [], ); const pivotBy: DataSourcePivotBy[] = React.useMemo( () => [ { field: 'preferredLanguage' }, { field: 'canDesign', // customize the column group columnGroup: ({ columnGroup }) => { return { ...columnGroup, header: `${ columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer' }`, }; }, // customize columns generated under this column group column: ({ column }) => ({ ...column, header: `🎉 ${column.header}`, }), }, ], [], ); return ( primaryKey="id" data={dataSource} groupBy={groupBy} pivotBy={pivotBy} aggregationReducers={aggregationReducers} defaultGroupRowsState={groupRowsState} lazyLoad={true} > {({ pivotColumns, pivotColumnGroups }) => { return ( debugId="remote-pivoting-example" defaultColumnPinning={defaultColumnPinning} columns={columns} hideEmptyGroupColumns pivotColumns={pivotColumns} pivotColumnGroups={pivotColumnGroups} columnDefaultWidth={220} /> ); }} ); } ``` For very large trees, the same guide covers [lazy-load batching](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md#another-pivoting-example-with-batching) so groups stream in pages while the pivot column model stays consistent. ## Scenario 6: Let users change the report at runtime Many analytics products need more than one fixed pivot. Users want to switch “group by department, pivot by month” to “group by region, pivot by product line” without a new screen. Because `groupBy`, `pivotBy`, and `aggregationReducers` are ordinary React props, you can drive them from UI state. The [dynamic pivoting example](https://infinite-table.com/docs/learn/examples/dynamic-pivoting-example.md) shows client-side and server-side variants where those dimensions change at runtime, including custom number/currency formatting on the generated columns. That is the difference between a static export and an interactive report builder: the DataSource recomputes (or re-fetches) the pivot surface; `` keeps rendering whatever `pivotColumns` it receives. ## When pivoting is a good fit Reach for pivoting when the question is not “which rows match this filter?” but “how do values compare across dimensions?” Good use cases: - Revenue by region and product line (with region/channel totals) - Headcount or average salary by department and location - Support volume by priority and channel - Inventory units by warehouse and stock status - SaaS MRR / churn by plan and billing interval - Incident count by severity and owning service In each case the column structure depends on the data. Generated pivot columns are the point: you describe dimensions and aggregations, and Infinite Table creates the report surface. ## Go deeper in the docs - [Pivoting overview](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md) — core `pivotBy` setup, totals, and server-side pivoting - [Customizing pivot columns](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/customizing-pivot-columns.md) — inheritance, headers, widths, per-value column config - [Dynamic pivoting example](https://infinite-table.com/docs/learn/examples/dynamic-pivoting-example.md) — change group/pivot/aggregations from the UI - [Grouping and aggregations](https://infinite-table.com/docs/learn/grouping-and-pivoting/group-aggregations.md) — reducer shapes that power pivot values --- # Real-time data updates in your React DataGrid > Use the Infinite Table DataSource API to stream row updates, batch changes, and keep large datasets responsive. Published: 2026-07-07 Author: radu Tags: realtime, datasource, performance Canonical page: https://infinite-table.com/blog/2026/07/07/real-time-data-updates-with-the-datasource-api Most data grids are not showing static data. Dashboards receive fresh metrics, trading screens react to price changes, logistics apps track moving assets, and admin tools often need to reflect edits made by other users. Infinite Table already has a [dedicated for for updating data in real time](https://infinite-table.com/docs/learn/working-with-data/updating-data-in-realtime.md), and this article adds more guide on how you can update the `DataSource`, not the whole grid. ## The DataSource API owns row updates The `DataSource` component is responsible for loading, processing, and preparing data for ``. When you need to change rows after the grid is mounted, use the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md). The `` component doesn't need to be a direct child of the `` You can get access to the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) from `DataSource.onReady`: ```tsx const onReady = (dataSourceApi) => { // store the dataSourceApi and use it when updates arrive }; ; ``` Or from `InfiniteTable.onReady`, where you receive both the grid API and the DataSource API: ```tsx const onReady = ({ api, dataSourceApi }) => { // api controls grid behavior // dataSourceApi controls data updates }; ; ``` ## Update one row by primary key To update a row, call `dataSourceApi.updateData` with an object that includes the configured [`primaryKey`](https://infinite-table.com/docs/reference/datasource-props/index.md#primaryKey) field. Any other fields you include are merged into the existing row data. ```tsx {1,3} dataSourceApi.updateData({ id: 42, salary: 124000, currency: 'USD', reposCount: 37, }); ``` This keeps the update local to the row data that changed. You do not need to rebuild the whole array just because one value moved. ## Update many rows without creating render noise When several rows change together, use `dataSourceApi.updateDataArray`. ```tsx {1,3,9} dataSourceApi.updateDataArray([ { id: 42, salary: 124000, currency: 'USD', }, { id: 73, salary: 118500, currency: 'EUR', }, ]); ``` The docs call out an important implementation detail: DataSource row mutations are batched by default. Multiple insert, update, or delete calls made in the same `requestAnimationFrame` resolve through the same promise and trigger one render pass. ```tsx const firstUpdate = dataSourceApi.updateData({ id: 1, salary: 115000, }); const secondUpdate = dataSourceApi.updateData({ id: 2, salary: 99000, }); firstUpdate === secondUpdate; // true ``` That batching matters when updates are frequent. It lets your app react to a stream of changes while Infinite Table keeps the rendering work grouped. ## See it with 10k rows The live demo below uses the same example from the [Live Updates docs page](https://infinite-table.com/docs/learn/examples/live-updates-example.md). It loads 10k rows and, when started, updates five rows from the visible viewport every 30ms. **Example: Real-time DataSource updates with 10k rows** Click **Start updates** to update visible rows in real time. The example uses the DataSource API to change individual rows without replacing the whole dataset. ```tsx import * as React from 'react'; import '@infinite-table/infinite-react/index.css'; import { DataSourceApi, InfiniteTable, InfiniteTableApi, InfiniteTablePropColumns, DataSource, } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; reposCount: number; }; const dataSource = () => { return fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/developers10k-sql`) .then((r) => r.json()) .then((data: Developer[]) => { return data; }); }; export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } const CURRENCIES = ['USD', 'CAD', 'EUR']; const stacks = ['frontend', 'backend', 'fullstack']; const updateRow = (api: DataSourceApi, data: Developer) => { const getDelta = (num: number): number => Math.ceil(0.2 * num); const initialData = data; if (!initialData) { return; } const salaryDelta = getDelta(initialData?.salary); const reposCountDelta = getDelta(initialData?.reposCount); const newSalary = initialData.salary + getRandomInt(-salaryDelta, salaryDelta); const newReposCount = initialData.reposCount + getRandomInt(-reposCountDelta, reposCountDelta); const newData: Partial = { id: initialData.id, salary: newSalary, reposCount: newReposCount, currency: CURRENCIES[getRandomInt(0, CURRENCIES.length - 1)] || CURRENCIES[0], stack: stacks[getRandomInt(0, stacks.length - 1)] || stacks[0], age: getRandomInt(0, 100), }; api.updateData(newData); }; const ROWS_TO_UPDATE_PER_FRAME = 5; const UPDATE_INTERVAL_MS = 30; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', style: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return {}; } return { color: 'black', background: value > 80 ? 'tomato' : value > 60 ? 'orange' : value > 40 ? 'yellow' : value > 20 ? 'lightgreen' : 'green', }; }, }, salary: { field: 'salary', type: 'number', }, reposCount: { field: 'reposCount', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default function App() { const [running, setRunning] = React.useState(false); const [apis, onReady] = React.useState<{ api: InfiniteTableApi; dataSourceApi: DataSourceApi; }>(); const intervalIdRef = React.useRef(null); React.useEffect(() => { const { current: intervalId } = intervalIdRef; if (!running || !apis) { return clearInterval(intervalId); } intervalIdRef.current = setInterval(() => { const { dataSourceApi, api } = apis!; const { renderStartIndex, renderEndIndex } = api.getVerticalRenderRange(); const dataArray = dataSourceApi.getRowInfoArray(); const data = dataArray .slice(renderStartIndex, renderEndIndex) .map((x) => x.data as Developer); for (let i = 0; i < ROWS_TO_UPDATE_PER_FRAME; i++) { const row = data[getRandomInt(0, data.length - 1)]; if (row) { updateRow(dataSourceApi, row); } } return () => { clearInterval(intervalIdRef.current); intervalIdRef.current = null; }; }, UPDATE_INTERVAL_MS); }, [running, apis]); return ( data={dataSource} primaryKey="id"> debugId="realtime-updates-example" domProps={domProps} onReady={onReady} columnDefaultWidth={130} columnMinWidth={50} columns={columns} /> ); } ``` ## When to use this pattern Reach for the DataSource API when data changes after initial load: - websocket or Server-Sent Events feeds - polling that returns changed records - optimistic updates after user edits - background imports that append or remove rows - dashboards where visible values change continuously For simple local state, replacing the `data` prop can be fine. For ongoing row-level changes, the DataSource API gives you a clearer contract: every mutation includes the primary key, and Infinite Table handles the data update pipeline. Start with the docs page on [updating data in real time](https://infinite-table.com/docs/learn/working-with-data/updating-data-in-realtime.md), then open the live updates example and adapt the update loop to your app's data source. --- # Debugging your DataGrid performance with custom tracks in Chrome DevTools > Infinite Table will now add custom tracks to Chrome DevTools performance profiler to help you debug the performance of your DataGrid Published: 2025-10-20 Author: radu Tags: devtools, performance Canonical page: https://infinite-table.com/blog/2025/10/20/debugging-your-datagrid-performance-with-custom-tracks-in-chrome-devtools-performance-profiler > We want to make sure Infinite Table is the top React DataGrid in terms of performance. And we also want to give you the right tools to track down any slowness you might find in your app. Infinite Table was the [first DataGrid with a DevTools extension](https://infinite-table.com/blog/2025/05/12/the-first-devtools-for-a-datagrid.md). Today we announce Infinite Table is the first DataGrid that gives you custom tracks in Chrome DevTools performance profiles. We've been inspired by the [React 19.2 release blogpost](https://react.dev/blog/2025/10/01/react-19-2#performance-tracks) where the React team announced the custom tracks being available in the performance profiler. So we thought, why not! To see your Infinite DataGrid instance in the Chrome DevTools profiler, make sure you specify the [`debugId`](https://infinite-table.com/docs/reference/infinite-table-props.md#debugId) prop. Basically, if you have multiple grids in your app, you'll be able to see a dedicated track for each separate instance. ```tsx {3} title="Specify the debugId prop to see the DataGrid in the Chrome Profiler" ``` Once your [`debugId`](https://infinite-table.com/docs/reference/infinite-table-props.md#debugId) prop is configured, you can hit the recording the performance profile in Chrome DevTools, and once the profile is done, you'll see something similar to the image below. ![Infinite Table DataGrid custom tracks in Chrome DevTools Performance Profiler](https://infinite-table.com/blog-images/infinite-custom-tracks-in-devtools-profiler.png) For now, we mainly show data-heavy operations, like sorting, grouping/filtering and flattening the data array, but more will be added in the near future. ```tsx import { InfiniteTable, DataSource, DataSourceData, type InfiniteTableColumn, type InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource: DataSourceData = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers10k-sql?`) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns = { firstName: { field: 'firstName' }, country: { field: 'country' }, salary: { field: 'salary', type: 'number', }, age: { field: 'age' }, id: { field: 'id' }, canDesign: { field: 'canDesign' }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, hobby: { field: 'hobby' }, city: { field: 'city' }, currency: { field: 'currency' }, }; const shouldReloadData = { sortInfo: false, groupBy: false, }; const groupColumn: InfiniteTableColumn = { defaultWidth: 200, }; export default function App() { return ( <> primaryKey="id" data={dataSource} defaultSortInfo={[ { field: 'country', dir: -1 }, { field: 'salary', dir: 1 }, ]} defaultGroupBy={[{ field: 'country' }, { field: 'preferredLanguage' }]} shouldReloadData={shouldReloadData} > groupColumn={groupColumn} groupRenderStrategy="single-column" debugId="infinite-table-example" columns={columns} columnDefaultWidth={120} /> ); } ``` The custom tracks give you high-fidelity information about the time the operation took, but give you additional insights - like the count of your data-array for sorting operations, what types of manipulations actually take place with the data and more. We're really interested to see what other insights you find useful and want us to include in the custom tracks. Reach out to us on [X](https://x.com/get_infinite) and let's start the conversation! --- # Customizing your DataGrid component with Tailwind CSS > Find out how to customize your DataGrid component to fit your app needs, using Tailwind CSS Published: 2025-10-09 Author: radu Tags: theming, customizing Canonical page: https://infinite-table.com/blog/2025/10/09/customizing-your-datagrid-component-with-tailwind We haven't spoken about this very much, but Infinite Table does offer you very powerful ways to customize the structure of your component. After all, we're a React-first DataGrid, so the component and composition patterns it offers should feel at home in a React app. ```tsx title="Default structure of InfiniteTable" ``` ## Customizing the nesting of the InfiniteTable component However, be aware that you the `` component doesn't have to be a direct child of the `` component. The `` component doesn't actually render anything, but its job is to load, process and prepare the data in a way that `` understands and can display. And actually you can use the DataSource context to gain access to the data yourself. ```tsx {4} title="InfiniteTable can be nested anywhere inside the component"

Your DataGrid

``` Inside the `` component you can use the DataSource-provided context via the [`useDataSourceState`](https://infinite-table.com/docs/reference/hooks/index.md#useDataSourceState) hook that our component exposes. ## Choosing what to render Besides the flexibility of nesting your DataGrid component anywhere in your app, we also offer you the ability to choose what parts of the DataGrid you want to render and where. Let's suppose you want to show the header after the body of the DataGrid or choose to insert something in between. That should be easy, right? **It is with Infinite!** - but try to do that with the other commercial DataGrids out there! ```tsx import { InfiniteTable, DataSource, InfiniteTableColumn, DataSourceGroupBy, components, useDataSourceState, DataSourceState, } from '@infinite-table/infinite-react'; import * as React from 'react'; const { CheckBox } = components; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: Record> = { age: { field: 'age', header: 'Age', type: 'number', defaultWidth: 100, renderValue: ({ value }) => value, }, salary: { field: 'salary', type: 'number', defaultWidth: 210, }, currency: { field: 'currency', header: 'Currency', defaultWidth: 100 }, preferredLanguage: { field: 'preferredLanguage', header: 'Programming Language', }, canDesign: { defaultWidth: 135, field: 'canDesign', header: 'Design Skills', renderValue: ({ value }) => { return (
{value === null ? 'Some' : value === 'yes' ? 'Yes' : 'No'}
); }, }, country: { field: 'country', header: 'Country', }, firstName: { field: 'firstName', header: 'First Name' }, stack: { field: 'stack', header: 'Stack' }, city: { field: 'city', header: 'City', renderHeader: ({ column }) => `${column.computedVisibleIndex} City`, }, }; export default function App() { const groupBy: DataSourceGroupBy[] = React.useMemo( () => [ { field: 'country', }, { field: 'stack' }, ], [], ); return (
data={dataSource} primaryKey="id" defaultGroupBy={groupBy} >

Your DataGrid

); } function AppGrid() { const dataLength = useDataSourceState( (state: DataSourceState) => state.dataArray.length, ); return (
groupRenderStrategy="single-column" defaultActiveRowIndex={0} columns={columns} columnDefaultWidth={150} >
Showing {dataLength} rows
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` As demoed above, the good part is that you can very easily add additional elements to your structure and have the grouping toolbar displayed on the side, vertically. ```tsx {8} title="Example structure for vertical grouping toolbar"
``` In the example above, try dragging the header of the `age` column onto the `GroupingToolbar` to add grouping by `age`. ## Usage with TailwindCSS Our Tailwind DataGrid example below shows how you can leverage [Tailwind CSS](https://tailwindcss.com/) to style the DataGrid. When setting up InfiniteTable in a TailwindCSS app, you'll need to use the CSS layer that is defined in our styles, called `'infinite-table'`. Basically, you have to update your app to list this CSS layer in the proper order. [Tailwind CSS docs on layer ordering](https://tailwindcss.com/docs/preflight#overview) list the CSS layers that Tailwind works with. ```css title="Default tailwind CSS layer order" @layer theme, base, components, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/preflight.css" layer(base); @import "tailwindcss/utilities.css" layer(utilities); ``` We need to insert `'infinite-table'` CSS layer before the Tailwind `'components'` layer, so Tailwind CSs can easily override our styles. But we to put it before `'base'` so the Tailwind resets don't unexpectedly affect Infinite Table. ```css title="Tailwind CSS layers with infinite-table layer specified in the correct position" @layer theme, base, infinite-table, components, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/preflight.css" layer(base); @import "tailwindcss/utilities.css" layer(utilities); ``` **Example** In this demo, hover over a cell to see a Tailwind color applied. This affects all cells, as the Tailwind CSS className was applied by overriding the `className` of all [`columnTypes`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes). Also we applied `px-6` to change the horizontal padding. ```tsx import { InfiniteTable, DataSource, InfiniteTableColumn, components, InfiniteTableProps, } from '@infinite-table/infinite-react'; import * as React from 'react'; const { CheckBox } = components; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const columns: Record> = { age: { field: 'age', header: 'Age', type: 'number', defaultWidth: 100, renderValue: ({ value }) => value, }, salary: { field: 'salary', type: 'number', defaultWidth: 150, }, currency: { field: 'currency', header: 'Currency', defaultWidth: 120 }, preferredLanguage: { field: 'preferredLanguage', header: 'Programming Language', }, canDesign: { defaultWidth: 135, field: 'canDesign', header: 'Design Skills', renderValue: ({ value }) => { return (
{value === null ? 'Some' : value === 'yes' ? 'Yes' : 'No'}
); }, }, country: { field: 'country', header: 'Country', }, firstName: { field: 'firstName', header: 'First Name' }, stack: { field: 'stack', header: 'Stack' }, city: { field: 'city', header: 'City', renderHeader: ({ column }) => `${column.computedVisibleIndex} City`, }, }; const columnDefaults = { className: `px-6 py-2 hover:bg-blue-500/10`, }; const columnTypes: InfiniteTableProps['columnTypes'] = { default: columnDefaults, number: columnDefaults, }; export default function App() { return ( data={dataSource} primaryKey="id"> columnTypes={columnTypes} columns={columns} columnDefaultWidth={150} >
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` ## Advanced Tailwind configuration The next example provides you with a more in-depth configuration. For changing the `className` for rows on hover, use the [`rowHoverClassName`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowHoverClassName) prop. In this example, we applied `bg-orange-900/70!` (notice the Tailwind important `!` modifier) - because the default Infinite styles target the `background` of the rows, and not the `background-color` as Tailwind targets. Hence the important CSS modifier. ```tsx import { InfiniteTable, DataSource, components, useInfiniteHeaderCell, type InfiniteTableColumn, type InfiniteTableProps, type InfiniteTablePropColumnTypes, } from '@infinite-table/infinite-react'; import * as React from 'react'; const { CheckBox } = components; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const HeaderCell = (props: React.HTMLProps) => { const { domRef } = useInfiniteHeaderCell(); return (
{props.children}
); }; const columns: Record> = { age: { field: 'age', header: 'Age', type: 'number', defaultWidth: 100, renderValue: ({ value }) => value, }, salary: { field: 'salary', type: 'number', defaultWidth: 150, }, currency: { field: 'currency', header: 'Currency', defaultWidth: 120 }, preferredLanguage: { field: 'preferredLanguage', header: 'Programming Language', }, canDesign: { defaultWidth: 135, field: 'canDesign', header: 'Design Skills', renderValue: ({ value }) => { return (
{value === null ? 'Some' : value === 'yes' ? 'Yes' : 'No'}
); }, }, country: { field: 'country', header: 'Country', }, firstName: { field: 'firstName', header: 'First Name' }, stack: { field: 'stack', header: 'Stack' }, city: { field: 'city', header: 'City', }, }; const columnDefaults: InfiniteTablePropColumnTypes['default'] = { className: ({ rowInfo }) => { const cls = rowInfo.indexInAll % 2 === 0 ? 'bg-orange-800/10' : 'bg-gray-700/30'; // use the hover to make this specific cell more proeminent return `${cls} hover:bg-orange-900/90!`; }, components: { HeaderCell, }, }; const columnTypes: InfiniteTableProps['columnTypes'] = { default: columnDefaults, number: columnDefaults, }; export default function App() { return ( data={dataSource} primaryKey="id"> columnTypes={columnTypes} rowHoverClassName="bg-orange-900/70!" columns={columns} columnDefaultWidth={150} >
); } const dataSource = () => { return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` --- # Using drag-and-drop to update row grouping via the Grouping Toolbar > We've enhanced the InfiniteTable DataGrid with a Grouping Toolbar, which allows you to drag and drop columns to group/ungroup Published: 2025-08-16 Author: admin Tags: grouping Canonical page: https://infinite-table.com/blog/2025/08/16/grouping-toolbar-now-available-in-the-datagrid With version `7.2.0`, we added another component to make your interaction with the DataGrid easier - namely the `GroupingToolbar`. This toolbar allows users to interact with row grouping very easily, via drag and drop. Drag column headers on the GroupingToolbar component and off you go, grouping is updated. Additionally, you can drag items on the GroupingToolbar in order to change the order of the row grouping. ```tsx {3} title="Base structure for using the grouping toolbar" ``` Simply reference the component via `InfiniteTable.GroupingToolbar` and nest it under ``. In the above and below examples, for simplicity, we're not showing the whole configuration of the `` and `` components - for full code examples, see further below. The good part is that you can very easily add additional elements to your structure and have the grouping toolbar displayed on the side, vertically. ```tsx {8} title="Example structure for vertical grouping toolbar"
``` [Using the GroupingToolbar](https://codesandbox.io/s/wandering-leftpad-2zxwxr) In the example above, try dragging the header of the `hobby` column onto the GroupingToolbar to add grouping by `hobby`. ## Horizontal and vertical layout As shown above, you can use the `GroupingToolbar` both horizontally and vertically. This is configured via the `orientation` prop - either `"horizontal"` (the default) or `"vertical"`. Make sure you configure this to match your desired layout. [Vertical layout demo](https://codesandbox.io/s/still-bird-td2rgc) ## Customizing and Extending the GroupingToolbar When building this, we were sure you will want to customize almost everything about the toolbar. So we prepared a simple way to do this, via the `components` prop of the `GroupingToolbar`. The following components are available: - `Placeholder` - controls the placeholder that's visible when there are no row groups available. - `ToolbarItem` - used to replace the toolbar items - corresponding to the row groups. - `Host` - the component itself - useful to override when you want to add some other React elements before or after the toolbar items. In the example below, we demo how you can display a custom placeholder for the GroupingToolbar. [Using a custom placeholder in the GroupingToolbar](https://codesandbox.io/s/sad-rubin-6kx2v6) With all these ways to hook into the component, there are no limits to the styling and structure of your layout. Give it a try and let us know (via github issues or [twitter](https://x.com/get_infinite)) if there's anything you'd like to see improved or have questions about! ## Summary The new `GroupingToolbar` component brings an intuitive drag-and-drop interface to row grouping in `InfiniteTable`. Whether you prefer horizontal or vertical layouts, the toolbar provides a seamless way to manage grouping while maintaining the flexibility to customize its appearance and behavior. We're excited to see how you'll use this new feature in your applications. Happy coding! --- # The First DataGrid with a DevTools Extension > We've launched a Chrome DevTools Extension for Infinite Table Published: 2025-05-12 Author: admin Tags: devtools, product Canonical page: https://infinite-table.com/blog/2025/05/12/the-first-devtools-for-a-datagrid We're happy to announce that [Infinite Table DevTools extension](https://chromewebstore.google.com/detail/infinite-table-devtools-e/jpipjljbfffijmgiecljadbogfegejfa) is now live! Infinite Table is the first DataGrid with a Chrome DevTools extension. Starting with version `7.0.0` of Infinite, you can specify the `debugId` property on the `` instance and it will be picked up by the devtools. To see the extension on a live demo, head to the [chrome webstore](https://chromewebstore.google.com/detail/infinite-table-devtools-e/jpipjljbfffijmgiecljadbogfegejfa) to download the extension. Then visit [our live demo page](https://infinite-table.com/full-demo) and open your browser devtools - you should see the "Infinite Table" devtool tab. Click it and enjoy interacting with the DataGrid! ```tsx {16} const columns = { name: { field: 'firstName', }, lastName: { field: 'lastName', }, age: { field: 'age', }, } const App = () => { return } ``` If you have multiple instances, each with a unique `debugId` property, they will all show up Infinite Table DevTools Extension ## Current features The Devtools extension was launched with an initial feature-set, which will expand as we grow and as we get user feedbak - so be sure to tell us what you'd like to see in the devtools. Currently, it offers the ability to do the following: - see the list of all columns and adjust which are visible - see timings of the following data operations: sorting, filtering, group/pivot/tree. This always show how much the last operation of that type took. - interact with the grouping and sorting information in the `` - and revert it to user-values at any time - see and clear the logs - see various warning messages and performance-related issues. ## Planned features As we already mentioned, we're planning to expand the devtools, as we're just getting a taste of what's possible. It took us some time to figure our our best workflow in developing the devtools, and we're now confident we can iterate much faster. Having said this, we're looking for feedback from you on what insights you'd like to see in the InfiniteTable DataGrid via the devtools. We have our own list of things we want to work on, but we plan to incorporate user-feedback asap. So here's our wishlist for the devtools: - ability to see more timings on various operations - including a chart with historical values during the lifetime of a DataGrid instance - something similar to how React DevTools shows render operations and their durations. - add the ability to filter logs via channel - show more performance tips&tricks that can make your DataGrid faster - allow you to interact with many props of the DataGrid - row and cell selection, keyboard navigation, filters, column state, sorting, pivoting, pivot result columns, aggregations, column groups, lazy loading, theming and more. - give you a full state of the DataGrid, and the ability to apply and restore it at any time. - show you the details of your license key and remind you if it's close to the expiration date. ## Your turn It's your turn to give us feedback on the Infinite Table DevTools Extension! Let us know what you think and how you'd like to use it in order to enhance your interaction with the DataGrid. --- # Async Context Menus > Learn how to use async context menus in Infinite Table. Published: 2025-03-20 Tags: menus Canonical page: https://infinite-table.com/blog/2025/03/20/async-context-menus Infinite Table 6.1.0 introduces support for lazy loading context menus. This is useful when you need to load your context menu items conditionally, from the backend, based on the cell's value or other conditions. ## How it works Starting with version `6.1.0`, the [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) and [`getContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getContextMenuItems) props can now return a `Promise` that resolves to an array of `MenuItem` objects (or an object with `items` and `columns` properties, if you need to also configure the columns). [Async Context Menus](https://codesandbox.io/s/nostalgic-borg-qg8q7r) The [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) is called with an object that gives you access to all the info regarding the current right-clicked cell - both the row information and the current column. You can use that to decide whether you want to return a menu immediately or to fetch some data from the server and display the context menu after the server response comes in. --- # shadcn/ui theme available for the InfiniteTable React DataGrid Published: 2024-10-16 Author: admin Tags: theming Canonical page: https://infinite-table.com/blog/2024/10/16/shadcn-ui-theme-available We've all come to know and love ❤️ [shadcn/ui](https://ui.shadcn.com/). It comes with a consistent look and feel for all the components in the UI kit, and it's built on top of Tailwind, so people feel at home using it. As React developers, we're thankful for all the hard work happening in the React ecosystem, and we're happy to see more and more UI libraries focusing on providing great developer experience. After recently building [a few other themes](https://infinite-table.com/blog/2024/10/10/new-themes-available.md), we knew we had to build a shadcn/ui theme for ``. So we built one! It's simply called `shadcn`. For it to work, you'll need to make sure the shadcn/ui CSS variables are available, as the `` theme variables will rely on the values of those CSS variables. Other than that, simply import the `` CSS file and you're good to go. ```tsx import '@infinite-table/infinite-react/index.css';
``` You'll have to include the `infinite-theme-name--shadcn` class name on a parent element of `` (or even on the `` component itself). Additionally, for dark mode, you'll have to use the `dark` class name (on the body element for example) to put the shadcn/ui CSS variables in dark mode, and then `` will pick that up. This means that for this theme, using the `infinite-theme-mode--dark` class name is optional. [CodeSandbox demo](https://codesandbox.io/s/lucid-water-fmj7zx) Enjoy! --- # Flashing column cells in Infinite Table Published: 2024-10-15 Author: admin Tags: realtime, customizing Canonical page: https://infinite-table.com/blog/2024/10/15/how-do-i-flash-cells Flashing cells is an important feature that has been requested by some of our users - both [in public](https://github.com/infinite-table/infinite-react/issues/250) and private conversations. It's also a very useful addition for DataGrids users that work in the financial industry. Version `5.0.0` of `` shipped flashing and in this blogpost we want to show how to use it. ## Configuring a flashing column. In order to configure a column to flash its cells when the data changes, you need to specify a custom `ColumnCell` component. ```tsx {14} import { FlashingColumnCell } from '@infinite-table/infinite-react'; const columns: InfiniteTablePropColumns = { id: { field: 'id', }, firstName: { field: 'firstName', }, salary: { field: 'salary', components: { ColumnCell: FlashingColumnCell, } }, }; ``` `@infinite-table/infinite-react` exports a `FlashingColumnCell` React component that you can pass to the `components.ColumnCell` prop of any column you want to flash. [CodeSandbox demo](https://codesandbox.io/s/infinite-flashing-lnf83g) The default flashing duration is `1000` milliseconds. ## Customizing the flashing duration If you want to customize the flashing duration, you need to pass a different `components.ColumnCell` to the column. You can very easily do this by calling `createFlashingColumnCellComponent` and passing the `flashDuration` option. ```tsx import { createFlashingColumnCellComponent } from '@infinite-table/infinite-react'; const FlashingColumnCell = createFlashingColumnCellComponent({ flashDuration: 500, flashClassName: 'my-flash-class', }); const columns: InfiniteTablePropColumns = { salary: { field: 'salary', components: { ColumnCell: FlashingColumnCell, } } } ``` When calling `createFlashingColumnCellComponent`, besides the `flashDuration` option, you can also pass a `flashClassName`, which is a CSS class name that will be applied to the flashing cell for the duration of the flash. ## Customizing the flash colors If you want to customize the flash colors, you have three CSS variables available: - `--infinite-flashing-background`: background color to be used when non-numeric cells flash. - `--infinite-flashing-up-background`: background color to use for flashing numeric cells, when the value goes up. - `--infinite-flashing-down-background`: background color to use for flashing numeric cells, when the value goes down. The example below is configured to use the following colors: - flash up - yellow - flash down - magenta - flash non-numeric - blue Also, the flashing duration is configured to take 2 seconds. Besides clicking the "start updates" button, you can also edit the salary value in any cell. When you confirm the edit, the salary value will flash. [Flashing takes 2s and has custom colors](https://codesandbox.io/s/infinite-flashing-forked-fpjrsg?workspaceId=cf52b898-10a5-4d0b-833f-96a3a9220dc5) ## Taking it further Infinite Table implements flashing by passing in a custom `ColumnCell` component. However, you're not limited to using our [default implementation](https://github.com/infinite-table/infinite-react/blob/master/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx). You can very easily create your own component and apply your own custom logic. Maybe you want display both the new and the old values in the cell - this can be implemented quite easily. It's up to you to extend the cell rendering to suit your business requirements. The current flashing implementation is flashing on any change in a cell, but you might be interested only in some of the changes. You can definitely use [`onEditPersistSuccess`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditPersistSuccess) to detect when a cell edit is persisted and then decide whether to flash the cell or not. The possibilities are very diverse. We're keen to see what you build! --- # New DataGrid themes available - ocean and balsam Published: 2024-10-10 Author: admin Tags: theming, product Canonical page: https://infinite-table.com/blog/2024/10/10/new-themes-available With the release of `` v5.0.0 we've added two new themes: `ocean` and `balsam`. Now you have a selection of themes to choose from: `default`, `minimalist`, `ocean` and `balsam`. To apply a theme, you have to set the className `"infinite-theme-name--THEME_NAME"` to any parent element of the `` component (or even on the component itself). [CodeSandbox demo](https://codesandbox.io/s/infinite-theme-demo-35f9l2?workspaceId=cf52b898-10a5-4d0b-833f-96a3a9220dc5) Learn how to theme Infinite Table to match your brand --- # How to configure the DataGrid to maximise screen real estate Published: 2024-06-18 Author: admin Tags: customizing, theming Canonical page: https://infinite-table.com/blog/2024/06/18/how-to-configure-datagrid-to-maximise-screen-real-estate Many modern apps rely heavily on white-space to make the user interface easy to read and follow. However, there are financial apps or data-heavy apps where you need to display a lot of information in a small space. In this blogpost, we want to show you how to tweak the theming of the Infinite React DataGrid to make it more dense and maximise screen real estate. ## Configuring the spacing in the DataGrid cells The CSS variable you want to target is `--infinite-cell-padding` - it's used to set the padding of the cells in the DataGrid. By default, the padding is set to `var(--infinite-space-2) var(--infinite-space-3)`. This means that the padding is set to `4px 8px` for a root font size of `16px`. ```css {2} title="Default definition for --infinite-cell-padding" :root { --infinite-cell-padding: var(--infinite-space-2) var(--infinite-space-3); /* vertical horizontal */ --infinite-space-2: .25rem; /* 4px - for a root font size of 16px */ --infinite-space-3: .5rem; /* 8px */ } ``` You can override this variable in your CSS to make the padding smaller. For example, you can set the padding to `2px 4px` by setting the variable like this: ```css {2} title="Override the --infinite-cell-padding variable" body { --infinite-cell-padding: 2px 4px; } ``` It's important to understand that cell height is not given by the padding, but by the [`rowHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowHeight) prop. So if you want to make the DataGrid more dense, you should also consider setting the [`rowHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowHeight) prop to a smaller value. [Using rowHeight and cell padding to configure a dense mode in DataGrid cells](https://codesandbox.io/s/react-datagrid-infinite-table-theme-switching-forked-psnzfr) ## Configuring the spacing in the column headers For configuring padding inside column headers, you need to use the ```--infinite-header-cell-padding``` CSS var. ```css {2} title="Default definition for --infinite-header-cell-padding" :root { --infinite-header-cell-padding: var(--infinite-header-cell-padding-y) var(--infinite-header-cell-padding-x); --infinite-header-cell-padding-x: var(--infinite-space-3); --infinite-header-cell-padding-y: var(--infinite-space-3); } ``` You can make the padding smaller for example give it a value of `2px 4px` by setting the variable like this: ```css {2} title="Override the --infinite-header-cell-padding variable" body { --infinite-header-cell-padding: 2px 4px; } ``` [Dense mode in both cells and column headers](https://codesandbox.io/s/react-datagrid-dense-mode-forked-tz3gft) The above demo also uses the [`headerOptions.alwaysReserveSpaceForSortIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#headerOptions.alwaysReserveSpaceForSortIcon) prop to make sure that the column headers don't reserve a space for the sort icon when the respective column is not sorted. Another option would be to override the CSS spacing scale that InfiniteTable defines - but that affects more than just the padding of the cells and headers. ```CSS title="Default values for the spacing scale" :root { --infinite-space-1: .125rem; --infinite-space-2: .25rem; --infinite-space-3: .5rem; --infinite-space-4: 0.75rem; --infinite-space-5: 1rem; } ``` You're encouraged to experiment with these variables to find the right balance for your app. --- # How to use Excel-like editing in your DataGrid Published: 2024-06-13 Author: admin Tags: editing Canonical page: https://infinite-table.com/blog/2024/06/13/how-to-use-excel-like-editing-in-datagrid Excel-like editing is a very popular request we had. In this short article, we show you how to configure Excel-like editing in the Infinite React DataGrid. [Click a cell and start typing](https://codesandbox.io/s/excel-like-editing-infinite-datagrid-y6xtw6) This behavior is achieved by using the [Instant Edit keyboard shorcut](https://infinite-table.com/docs/learn/keyboard-navigation/keyboard-shortcuts.md#instant-edit). ## Configuring keyboard shortcuts ```ts {4,12} import { DataSource, InfiniteTable, keyboardShortcuts } from '@infinite-table/infinite-react'; function App() { return primaryKey="id" data={dataSource}> columns={columns} keyboardShortcuts={[ keyboardShortcuts.instantEdit ]} />
} ``` The `instantEdit` [keyboard shorcut](https://infinite-table.com/docs/learn/keyboard-navigation/keyboard-shortcuts.md) is configured (by default) to respond to any key (via the special `*` identifier which matches anything) and will start editing the cell as soon as a key is pressed. This behavior is the same as in Excel, Google Sheets, Numbers or other spreadsheet software. To enable editing globally, you can use the [`columnDefaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultEditable) boolean prop on the `InfiniteTable` DataGrid component. This will make all columns editable. Or you can be more specific and choose to make individual columns editable via the [column.defaultEditable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultEditable) prop. This overrides the global [`columnDefaultEditable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultEditable). Read about how you can configure various editors for your columns. A picture is worth a thousand words - see a chart for the editing flow. ## Finishing an Edit An edit is generally finished by user interaction - either the user confirms the edit by pressing the `Enter` key or cancels it by pressing the `Escape` key. As soon as the edit is confirmed by the user, `InfiniteTable` needs to decide whether the edit should be accepted or not. In order to decide (either synchronously or asynchronously) whether an edit should be accepted or not, you can use the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) prop or the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) alternative. When neither the global [`shouldAcceptEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldAcceptEdit) nor the column-level [column.shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) are defined, all edits are accepted by default. Once an edit is accepted, the [`onEditAccepted`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditAccepted) callback prop is called, if defined. When an edit is rejected, the [`onEditRejected`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditRejected) callback prop is called instead. The accept/reject status of an edit is decided by using the `shouldAcceptEdit` props described above. However an edit can also be cancelled by the user pressing the `Escape` key in the cell editor - to be notified of this, use the [`onEditCancelled`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditCancelled) callback prop. Using shouldAcceptEdit to decide whether a value is acceptable or not In this example, the `salary` column is configured with a [shouldAcceptEdit](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.shouldAcceptEdit) function property that rejects non-numeric values. [CodeSandbox demo](https://codesandbox.io/s/infinite-table-editing-custom-edit-value-2x7nrw) ## Persisting an Edit By default, accepted edits are persisted to the `DataSource` via the [DataSourceAPI.updateData](https://infinite-table.com/docs/reference/datasource-api/index.md#updateData) method. To change how you persist values (which might include persisting to remote locations), use the [`persistEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#persistEdit) function prop on the `InfiniteTable` component. The [`persistEdit`](https://infinite-table.com/docs/reference/infinite-table-props.md#persistEdit) function prop can return a `Promise` for async persistence. To signal that the persisting failed, reject the promise or resolve it with an `Error` object. After persisting the edit, if all went well, the [`onEditPersistSuccess`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditPersistSuccess) callback prop is called. If the persisting failed (was rejected), the [`onEditPersistError`](https://infinite-table.com/docs/reference/infinite-table-props.md#onEditPersistError) callback prop is called instead. --- # Master Detail React DataGrid with Charts Published: 2024-06-05 Author: admin Tags: master-detail Canonical page: https://infinite-table.com/blog/2024/06/05/master-detail-datagrid-with-charts In this demo, we show you how easy it is to leverage the master-detail support in our React DataGrid in order to toggle between a table and a chart view in the row detail. [It's very easy to change between an InfiniteTable or a chart in the row detail](https://codesandbox.io/s/master-detail-with-charts-gg7h4f) In the [RowDetail](https://infinite-table.com/docs/reference/infinite-table-props.md#components.RowDetail) component, we render a ``, which in turn will render either an `` component or a chart. The `` in InfiniteTable is very powerful and does all the data processing the grid needs. All the row grouping, sorting, filtering, aggregations, pivoting are done in the `` - so you can use it standalone, or with InfiniteTable - it's totally up to you. In practice, this means that you can use the `` to process your data and then simply pass that to a charting library like `ag-charts-react`. ```tsx const detailGroupBy: DataSourcePropGroupBy = [{ field: "stack" }]; const detailAggregationReducers: DataSourcePropAggregationReducers = { salary: { field: "salary", initialValue: 0, reducer: (acc, value) => acc + value, done: (value, arr) => Math.round(arr.length ? value / arr.length : 0), }, }; function RowDetail() { const rowInfo = useMasterRowInfo()!; const [showChart, setShowChart] = React.useState(rowInfo.id % 2 == 1); return (
{/** * In this example, we leverage the DataSource aggregation and grouping feature to * calculate the average salary by stack for the selected city. */} data={detailDataSource} primaryKey="id" groupBy={detailGroupBy} aggregationReducers={detailAggregationReducers} > {/** * Notice here we're not rendering an InfiniteTable component * but rather we use a render function to access the aggregated data. */} {(params) => { // here we decide if we need to show the chart or the grid if (!showChart) { return ( ); } // the dataArray has all the aggregations and groupings done for us, // so we need to retrieve the correct rows and pass it to the charting library const groups = params.dataArray.filter((rowInfo) => rowInfo.isGroupRow); const groupData = groups.map((group) => ({ stack: group.data?.stack, avgSalary: group.reducerData?.salary })); return ( ); }}
); } ``` The demo above is using the `ag-charts-react` package to render the charts. Read more about the [rendering custom content in a master-detail setup](https://infinite-table.com/docs/learn/master-detail/custom-row-detail-content.md). --- # A minimalist theme for your favorite React DataGrid Published: 2024-05-27 Author: admin Tags: theming Canonical page: https://infinite-table.com/blog/2024/05/27/minimalist-theme-for-react-datagrid We implemented a minimalist theme for the Infinite React DataGrid - it's designed to be simple and clean, with a focus on readability and performance. Building a second theme forced us think about dark/light mode support and how to make the theme more customizable. [CodeSandbox demo](https://codesandbox.io/s/react-datagrid-infinite-table-theme-switching-666xq7) Read more about the [available themes in our React DataGrid](https://infinite-table.com/docs/learn/theming/index.md#available-themes). ## Available themes ### Default theme The `default` theme is applied when you don't specify any explicit theme by default. ### Minimalist theme The `minimalist` theme is inspired from minimalistic designs and is a good choice if you want to keep the UI simple and clean. ## Applying the theme A theme is applied by using the `"infinite-theme-name--THEME_NAME"` CSS className in any parent element of the `` component (or even on the component element). You will want to apply the theme name and theme mode classNames to the same element, so you'll end up with a className like `"infinite-theme-name--minimalist infinite-theme-mode--dark"`. ```tsx title="Applying the minimalist theme with dark mode explicitly" ``` ## Using theme mode There are two theme modes available in Infinite: `light` and `dark`. Unless otherwise explicitly configured, the theme mode is applied based on the user OS settings for the [preferred color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme). However, the theme mode can be enforced, by having a parent element with a CSS className of `"infinite-theme-mode--light"` or `"infinite-theme-mode--dark"` ```tsx title="Applying light mode via container className"
``` ```tsx title="Explicitly applying dark theme via container className"
``` --- # The best testing strategies for frontends Published: 2024-04-22 Author: admin Tags: testing, engineering Canonical page: https://infinite-table.com/blog/2024/04/22/the-best-testing-strategies-for-frontends In our previous article, we focused on documenting [the best testing setup for frontends](https://infinite-table/blog/2024/04/18/the-best-testing-setup-for-frontends-playwright-nextjs), which used Playwright and Next.js. You can check out the repository [here](https://github.com/infinite-table/testing-setup-nextjs-playwright) where you can find the full setup. We consider the combination described above, Playwright + NextJS being the best combo around for testing frontends. Ok, you can switch out NextJS with other meta-framework that offers file-system routing, but the idea is the same: every test is made of 2 sibling files, with the same name but different extension. In the test files, Playwright is configured to navigate automatically to the page being tested, so no need for adjustments if you move files around. This saves you a lot of time and hustle and makes your tests more robust and focused. But in addition to end-to-end testing with Playwright and NextJS, there are other forms of testing out there which are available and can be used to complement your testing strategy. In this article, we'll focus on what we think are the best testing strategies for frontends. So here are a few options: - E2E testing - Component testing - Visual regression testing - Unit testing For each of those options there are plenty of tools you can use, each with its own pros and cons. ## E2E testing With the advent of tools like [Puppeteer](https://pptr.dev/) and now [Playwright](https://playwright.dev/), end-to-end testing has become much easier and more reliable. For anyone who's used Selenium in the past, you know what I'm talking about. Puppeteer has opened the way in terms of E2E tooling, but Playwright has taken it to the next level and made it easier to await for certain selectors or conditions to be fulfilled (via [locators](https://playwright.dev/docs/locators)), thus making tests more reliable and less flaky. Also, it's a game changer that it introduced a test-runner - this made the integration between the headless browser and the actual test code much smoother. ### Reasons to use E2E End to end testing is actually a real browser, so the closest possible environment to what your app will be using. No need to fake the page with JSDOM, no need to only do shallow rendering in React. Just use the platform! ## Component testing Probably [Enzyme](https://enzymejs.github.io/enzyme/) was the first to popularize component testing in React by doing shallow rendering and expecting some things to be there in the React component tree. Then [React Testing library](https://testing-library.com/) came and took component testing to a whole new level. The tools are great for what they're doing, but with the advent of better tooling, we should move on to better ways of testing. With the tools we have now in 2024, there's no more need to use JSDOM and simulate a browser enviroment. It used to be very cumbersome to start a headless browser back in the day, but now with Playwright/Puppeteer, it's a breeze. ## Visual regression testing Also in the days before Playwright was around, there was much hype about visual regression testing. It was very very tempting to use it - who wouldn't want to have a tool that automatically checks if the UI has (mistakenly) changed? It might fit a few use-cases, but in general, it's not worth the effort of maintaining all those tests for any little change in the UI. True, you can set thresholds for the differences, but it's still a lot of work to maintain it, especially in highly dynamic frontends and teams. With better CSS approaches like [TailwindCSS](https://tailwindcss.com/) and [Vanilla Extract](https://vanilla-extract.style/) (which we're heavily using) it's much easier to maintain the UI and make sure it doesn't change unexpectedly. No more conflicting CSS classes, much less CSS specificity issues and much less CSS code in general. One of the troubles in large and tangled CSS codebases is that it's write-only. Well, not write-only per se, but teams are generally afraid to remove a line of CSS cause it might break someone else's code or it might still be used. With [Vanilla Extract](https://vanilla-extract.style/) you can be sure that if you remove a CSS class, it's not used anywhere else in the codebase. It's been a game changer in terms of CSS maintainability and productivity for us at [Infinite Table](https://infinite-table.com/). So with all those tools to make styling easier, the need for visual regression testing has dropped significantly. ## Unit testing Unit testing will be here to stay - at least if besides your UI, your app has some significant business logic. We're using it in combination with E2E testing to make sure complex use-cases work as expected. For example, our [logic for row grouping](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md) is fully tested with unit tests. We do have E2E tests for it, but with unit tests we can have full coverage of all the nitty-gritty details of the grouping logic. We do the same for pivoting and aggregating. Column sizing and column grouping are also covered with unit tests. We think there's always going to be a place for unit testing to ensure robustness and reliability of the app under even the most complex use-cases and user inputs. ## Conclusion In our experience, the best testing strategy for modern frontends is a combination of E2E testing (using Playwright+NextJS), and unit testing. Visual regression testing is not worth the effort in our opinion, especially with the advent of better CSS tooling like TailwindCSS and [Vanilla Extract](https://vanilla-extract.style/). Though we used shallow component testing in the past, we're not going back to it - mocking the DOM and the browser is no longer worth it when you can use a real browser with Playwright. We hope this article has been helpful in guiding you towards the best testing strategy for your frontend. If you have any questions or comments, feel free to reach out to us at [admin@infinite-table.com](mailto:admin@infinite-table.com). We're always happy to help! --- # The best testing setup for frontends, with Playwright and NextJS Published: 2024-04-18 Author: admin Tags: testing, engineering Canonical page: https://infinite-table.com/blog/2024/04/18/the-best-testing-setup-for-frontends-playwright-nextjs We want to share with you the best testing setup we've experienced - and this includes using [Playwright](https://playwright.dev/) and [NextJS](https://nextjs.org/). It's a setup we've come up with for Infinite React DataGrid, which is a complex component, with lots of things to test, but this configuration has helped us ship with more confidence and speed. ## What you should expect from a testing setup ### Fast feedback ⚡️ Quick ⚡️ feedback is a no-brainer, since without a fast turnaround, devs will not have the patience to run the tests and will move on to the next "burning" issue or to the next cup of coffee. Also, you can't run all the test suite at once, so you need to be able to run only the tests that are relevant to the changes you've made. This has long been available in unit-testing frameworks, but it's not so common in end-to-end testing, when loading a webpage and rendering an actual component is involved. In this article we want to show you how we achieved fast feedback that allows rapid developer iterations. ### Stability and predictability You don't need flaky tests that fail randomly - it's the last thing you want when doing a release, or even during development. Waiting for an element to appear on page or an animation to finish or an interaction to complete is a common source of flakiness in end-to-end tests, but Playwright gives you the tools to address these issues - thank you [Playwright locators](https://playwright.dev/docs/locators) 🙏 and other playwright testing framework features. ### Ease of maintenance and debugging Another crucial point when you setup a testing framework and start writing tests is how easy is to write a new test, to inspect what is being tested and to reproduce failing tests. All these should be as easy as opening loading a URL in a browser - this is exactly what this setup gives you, with NextJS and Playwright playing very well together. When one of your tests fails, Playwright outputs a command you can run to reproduce the exact failure and actually see the UI at the moment of the failure, with the ability to navigate through the test timeline and see what happened before the failure. ## Setting up NextJS and Playwright ### Step 1 - creating the NextJS app ```sh $ npx create-next-app@latest ``` You're being asked a few questions. For `Would you like to use src/ directory?` we chose `Yes`. Also, we're using TypeScript. When you run this command, make sure for this question `Would you like to use App Router?` you reply `No`, as you want to use file-system routing to make it very easy and intuitive to add new pages and tests. Check out our repo for this stage of the setup - [Step 1 - setting up NextJS](https://github.com/infinite-table/testing-setup-nextjs-playwright/tree/01-setup-nextjs). Before you go to the next step, you can configure your `next.config.mjs` to use the `.page` extension for your pages. ```js const nextConfig = { reactStrictMode: true, pageExtensions: ["page.tsx", "page.ts", "page.js"], }; export default nextConfig ``` This is useful so NextJS will only compile those files as pages that your tests will be targeting, and not all the files in the `pages` folder, which will also contain your tests. So you know all your `.page` files are pages that your tests will be run against and all your `.spec` files are tests (see next step). ### Step 2 - setting up Playwright ```sh $ npm init playwright@latest ``` Again a few questions about your setup. `Where to put your end-to-end tests?` - choose `src/pages` - which makes your NextJS pages folder the place where you put your end-to-end tests. This script installs `@playwright/test` and creates a `playwright.config.ts` file with the default configuration. Most importantly, the `testDir` is configured to `./src/pages`. By default, all `.spec` files in the `testDir` (which is set to `src/pages`) will be run as tests. Check out our repo for this stage of the setup - [Step 2 - setting up Playwright](https://github.com/infinite-table/testing-setup-nextjs-playwright/tree/02-setup-playwright). There are some additional configurations you might want to do in this step. You probably want to change the default `reporter` from `'html'` to `'list'` in your `playwright.config.ts` - the `'html'` reporter will open a browser window with the test results, which you might not prefer. You'd rather see the results in the terminal. ```ts {3} title="Configure the reporter in playwright.config.ts" export default defineConfig({ testDir: "./src/pages", reporter: "list", // the 'html' reporter will open a browser window with the test results // ... }) ``` For now, you might want to only run your tests in one browser, so comment out any additional entries in the `projects` array in your `playwright.config.ts` file - that controls the devices that will be used in your tests. The last piece of the puzzle before running your first test with Playwright is defining the `test` script in your `package.json`. ```json {4} title="package.json" { "name": "testing-setup-nextjs-playwright", "scripts": { "test": "npx playwright test", "dev": "next dev", "build": "next build", }, } ``` Executing the `npm run test` command will run the tests in the `src/pages` folder - for now, you should have a single file, `example.spec.ts`, which was generated by the `npm init playwright` command. ![Playwright test output](https://infinite-table.com/blog-images/step-2-initial-results.png) Your initial test file was something very basic. This file is importing the `test` (and `expect`) function from `@playwright/test` - and this is what you're using to define tests (and write assertions). ```ts {1} title="example.spec.ts" import { test, expect } from "@playwright/test"; test("has title", async ({ page }) => { await page.goto("https://playwright.dev/"); // Expect a title "to contain" a substring. await expect(page).toHaveTitle(/Playwright/); }); ``` ### Step 3 - configuring the naming convention in Playwright to open the right pages This step is probably the most important one in your configuration. Normally your tests will open webpages before you start testing - but this is not something you want to do explicitly in your project. Rather, you want your tests to automatically navigate to the corresponding page for the test. This is what this step is achieving - and we're using [Playwright fixtures](https://playwright.dev/docs/test-fixtures) to do this. Think of a fixture as some code that's configuring the testing environment for each of your tests. A fixture will extend the `test` function from `@playwright/test` with additional functionalities. Mainly, we want before every test to open the correct page, without writing this explicitly in every test. Based on the location of the test file in the file system, we want to navigate to a webpage for it and we assume it will have the same path as the test file. This is possible because NextJS is configured to use file-system routing. ```ts {1,3-5} title="Defining the fixture file - test-fixtures.ts" import { test as base, expect, PlaywrightTestArgs, PlaywrightTestOptions, Page, } from "@playwright/test"; export * from "@playwright/test"; export const test = base.extend< PlaywrightTestArgs & PlaywrightTestOptions >({ //@ts-ignore page: async ({ baseURL, page }, use, testInfo) => { const testFilePath = testInfo.titlePath[0]; const fileName = testFilePath.replace(".spec.ts", ""); const url = `${baseURL}${fileName}`; // navigate to the corresponding page for this test await page.goto(url); await use(page); }, }); ``` We'll give this fixture file the name `test-fixtures.ts` and put it in the root of the project. Now instead of importing the `test` function from `@playwright/test` we want to import it from the `test-fixtures.ts` file - we'll do this in all our tests. To make this easier, let's also define a path alias in the `tsconfig.json` file. ```json {4} title="tsconfig.json" { "compilerOptions": { "paths": { "@playwright/test": ["test-fixtures.ts"], } } } ``` We're ready to write our first test page in NextJS and use the new fixture in the Playwright test. ```tsx title="src/pages/example.page.tsx" export default function App() { return
Hello world
; } ``` ```ts {1} title="src/pages/example.spec.ts" import { test, expect } from "@testing"; // notice the import test("Main example has corrent content", async ({ page }) => { // notice we don't need to navigate to the page, this is done by the fixture await expect(await page.innerText("body")).toContain("Hello world"); }); ``` For our tests against the NextJS app, we obviously need to start the app. Let's configure a custom port of `5432` in the package.json `dev` script. ```json {3} title="package.json" { "scripts": { "dev": "next dev --port 5432", "test": "npx playwright test" } //... } ``` We need to use the same port in the Playwright configuration file. Also we'll use a smaller test `timeout` (the default is 30s). ```ts {9,11} title="playwright.config.ts" import { defineConfig } from "@playwright/test"; /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ testDir: "./src/pages", reporter: "list", use: { baseURL: "http://localhost:5432/", }, timeout: process.env.CI ? 10000 : 4000, // ... more options }); ``` We're now ready to roll! `npm run dev` will run NextJS and `npm run test` will run the tests against your NextJS app. To make the setup easier, avoid using `index.page.tsx` pages in NextJS - give your pages another name, to avoid issues with directory index pages in tests. This can easily be solved in the test fixture, but for the sake of clarity and brevity we're not doing it now. Check out our repo for this stage of the setup - [Step 3 - configuring the Playwright fixture and naming convention](https://github.com/infinite-table/testing-setup-nextjs-playwright/tree/03-configure-naming-convention). ### Step 4 - adding watch mode As we mentioned initially, no testing setup is great unless it gives you very fast feedback. For this, we obviously need watch mode. We want to be able to re-run tests when our test code has changed, but even better, when our NextJS page has changed - so the page the test is running against. NextJS has watch mode built-in in dev mode, so whenever a page is changed, it's recompiled and the browser is served the updated page. We'll use this in our advantage, so tests will always see the latest version of the page. This means the last piece of the puzzle is to make Playwright re-run the tests when the page has changed or the test itself has changed. For this, we'll use [`chokidar`](https://www.npmjs.com/package/chokidar) - more specifically the [`chokidar-cli`](https://www.npmjs.com/package/chokidar-cli) package. `chokidar` is probably the most useful file watching library for the nodejs ecosystem and it will serve us well. ```json {4} title="package.json" { "scripts": { "test": "npx playwright test", "test:watch": "chokidar '**/*.spec.ts' '**/*.page.tsx' -c 'test_file_path=$(echo {path} | sed s/page.tsx/spec.ts/) && npm run test -- --retries=0 ${test_file_path}'" } } ``` The `test:watch` script is watching for changes in `.spec.ts` files and `.page.tsx` files and whenever there's a change in one of those files, it's re-running the respective test. (When a change was found in a `.page.tsx` file, we're using `sed` to replace the `.page.tsx` extension with `.spec.ts`, because we want to pass the test file to the `npm run test` command so it knows what test to re-run.) The above `test:watch` script was written for MacOS (and Unix-like systems). If you're using Windows, you might need to adjust the command to achieve the same result. Don't forget to run `npm run dev` before running `npm run test` or `npm run test:watch` - you need the NextJS app running to be able to run the tests. After all, that's what you're testing 😅. ### Step 5 - running tests on production build In the last step, we want to build a production build of the NextJS app and run the tests against it. So first let's configure the `next.config.mjs` file to build a static site when `npm run build` is run. ```js {3} title="next.config.mjs - configured to export a static site" const nextConfig = { reactStrictMode: true, output: "export", pageExtensions: ["page.tsx", "page.ts", "page.js"], }; export default nextConfig; ``` Notice the `"output": "export"` property. Having configured this, the `npm run build` will create an `/out` folder with the compiled assets and pages of the app. Next we need an NPM script to serve the compiled app with a static server. ```json {3,4} title="package.json - serve script" { "scripts": { "serve": "npx http-server --port 5432 out", "//...": "// other scripts" }, } ``` We could either run this `serve` script ourselves to start the webserver before running our tests or even better, we can instruct Playwright to [use this webserver automatically](https://playwright.dev/docs/test-webserver#configuring-a-web-server). So let's do that in our `playwright.config.ts` file. ```ts {3,5} title="playwright.config.ts - configured to use a custom server" export default defineConfig({ //... other options // on CI, run the static server to serve the built app webServer: process.env.CI ? { command: "npm run serve", url: "http://localhost:5432", reuseExistingServer: true, timeout: 120 * 1000, } : undefined, }) ``` In order for Playwright to correctly detect the webserver is running ok, we need to make sure we have a valid index page at that address, so we need to add a `index.page.tsx` file in the `pages` folder. ```tsx title="src/pages/index.page.tsx" export default function App() { return
Index page
; } ``` This is just useful in the CI environment so that Playwright can detect the server is running and the app is served correctly. Next, in order to run our tests as if we're in the CI environment, let's add a `test:ci` script, which is basically calling the `test` script but setting the `CI` environment variable to `true`. ```json {3,4} title="package.json - test:ci script" { "scripts": { "test:ci": "CI=true npm run test", "test": "npx playwright test", "serve": "npx http-server --port 5432 out", "//...": "// other scripts" }, } ``` We're now ready to run our tests against the production build of the NextJS app. ```sh npm run build && npm run test:ci ``` This script first builds the NextJS static app and then runs the tests against it. ## Configuring CI github actions We're now ready to integrate our [testing workflow into CI via Github actions](https://playwright.dev/docs/ci-intro). Create a YAML file `.github/workflows/test.yml` in the root of your project with the following content. ```yaml {19,23} title=".github/workflows/test.yml" name: Playwright Tests on: push: branches: [main, master] pull_request: branches: [main, master] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: lts/* - name: Install dependencies run: npm ci - name: Build app run: npm run build - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npm run test - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 ``` With this, you're ready to go! Push your changes to the main branch and see your tests running and passing in the CI environment. Go green! 🟢 ## Demo repository You can find the full setup in our [testing-setup-nextjs-playwright repo](https://github.com/infinite-table/testing-setup-nextjs-playwright/tree/main?tab=readme-ov-file). Check it out and give it a star if you find it useful. ## Profit 🚀 With this setup, you have a very convenient way to write your tests against real pages, loaded in a real browser, just like the end user experiences. And with the watch mode giving you instant feedback, you no longer have an excuse to not write tests. This is the same setup we've been using for developing and testing the [Infinite Table React DataGrid](https://infinite-table.com) and it has been serving us really well. DataGrids are some of the most complex UI components one can build, so having a reliable tool that allowed us to iterate very quickly was crucial to us. This helped us add new features, while being confident that all of the existing core functionalities like row/column grouping, filtering, sorting, pagination, pivoting still work as expected. The setup was a pivotal point in our development process and it's what gives us and our enterprise customers the peace of mind that the product is stable and reliable, both now and in the future. --- # How to use multiple cell selection in the DataGrid Published: 2024-03-08 Author: admin Tags: cell-selection Canonical page: https://infinite-table.com/blog/2024/03/08/how-to-select-cells-and-use-cell-selection The article will cover some popular cell-selection scenarios in Infinite React DataGrid. ## Multiple cell selection By far, the most common use-case for cell selection is multiple cell selection. For this, you need to configure the [`selectionMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode) prop on the `` component to use `"multi-cell"`. In addition, if you want to specify a default value for cell selection, you can use the [`defaultCellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultCellSelection) prop - or the controlled alternative [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection), in which case also make sure you update the value when [`onCellSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onCellSelectionChange) is called. ```tsx ``` [CodeSandbox demo](https://codesandbox.io/s/little-wood-55dysw) When multiple cell selection is configured in the React DataGrid, the user can select cells by `CMD`/`CTRL` clicking to add a single cell to the selection or by `SHIFT` clicking to select a range of cells. ## Showing a chart based on selected cells Let's implement a common use-case for multiple cell selection - showing charts based on the selected cells, for example, a bar chart, with names on the x axis and ages on the y axis. [CodeSandbox demo](https://codesandbox.io/s/funny-silence-2v9r2t) In this example, to retrieve the values from the selected cells, we used the [`mapCellSelectionPositions`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#mapCellSelectionPositions) from the [cell selection API](https://infinite-table.com/docs/reference/cell-selection-api/index.md). ## Cell selection format The [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) prop is an object with the following shape: - `defaultSelection` - `boolean` - whether or not cells are selected by default. - either: - `selectedCells`: `[rowId, colId][]` - an array of cells that should be selected (this is combined with `defaultSelection: false`) - or - `deselectedCells`: `[rowId, colId][]` - an array of cells that should be deselected (this is combined with `defaultSelection: true`) When `defaultSelection` is `true`, you will only need to specify the `deselectedCells` prop. And when `defaultSelection` is `false`, you will only need to specify the `selectedCells` prop. In this way, you can either specify which cells should be selected or which cells should be deselected - and have a default that matches the most common case. The `selectedCells`/`deselectedCells` are arrays of `[rowId, colId]` tuples. The `rowId` is the `id` of the row ([the primary key](https://infinite-table.com/docs/reference/datasource-props/index.md#primaryKey)), and the `colId` is the `id` of the column (the identifier of the column in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) prop). ## Using include-lists and exclude-lists for specifying cell selection As already demonstrated in the previous snippet, you can pass a [default value for cell selection](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultCellSelection). In addition to listing or excluding specific cells from selection, you can use wildcards: ```tsx title="Include-list: selecting all cells in a column" const defaultCellSelection = { defaultSelection: false, // all cells are deselected by default selectedCells: [ // all cells in the stack column ['*', 'stack'], // also this specific cell ['row2', 'firstName'], ], }; ``` ```tsx title="Include-list: selecting all cells in a row" const defaultCellSelection = { defaultSelection: false, // all cells are deselected by default selectedCells: [ // all cells in the row ['row1', '*'], // also this specific cell ['row2', 'firstName'], ], }; ``` ```tsx title="Exclude-list: selecting everything except a column" const defaultCellSelection = { defaultSelection: true, // all cells are selected by default deselectedCells: [['*', 'stack']], }; ``` [Using wildcard selection to select whole cell or row](https://codesandbox.io/s/throbbing-platform-s9jtd4) ## Single cell selection Single cell selection is not common - what you probably want to use in this case is the [`activeCellIndex`](https://infinite-table.com/docs/reference/infinite-table-props.md#activeCellIndex) prop to emulate single cell selection - but that's basically cell navigation. --- # Setting up a master-detail DataGrid with Infinite Table for React Published: 2024-03-06 Author: admin Tags: master-detail Canonical page: https://infinite-table.com/blog/2024/03/06/setting-up-master-detail-datagrid We recently [announced the release of master-detail in the Infinite React DataGrid](https://infinite-table.com/blog/2024/02/26/master-detail-now-available-in-react-datagrid.md), so we also made a video tutorial to follow along, if video is your preferred learning method. This shows the very basics of configuring the Infinite React DataGrid with master-detail, and it's a great starting point for more advanced configurations. [Watch video](https://www.youtube.com/watch?v=5-T2tSEM96I) You can find the full source code for the tutorial in the code sandbox below. This example is two levels deep, but the Infinite React DataGrid supports any number of levels of master-detail. [CodeSandbox demo](https://codesandbox.io/s/elegant-feynman-y3hfcx) --- # Master detail is now available in the Infinite React DataGrid Published: 2024-02-26 Author: admin Tags: master-detail, product Canonical page: https://infinite-table.com/blog/2024/02/26/master-detail-now-available-in-react-datagrid Today is a big day for the Infinite React DataGrid - we're excited to announce that the master detail feature is now available! With this addition, our DataGrid is now enterprise-ready! We know master-detail scenarios are needed in many business applications, and we're happy to provide this feature to our users starting today! 1️⃣ [support for multiple levels of master-detail & rendering custom content](#what-can-you-do-with-master-detail) 2️⃣ [configurable detail height](#configurable-detail-height) 3️⃣ [control over expand/collapse state](#configurable-expandcollapse-state) 4️⃣ [caching mechanism for detail DataGrids](#master-detail-caching) ## What can you do with master-detail? Master-detail allows you to have rows in the DataGrid that expand to show more details. This can be used to show more information about the row, or even to show another DataGrid with related data. You can render basically anything in the detail row - it doesn't need to be another DataGrid. However, if you do want to show another DataGrid, you can, and you can do that at any level of depth. In the detail `` component, you have access to the master row, so it will be very easy to load related data based on the master row the user expands. [Basic master detail DataGrid example](https://codesandbox.io/s/tender-cdn-9cpznx) ## Configurable detail height Our master-detail implementation is very configurable - you can control the height of the row details, the expand/collapse state, and much more. The height of the row details is fully adjustable - see the [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) prop to learn about all the options you have. [Master detail with custom row detail height and custom content](https://codesandbox.io/s/beautiful-sammet-3gkwn9) As seen in the snippet above, it's also really easy to control the expand/collapse state of the row details. You can choose to have some rows expanded by default so details of those rows will be visible from the start. ## Configurable expand/collapse state Using the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState), you can control (in a declarative way) which rows are expanded and which are collapsed. In addition, if you prefer the imperative approach, we also have an [API to work with row details](https://infinite-table.com/docs/reference/row-detail-api/index.md). If you have some rows with details and some without, that's also covered. Use the [`isRowDetailEnabled`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailEnabled) to control which rows will have details and which will not. Another important configuration is choosing the column that has the row detail expand/collapse icon. Use the [`columns.renderRowDetailIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderRowDetailIcon) prop on the column that needs to display the expand/collapse icon. If this prop is a function, it can be used to customize the icon rendered for expanding/collapsing the row detail. ## Master detail caching By far the most common scenario will be to render another DataGrid in the detail row. For such cases we offer a caching mechanism that will keep the state of the detail DataGrid when the user collapses and then expands the row again. To enable caching, use the [`rowDetailCache`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailCache) prop. It can be one of the following: - `false` - caching is disabled - this is the default - `true` - enables caching for all detail DataGrids - `number` - the maximum number of detail DataGrids to keep in the cache. When the limit is reached, the oldest detail DataGrid will be removed from the cache. [Master detail DataGrid with caching for 5 detail DataGrids](https://codesandbox.io/s/thirsty-browser-xxf6wf) Read our docs on [caching detail DataGrids](https://infinite-table.com/docs/learn/master-detail/caching-detail-datagrid.md) to learn more how you can use this feature to improve the user experience. --- # How to customise the DataGrid default sorting Published: 2024-02-02 Author: admin Tags: sorting Canonical page: https://infinite-table.com/blog/2024/02/02/how-to-configure-default-sorting In this article, we'll show you how easy it is to configure the default sorting for the React DataGrid. ## Using the `defaultSort` prop on the DataSource Sorting is configured on the DataGrid `` component. For this, you use the [`defaultSortInfo`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultSortInfo) prop, as either an object, for sorting by a single column, or an array of objects, for sorting by multiple columns. ```tsx title="Specifying a default sort order" // sort by country DESC and salary ASC const defaultSortInfo={[ { field: "country", dir: -1 }, { field: "salary", dir: 1 }, ]} ``` That's it! Now, when the DataGrid is first rendered, it will be sorted by the `country` column in descending order, and then by the `salary` column in ascending order. For sorting to work properly for numeric columns, don't forget to specify `type: "number"` in the [column configuration](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.type). [CodeSandbox demo](https://codesandbox.io/s/default-sort-order-react-datagrid-54dzny) When the [`defaultSortInfo`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultSortInfo) is an array, the DataGrid will know you want to allow sorting by multiple columns. See our page on [multiple sorting](https://infinite-table.com/docs/learn/sorting/multiple-sorting.md) for more details. ## Local vs remote sorting The above example uses local sorting. If you don't explicitly specify a that changes in the [`sortInfo`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortInfo) should trigger a reload (via the [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldReloadData.sortInfo) prop), the sorting will be done locally, in the browser. However, you can also have remote sorting - for this scenario, make sure you use [shouldReloadData.sortInfo=true](https://infinite-table.com/docs/reference/infinite-table-props.md#shouldReloadData.sortInfo). In this case, it's your responsability to send the `sortInfo` to your backend using the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) prop of the DataSource - your `data` function will be called by the DataGrid whenever sorting changes. The arguments the function is called with will include the sort information (along with other details like filtering, grouping, aggregations, etc). ```tsx const dataSource: DataSourceData = ({ sortInfo }) => { if (sortInfo && !Array.isArray(sortInfo)) { sortInfo = [sortInfo]; } const args = [ sortInfo ? 'sortInfo=' + JSON.stringify( sortInfo.map((s) => ({ field: s.field, dir: s.dir, })), ) : null, ] .filter(Boolean) .join('&'); return fetch('https://your-backend.com/fetch-data?' + args) .then((r) => r.json()) .then((data: Developer[]) => data); }; ``` [Remote sorting example](https://codesandbox.io/s/vigilant-lena-shs2td) ## Responding to sorting changes When the user changes the sorting in the React DataGrid UI, the DataSource [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function is called for you, with the new sort information. However, you might want to respond in other ways - for this, you can use [`onSortInfoChange `](https://infinite-table.com/docs/reference/datasource-props#onSortInfoChange ) callback prop. If you use the controlled [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) instead of the uncontrolled [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo), you will need to configure the [`onSortInfoChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onSortInfoChange) callback to respond to sorting changes and update the UI. ## Using the column sort info for rendering At runtime, you have access to the column sort information, both in the column header - see [`columns.renderHeader`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderHeader) and in the column cells - see [`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue). [Customising the column header depending on the sort info](https://codesandbox.io/s/heuristic-butterfly-v5k6v7) For example, you can customise the icon that is displayed in the column header to indicate the sort direction. Via the [`columns.renderHeader`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderHeader) you have full access to how the column header is rendered and can use the sorting/filtering/grouping/aggregation/pivoting information of that column to customise the rendering. --- # How to customise the DataGrid loading state Published: 2024-01-23 Author: admin Tags: customizing, theming Canonical page: https://infinite-table.com/blog/2024/01/23/how-to-customise-datagrid-loading-state We're starting a series of short `"How to"` articles that are very focused and show how to achieve a specific thing with the Infinite Table DataGrid. In this article, we'll document how to customise the DataGrid loading state. ## Customising the loading text First off, you can customise the text that is displayed when the DataGrid is loading data. By default, the DataGrid displays a `"Loading"` text, but you can customise it to anything you want (even JSX, not only string values). ```tsx title="Customising the loading text" {9} const developers: Developer = [ { id: '1', firstName: 'Bob' }, { id: '2', firstName: 'Bill' }, ] // make sure to add "loading" to the DataSource so you see the loading state loading data={developers} primaryKey="id"> loadingText={ Loading your data ... } columns={{ firstName: { field: 'firstName', }, id: { field: 'id', } }} {...props} /> ``` For the value of [`loadingText`](https://infinite-table.com/docs/reference/infinite-table-props.md#loadingText) you can use JSX, not only strings. [CodeSandbox demo](https://codesandbox.io/s/infinite-table-datagrid-custom-loading-text-yzqlsj) ## Customising the loading component - the `LoadMask` In addition to the loading text, you can also customise the `LoadMask` component. This is the component that is displayed when the DataGrid is loading data. By default, it's a `
` with `width: 100%; height: 100%; zIndex: 1; display: flex` that contains the loading text. You do this by overriding the [`components.LoadMask`](https://infinite-table.com/docs/reference/infinite-table-props.md#components.LoadMask) prop in your Infinite Table configuration. ```tsx title="Customising the LoadMask component" {7,15} // make sure to add "loading" to the DataSource so you see the loading state export default function App() { return ( loading data={developers} primaryKey="id"> components={{ LoadMask, }} columns={columns} /> ); } function LoadMask() { return (
Loading App ...
); } ``` [CodeSandbox demo](https://codesandbox.io/s/infinite-table-datagrid-custom-loading-text-forked-vpqps3) --- # Building a DataGrid with the right tools Published: 2023-10-05 Author: admin Tags: product, engineering Canonical page: https://infinite-table.com/blog/2023/10/05/building-a-datagrid-with-the-right-tools Building for the browser has historically been very tedious. In the old days you had to resort to all sorts of hacks for getting the right layout - anyone remembers conditional comments targeting IE6-9? 😅 Yeah, we don't miss those days either. Things have evolved in the last few years, and the amount of goodies JS/CSS/HTML/layout goodies we now take for granted is staggering. New CSS features like flex/grid/custom properties really make a difference. Also browser performance has improved a LOT, and today we can do things in the browser that were unthinkable just a few years ago. However, not everything is easier now than it was back in the browser-war days. Handling all kinds of devices, managing changing dependencies, configuring build tools, choosing the right styling approach, proper E2E testing, keeping a small bundle size, CI pipelines, etc. are all things that can (and will) go wrong if you don't have the right tools. ## TypeScript It's obvious today to just go with `TypeScript`, but a few years ago, it was not as obvious. We've been using TypeScript for quite a few years now, and we're very happy with it. We can never imagine going back to plain JS. ## React Building on top of `React` has given us an amazing component model that's very composable and easy to reason about - and the ecosystem is huge. Read about our journey in the [Why another DataGrid?](https://infinite-table.com/blog/2022/11/08/why-another-datagrid.md) blog post. Back when React was launching, many of our team members were writing DataGrids - either in vanilla JS or using some libraries (`jQuery` anyone? - we don't miss browser incompatibilities). ## CSS Variables and Vanilla Extract As a `DataGrid` Infinite Table is built on top of CSS variables - we're going all in with CSS variables. They have a few gotchas in very advanced cases, but all-in-all they're amazing - and especially for performance. We're not short of [CSS variables that we expose - see the full list](https://infinite-table.com/docs/learn/theming/css-variables.md). Using them has been pivotal not only to the ease of theming, but also to the performance of the DataGrid. Being able to change a CSS custom property on a single DOM element and then reuse it across many elements that are children of the first one is a huge performance win. Our DataGrid performance would not be the same without CSS variables. ### Vanilla Extract The single tool that has made our life a lot easier working with CSS is [Vanilla Extract](https://vanilla-extract.style/). If you're developing a component library, you should definitely use it! Not so much for simple & static apps - there are other styling solutions that are easier to use, like [tailwindCSS](https://tailwindcss.com/). But for component libraries, **Vanilla Extract is amazing**! Did we mention it's amazing? 😅 The fact that you can use TypeScript with it, can use "Find All References", see where everything is used is a huge win. You're not writing readonly CSS anymore - because that tends to be the case with most CSS. People are afraid to change it or remove old CSS code, just in case those rules are still being used or referenced somehow. This way, CSS only grows with time, and this is a code smell. With Vanilla Extract, you get to forget about that. You know what's being used and what's not. Also, hashing class names to avoid collisions is nice - and something now very common in the modern JS ecosystem. It all started with CSS modules, and now it's everywhere, Vanilla Extract included. Other great features we use extensively are: - public facing CSS variables - their names are stable - private CSS variables - their names are hashed - sharing CSS values with the TS codebase is a dream come true. - Vanilla Extract recipes - generating and applying CSS classes based on a combination of properties. It's enough that you have 2-3 properties, each with a few values, and managing their combinations can be a pain. Vanilla Extract recipes manage this in a very elegant way. ## End-to-end testing with Playwright and NextJS Remember the days of Selenium? All those flaky tests, the slow execution, the hard to debug issues? They're gone! [Playwright](https://playwright.dev/) all the way! 300+ tests and going strong! Yes, you read that right! We have more than 300 tests making sure the all the DataGrid features are working as expected. Sorting, filtering, row grouping, column groups, pivoting, aggregations, lazy loading, live pagination, keyboard navigation, cell and row selection, theming - they're all tested! And we're not talking about unit tests, but end-to-end tests. We're testing the DataGrid in the browser, with real data just like real users would. Playwright is an amazing tool, but we're not using it standalone. Paired with a [NextJS](https://nextjs.org/) app, with file-system based routing, we've created files/routes for each functionality. Each NextJS file in turn has a Playwright test file with the same name, but a different extension. This has the benefit that it's always very obvious which test is running against which page. The test and the route always have the same file name, just the extension is different. The test source-code doesn't explicitly contain code that navigates to a specific page, all this is done under the hood, using this simple convention. This way, we have a very clear separation of concerns, and it's very easy to add new tests. We just create a new file in the `pages` folder, and a new test file sibling to it. Another amazing benefit is that we can start the NextJS app and point our browser to whatever page we want to see or debug and it's there. We can very easily do the actions the test is doing and see if we get the expected results. This is a huge win for debugging. ## A tailored state management We've built a very simple yet highly effective state management solution for our DataGrid. It's built to make updating the internal state of the DataGrid as easy as possible - we want a simple API, with clear actions. Our actions map almost 1-to-1 to the DataGrid properties, which makes it very obvious to know who changed what. We can't overstate how important it is to have a clear data flow through the DataGrid. This is because the DataGrid is by far the most complex UI component you'll ever use (and we'll ever build). You can't possibly go beyond that - at least not in common business apps, where you have the normal UI controls you can expect, like inputs, buttons, dropdowns, etc. Just the ComboBox can come near the complexity of the DataGrid, but it's still far behind. It's important to be able to tame all this complexity - otherwise it can slow down the development process and bring it to a halt, making it difficult to add new features or fix bugs. With our current model, even though the DataGrid grew in complexity and features, we never felt our velocity dropping! We enjoy that! ## No dependencies We're very proud of the fact that we have no dependencies in our DataGrid. When you install our package, you only install our package - and nothing else. Nothing that can go wrong due to version conflicts, missing dependencies, npm issues ([remember left-pad](https://www.davidhaney.io/npm-left-pad-have-we-forgotten-how-to-program/)?). Yes, we still depend on packages in our dev process, but we're striving to keep that small as well. It's already complex enough to keep TS, React, NextJS, npm (with workspaces), aliases, esbuild, tsup, playwright all working together in harmony. But we've got through it, and we're very happy with the result. It was worth it! ## Separating concerns We've separated our DataGrid into 2 main parts: - the `` component - handles data loading and processing - the `` component - handles the rendering This was a brilliant idea! It's new? No! It's not our invention, but we're happy we decided to apply it. It adds a better separation between the two big parts of the DataGrid. This also helps tame some of the complexity, while adding clarity to the codebase. It's easier to reason about the code when you know that the `` component is responsible for data loading and processing, while the `` component is ONLY responsible for rendering. ## Conclusion We're not sorry for choosing any of the above tools or approaches when building the InfiniteTable DataGrid component. Our developer velocity is high, and we're able to add new features and fix bugs at a fast pace. We're happy with the result and we're confident that we'll be able to keep this pace in the future. The right tools get the right job done! They make a lot easier. Looking back, we only regret we didn't have those tools 5 years ago - but hey, things are moving in the right direction, and we're happy to be part of this journey. What are your tools for developer productivity? --- # Infinite Table React DataGrid version 3.0.0 released > InfiniteTable DataGrid for React version 3.0.0 brings many small fixes and enhancements, along with a major new feature: cell selection Published: 2023-10-02 Author: admin Tags: product, cell-selection Canonical page: https://infinite-table.com/blog/2023/10/02/version-3-0-0 Version `3.0.0` is a release that brings a long awaited feature: cell selection. This allows the user to perform fined-grained cell selection, either via the [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) prop or via the [Cell Selection API](https://infinite-table.com/docs/reference/cell-selection-api/index.md). 1️⃣ [support for single and multiple cell selection](#1-support-for-single-and-multiple-cell-selection) 2️⃣ [cell selection using wildcards](#2-cell-selection-using-wildcards) 3️⃣ [cell selection API](#3-cell-selection-api) ## 1️⃣ Support for single and multiple cell selection It's been a [long-requested feature to implement cell selection](https://github.com/infinite-table/infinite-react/issues/120). We knew we needed to implement it, but we wanted to do it right while keeping it easy to understand. In fact, we prepared some things in advance - namely [`selectionMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode) was there, it just needed to accept a new value: `"multi-cell"`. ```tsx title="Configuring multi-cell selection" selectionMode="multi-cell" // <--- THIS primaryKey="id" data={[...]} /> ``` The line above is all you need to do to enable cell selection. This allows the user to `Click` or `Cmd/Ctrl+Click` to select a specific cell and `Shift+Click` to select a range of cells. It's exactly the behavior you'd expect from a spreadsheet application. Try `Cmd/Ctrl+Click`ing in the DataGrid cells below to see multiple cell selection in action. [SelectionMode set to 'multi-cell' to allow cell selection](https://codesandbox.io/s/sorting-group-columns-forked-qnvwwh) ### Using a default selection If you want to render the DataGrid with a default selection, you can use the [`defaultCellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultCellSelection) prop. ```tsx const defaultCellSelection = { defaultSelection: false, selectedCells: [ [3, 'hobby'], [4, 'firstName'], [4, 'hobby'], [4, 'preferredLanguage'], [4, 'salary'], ], }; ``` The format for the uncontrolled [`defaultCellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultCellSelection) (and also for the controlled [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection)) is an object with two properties: - `defaultSelection` - `boolean` - whether or not cells are selected by default. - and either - `selectedCells` - `[string|number, string][]` - only needed when `defaultSelection` is `false` - or - `deselectedCells` - `[string|number, string][]` - only needed when `defaultSelection` is `true` The value for `selectedCells` and `deselectedCells` should be an array of `[rowId, colId]` tuples. The `rowId` is the `id` of the row ([the primary key](https://infinite-table.com/docs/reference/datasource-props/index.md#primaryKey)), and the `colId` is the `id` of the column (the identifier of the column in the [`columns`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns) prop). This object shape for the [`defaultCellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultCellSelection)/[`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) props allows you full flexibility in specifying the selection. You can specify a single cell, a range of cells, or even a non-contiguous selection. You can default to everything being selected, or everything being deselected and then enumerate your specific exceptions. [Specifying a default cell selection in Infinite Table](https://codesandbox.io/s/cell-selection-with-default-value-in-infinite-table-fzdhwr) ## 2️⃣ Cell Selection using wildcards The above examples show how to select specific cells, but what if you want to select all cells in a column, or all cells in a row? Well, that turns out to be straightforward as well. You can use the `*` wildcard to select all cells in a column or all cells in a row. ```tsx title="All cells in row with id rowId3 and all cells in hobby column are selected" const defaultCellSelection = { defaultSelection: false, selectedCells: [ ['*', 'hobby'], ['rowId3', '*'], ], } ``` [Cell selection using wildcards](https://codesandbox.io/s/cell-selection-with-wildcards-in-infinite-table-48rs75) Wildcard selection is really powerful and it allows you to select lots of cells without the need to enumerate them all. For example, you can easily select all cells except a few. ### Listening to selection changes You can listen to selection changes by using the [`onCellSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onCellSelectionChange) prop. If you're using controlled cell selection, you have to update the [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) prop yourself in response to user interaction - so [`onCellSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onCellSelectionChange) will be your way of listening to selection changes. ## 3️⃣ Cell Selection API In addition to managing cell selection declaratively, which we encourage, you can also use the [Cell Selection API](https://infinite-table.com/docs/reference/cell-selection-api/index.md) to imperatively update the current selection. We offer the following methods: - [`selectCell`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#selectCell) - selects a single cell, while allowing you to keep or to clear previous selection - [`deselectCell`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#deselectCell) - deselects the specified cell - [`selectColumn`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#selectColumn) - selects a whole column in the DataGrid - [`deselectColumn`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#deselectColumn) - deselects the specified column - [`selectRange`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#selectRange) - selects a range of cells - [`deselectRange`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#deselectRange) - deselects the specified range of cells - [`selectAll`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#selectAll) - selects all cells in the DataGrid - [`clear`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#clear) - clears selection (deselects all cells in the DataGrid) - [`isCellSelected`](https://infinite-table.com/docs/reference/cell-selection-api/index.md#isCellSelected) - checks if the specified cell is selected or not ## Conclusion We'd love to hear your feedback - what do you think we've got right and what's missing. Please reach out to us via email at admin@infinite-table.com or follow us [@get_infinite](https://twitter.com/get_infinite) to keep up-to-date with news about the product. Talk soon 🙌 --- # Infinite Table DataGrid for React reaches version 2.0.0 > With version 2.0.0 InfiniteTable DataGrid for React brings lots of fixes and enhancements including support for sorting group columns, better APIs, improved pivoting, smarter column menus and more. Published: 2023-07-14 Author: admin Tags: product Canonical page: https://infinite-table.com/blog/2023/07/14/version-2-0-0 Version `2.0.0` is a release that allowed us to polish many areas of the component and consolidate its existing features and APIs. We hope this makes your experience with Infinite Table as your React DataGrid of choice even better. Though it doesn't add major new features, this version does improve the overall experience of using the component. In this article we're detailing the most important improvements this release brings. 1️⃣ [better support for sorting group columns](#1-better-support-for-sorting-group-columns) 2️⃣ [allows configuring the behavior when multiple sorting is enabled](#2-multi-sort-behavior) 3️⃣ [smarter column menus](#3-smarter-column-menus) 4️⃣ [improved support for boolean pivot columns](#4-improved-support-for-boolean-pivot-columns) 5️⃣ [better and more exhaustive APIs](#5-better-and-more-exhaustive-apis) [Watch video](https://www.youtube.com/embed/rhoj66cPzYM) ## 1️⃣ Better support for sorting group columns Before version `2.0.0`, group columns were sortable, but only if the configured `groupBy` fields were bound to actual columns. This release enables you to make group columns sortable even when other columns are not defined. For this to work, you have to specify a [sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) as an array, so the column knows how to sort the group values. ```tsx title="Configuring sortType for group columns" groupColumn={{ sortType: ['string', 'number'], field: 'firstName', defaultWidth: 150, }} groupRenderStrategy="single-column" columns={columns} columnDefaultWidth={120} /> ``` [Sorting group columns is now possible](https://codesandbox.io/s/sorting-group-columns-forked-gv5n3z) ## 2️⃣ Multi sort behavior We have introduced [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior) to allow you to configure how the component behaves when multiple sorting is enabled. Two options are available: - `append` - when this behavior is used, clicking a column header adds that column to the alredy existing sort. If the column is already sorted, the sort direction is reversed. In order to remove a column from the sort, the user needs to click the column header in order to toggle sorting from ascending to descending and then to no sorting. - `replace` - the default behavior - a user clicking a column header removes any existing sorting and sets that column as sorted. In order to add a new column to the sort, the user needs to hold the `Ctrl/Cmd` key while clicking the column header. [multiSortBehavior="replace"](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior) is the new default behavior, and also a more natural one, so we recommend using it. [Click column headers to see multi sort behavior in action - try clicking 'preferredLanguage' and 'salary'](https://codesandbox.io/s/spring-snowflake-mh6wpl) ## 3️⃣ Smarter column menus Column menus are now smarter - in previous versions of Infinite Table, users were able to hide the column that had the menu opened, and the menu would hang in its initial position. When this happens, in version `2.0.0`, the menu realigns itself to other existing columns, thus providing a better user experience. ## 4️⃣ Improved support for boolean pivot columns It's pretty common to pivot by boolean columns, and this is now fully supported in Infinite Table. Previous to version `2.0.0`, the column headers for boolean pivot columns were not rendered correctly. [Boolean pivot columns are now supported](https://codesandbox.io/s/lively-microservice-xtyyk7) ## 5️⃣ Better and more exhaustive APIs We have improved our APIs, with new methods and fixes. Among other things, we've polished our [Column API](https://infinite-table.com/docs/reference/column-api/index.md) to offer you the ability to do more with your columns. Previously there were things that were only possible to do if you had access to the internal state of the component, but now we've moved more things to the API. For example, our column sorting code is now centralised, and using [`toggleSort`](https://infinite-table.com/docs/reference/column-api/index.md#toggleSort) gives you the same action as clicking a column header (this was not the case previously). We've added quite a few more methods to our APIs, here's some of the most important ones: - ColumnAPI.[`toggleSort`](https://infinite-table.com/docs/reference/column-api/index.md#toggleSort) - ColumnAPI.[`setSort`](https://infinite-table.com/docs/reference/column-api/index.md#setSort) - ColumnAPI.[`getSortDir`](https://infinite-table.com/docs/reference/column-api/index.md#getSortDir) - ColumnAPI.[`clearSort`](https://infinite-table.com/docs/reference/column-api/index.md#clearSort) - ColumnAPI.[`isSortable`](https://infinite-table.com/docs/reference/column-api/index.md#isSortable) ## Conclusion We've been working on version `2.0.0` for a few months now and we hope you'll enjoy all the little details that make this version a better product, with all the improvements it brings in various areas of the component. We'd love to hear your feedback, so please reach out to us via email at admin@infinite-table.com or follow us [@get_infinite](https://twitter.com/get_infinite) to keep up-to-date with news about the product. Thank you 🙌 --- # Using Menus in Infinite Table > Find out how to use menus in Infinite Table to customise the DataGrid to fit your needs: custom context menus, column menus and more. Published: 2023-02-16 Author: admin Tags: menus Canonical page: https://infinite-table.com/blog/2023/02/16/using-menus-in-infinite-table _With version 1.1.0, our DataGrid now includes support for context menus, which are fully configurable so you can create custom menus for any cell in the table._ 1️⃣ are fully configurable 2️⃣ adjust their position based on the available space 3️⃣ can be used to create custom menus for any cell in the table 4️⃣ give you full access to the information in the cell or the whole DataGrid ## How it works In Infinite Table you can configure a context menu to be displayed when you right-click a cell by using the [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) prop. Simply specify a function that returns an array of objects, each with `label` and `key` properties. Each object in the array is a row in the context menu - with the `label` being the displayed content and the `key` being a unique identifier for the menu row. ```tsx title="Configuring_a_context_menu" const getCellContextMenuItems = ({ column, data, value }) => { if (column.id === 'currency') { return [ { label: `Convert ${value}`, key: 'currency-convert', onAction: (key, item) => { alert('clicked ' + item.key); }, }, ]; } if (column.id === 'age') { return null; } return [ { label: `Welcome ${value}`, key: 'hi', }, ]; }; data={data} primaryKey="id"> getCellContextMenuItems={getCellContextMenuItems} columns={columns} /> ; ``` In the [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) function prop, you have access to all the information you need, in the first argument of the function: - `column` - the column on which the user right-clicked - `data` - the data object for the row the user right-clicked - `value` - the value of the cell on which the context menu has been triggered. This is generally `data[column.field]`, but it can be different if the column has a [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) or [`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) - `rowInfo` - an object that contains more information about the row, like the `id` (the primary key) and the row index - `isGroupRow` - and more If [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) is specified and returns `null`, no custom context menu will be displayed, instead the default browser context menu will be shown (in this case, we do not call `preventDefault()` on the event object). If [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) returns an empty array, the default browser context menu will not be shown (in this case, we are calling `preventDefault()` on the event object), but also no custom context menu will be displayed, as there are no menu items to show. Each item on the context menu can specify an `onAction` function, which will be called when the user clicks on the menu item. The function will receive the `key` and the `item` as arguments. In addition, since the menu items are returned from inside the `getCellContextMenuItems` function, the `onAction` callback has access to the same information as the `getCellContextMenuItems` function. [Context menu for all cells](https://codesandbox.io/s/cell-context-menus-ibtnn0) ## Configuring the context menu to have multiple columns In the above example, notice each context menu item has only one cell, where the `label` property is displayed. However, Infinite Table for React allows you to create more complex menus, with multiple columns. In order to do this, use the same [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) prop, but return an object, with `columns` and `items` ```tsx const getCellContextMenuItems = () => { return { columns: [{ name: 'label' }, { name: 'lcon' }], items: [ { label: 'Welcome', icon: '👋', key: 'hi', }, { label: 'Convert', icon: '🔁', key: 'convert', }, ], }; }; ``` When [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems) is used to configure the column menus, each column `name` should have a corresponding property in the objects returned in the `items` array (each object also needs to keep the `key` property). Also, we recommend keeping a column named `label`. [Customising columns in the context menu ](https://codesandbox.io/s/custom-columns-for-context-menus-hcsz9e) ## Smart positioning Context menus in Infinite Table are smart enough to adjust their position based on the available space relative to the mouse-click coordinates. The menu will always try to fit inside the grid viewport and to look for the best position that will not cause the menu to be cut off or overflow outside the DataGrid. The same algorithm is applied to column menus and also to filter menus (the menu displayed when a filter is shown and the user wants to change the filter operator). ## Context menus outside cells, for the table body There are scenarios when you want to display a context menu even when you right-click outside a cell, but inside the table body - for those cases, you can use [`getContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getContextMenuItems) (in fact, you can use the [`getContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getContextMenuItems) prop for all context menus). The signature of [`getContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getContextMenuItems) is almost identical with that of [`getCellContextMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getCellContextMenuItems), with the exception that cell-related information can be undefined - if the user didn't right-click a cell, but somewhere else in the table body. [Context menus outside cells, for the table body](https://codesandbox.io/s/table-context-menus-0h2qzf) In the example above, if you click outside a cell, a menu with a single item will be displayed - `Add Item`. If you click on a cell, the menu will be different, and will show information about the clicked cell. ## Column menus Besides context menus, the DataGrid also supports menus for columns, that allow you to sort/unsort, pin/unpin, clear filtering and toggle column visibility. Just like context menus, the column menus can also be fully customised, by using the [`getColumnMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getColumnMenuItems) prop. ```tsx title="Customizing-column-menu" function getColumnMenuItems(items, { column }) { if (column.id === 'firstName') { // you can adjust the default items for a specific column items.splice(0, 0, { key: 'firstName', label: 'First name menu item', onClick: () => { console.log('Hey there!'); }, }); } // or for all columns items.push({ key: 'hello', label: 'Hello World', onClick: () => { alert('Hello World from column ' + column.id); }, }); return items; } ``` The first argument passed to the [`getColumnMenuItems`](https://infinite-table.com/docs/reference/infinite-table-props.md#getColumnMenuItems) prop is the array of items that is displayed by default in the column menu. You can either modify this array and return it or you can return another totally different array. [CodeSandbox demo](https://codesandbox.io/s/custom-column-menus-93jsyb) As with context menus, positioning column menus is also smart - the menu will always try to fit inside the grid viewport, so it will align to the right or the left of the column, depending on the available space. ## Conclusion In this article, we've explained just some of the scenarios that are now possible with Infinite Table for React, by using the new context and column menus. Learn more about working with context menus. Configuring column menus to fit your needs - read more. We hope you'll use these functionalities to build amazing DataGrids for your applications, that are fully tailored to your needs. If you find any issues or have any questions, please reach out to us on [Twitter](https://twitter.com/infinite_table) or in the [GitHub Discussions](https://github.com/infinite-table/infinite-react/discussions) or [issues](https://github.com/infinite-table/infinite-react/issues). We're happy to help and improve how you work with the component - we want to make it very easy and straight-forward to use it and are looking for ways to simplify our APIs to **achieve more with less**. --- # Filtering Data with Infinite Table for React > Learn how to filter data both client-side and server-side with Infinite Table for React Published: 2023-01-26 Author: admin Tags: filtering Canonical page: https://infinite-table.com/blog/2023/01/26/filtering-data-with-infinite-table-for-react _Today we shipped cutting-edge column filtering functionality, that enables intuitive client-side and server-side filtering_ 1️⃣ Narrow down your data with your own filter types and operators 2️⃣ Works both client-side and server-side 3️⃣ Easy customization of filters and filter editors 4️⃣ Optimized for performance 5️⃣ Easy to use across multiple columns Filters were, by far, the most requested feature to add to Infinite Table after our initial launch. The recently-released version `1.1.0` of Infinite Table for React introduces support for column filters, which work both client-side and server-side. In order to enable filtering - specify the [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue) property on the `` component, as shown below: ```tsx {4} title="Enabling_filters_on_the_DataSource" data={/* ... */} primaryKey="id" defaultFilterValue={[]}> columns={columns} /> ``` This configures the `` component with an empty array of filters; columns will pick this up and each will display a filter editor in the column header. Of course, you can define some initial filters: ```tsx title="Initial_filters:_filter_by_age_greater_than_40" defaultFilterValue={[ { field: 'age', filter: { type: 'number', operator: 'gt', value: 40 } } ]} ``` You can see how all of this looks like when we put it all together in the examples below. ## Local and Remote Filtering Because the `` [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) prop is a function that returns a `Promise` with remote data, the filtering will happen server-side by default. [Server-side filtering 10k records](https://codesandbox.io/s/infinite-table-with-remote-filters-i8b4wx) When using remote filtering, it's your responsability to send the DataSource [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) to the backend (you get this object as a parameter in your [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function). This value includes for each column the value in the filter editor, the column filter type and the operator in use. In this case, the frontend and the backend need to agree on the operator names and what each one means. Whenever filters change, when remote filtering is configured, the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function prop is called again, with an object that has the `filterValue` correctly set to the current filters (together with `sortInfo` and other data-related props like `groupBy`, etc). However, we can use the [`filterMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode) to force client-side filtering: ```tsx filterMode="local" filterDelay={0} /> ``` We also specify the [filterDelay=0](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) in order to perform filtering immediately, without debouncing and batching filter changes, for a quicker response ⚡️ 🏎 [Client-side filtering 10k records](https://codesandbox.io/s/infinite-table-with-client-side-filters-sqbdbu) Even if your data is loaded from a remote source, using `filterMode="local"` will perform all filtering on the client-side - so you don't need to send the `filterValue` to the server in your `data` function. ## Defining Filter Types and Custom Filter Editors Currently there are 2 filter types available in Infinite Table: - `string` - `number` Conceptually, you can think of filter types similar to data types - generally if two columns will have the same data type, they will display the same filter. Each filter type supports a number of operators and each operator has a name and can define it's own filtering function, which will be used when local filtering is used. [Custom filter type and filter editor for canDesign column](https://codesandbox.io/s/infinite-table-filters-with-custom-editor-and-filter-type-ptlq2v) The example above, besides showing how to define [a custom filter type](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes), also shows how to define a custom filter editor. For defining a custom filter editor to be used in a filter type, we need to write a new React component that uses the [`useInfiniteColumnFilterEditor`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnFilterEditor) hook. ```tsx import { useInfiniteColumnFilterEditor } from '@infinite-table/infinite-react'; export function BoolFilterEditor() { const { value, setValue } = useInfiniteColumnFilterEditor(); return <>{/* ... */}; } ``` This custom hook allows you to get the current `value` of the filter and also to retrieve the `setValue` function that we need to call when we want to update filtering. Read more about this [in the docs - how to provide a custom editor](https://infinite-table.com/docs/learn/filtering/providing-a-custom-filter-editor.md). ## Customise Filterable Columns and Filter Icons Maybe you don't want all your columns to be filterable. For controlling which columns are filterable and which are not, use the [`columns.defaultFilterable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultFilterable) property. This overrides the global [`columnDefaultFilterable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnDefaultFilterable) prop. We have also made it easy for you to customize the filter icon that is displayed in the column header. [Custom filter icons for firstName and salary columns](https://codesandbox.io/s/infinite-table-custom-filter-icon-jc7jr8) You change the filter icon by using the [`columns.renderFilterIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderFilterIcon) prop - for full control, it's being called even when the column is not filtered, but you have a `filtered` property on the argument the function is called with. In the example above, the `salary` column is configured to render no filter icon, but the `header` is customized to be bolded when the column is filtered. ## Ready for Your Challenge! We listened to your requests for advanced filtering. And we believe that we've come up with something that's really powerful and customizable. Now it's your turn to try it out and show us what you can build with it! 🚀 If you have any questions, feel free to reach out to us on [Twitter](https://twitter.com/infinite_table) or in the [GitHub Discussions](https://github.com/infinite-table/infinite-react/discussions). Make sure you try out filtering in Infinite Table for yourself ([and consult our extensive docs](https://infinite-table.com/docs/learn/filtering/index.md) if required). Learn how to use filtering in the browser. Figure out how to use filtering with server-side integration. --- # 📣 Infinite Table is Here 🎉 > Infinite Table is ready for prime time. With version 1.0.0 we're releasing a DataGrid that's feature packed and ready to be used in enterprise-grade apps Published: 2023-01-16 Author: admin Tags: product Canonical page: https://infinite-table.com/blog/2023/01/16/infinite-table-is-here _Infinite Table React is ready for prime time._ _With version 1.0.0 we're releasing a DataGrid that's feature packed and ready to be used in the wild!_ 1️⃣ seriously fast 2️⃣ no empty or white rows while scrolling 3️⃣ packed with features 4️⃣ built from the ground up for React 5️⃣ clear, concise and easily composable props & API We think you'll love Infinite Table. This is the DataGrid we would have loved to use more than 15 years ago when [we started working with tables in the browser](https://infinite-table.com/blog/2022/11/08/why-another-datagrid.md). And now it's finally here 🎉. ### Built from the Ground Up with React & TypeScript #### React all the Way Infinite Table feels native to React, not as a after-thought, but built with React fully in mind. It's declarative all the way and exposes everything as props, both controlled and uncontrolled. If you don't like the default behavior of a prop, use the controlled version and implement your own logic and handling - see for example the [following props related to column order](https://infinite-table.com/docs/reference/infinite-table-props.md#search=columnorder): - [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) - controlled property for managing order of columns - [`defaultColumnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnOrder) - uncontrolled version of the above - [`onColumnOrderChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnOrderChange) - callback prop for notifications and for updating controlled column order #### Fully Controlled React introduced controlled components to the wider community and we've been using them for years. It's where the power of React lies - giving the developer the flexibility to fully control (when needed) every input point of an app or component. All the props which Infinite Table exposes, have both controlled and uncontrolled versions. This allows you to start using the component very quickly and without much effort, but also with the all-important flexibility to fully control the component when needed, as your app grows and you need more control over the DataGrid. #### TypeScript & Generic Components Infinite Table is also built with TypeScript, giving you all the benefits of a great type system. In addition, the exposed components are exported as generic components, so you can specify the type of the data you're working with, for improved type safety. ```tsx import { InfiniteTable, DataSource } from '@infinite-table/infinite-react' type Person = { id: number, name: string, age: number} const data: Person[] = [ { id: 1, name: 'John', age: 25 }, //... ]; const columns = { id: { field: 'id' }, name: { field: 'name' }, } // ready to render data={data} primaryKey="id"> columns={columns} /> ``` ### Why Use Infinite Table, cont. #### Fast - virtualization Infinite Table is fast by leveraging **virtualization** both **vertically** (for rows) and **horizontally** (for columns). This means DOM nodes are created only for the visible cells, thus reducing the number of DOM nodes and associated memory strain and improving performance. #### No white space while scrolling - clever layout & rendering In addition to virtualization, we use clever layout & rendering techniques to avoid white space while scrolling. When you scroll, the table will not show any empty rows or white space - no matter how fast you're scrolling! We think this is one of the features that sets us apart from other components. We've spent a lot of time and effort making sure no whitespace is visible while scrolling the table. ### Batteries Included We want you to be productive immediately and stop worrying about the basics. Infinite Table comes with a lot of features out of the box, so you can focus on the important stuff. It helps you display huge datasets and get the most out of your data by providing you the right tools to enjoy these features: - [ sorting](https://infinite-table.com/docs/learn/sorting/overview.md) - [ row grouping](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md) - both server-side and client-side - [ pivoting](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md) - both server-side and client-side - [ aggregations](https://infinite-table.com/docs/learn/grouping-and-pivoting/grouping-rows.md#aggregations) - [ live pagination](https://infinite-table.com/docs/learn/working-with-data/live-pagination.md) - [ lazy loading](https://infinite-table.com/docs/learn/working-with-data/lazy-loading.md) - [ keyboard navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells.md) - [ fixed and flexible columns](https://infinite-table.com/docs/learn/columns/fixed-and-flexible-size.md) - [ column grouping](https://infinite-table.com/docs/learn/columns/column-grouping.md) - [ theming](https://infinite-table.com/docs/learn/theming/index.md) - ... and many others Infinite Table is built for companies and individuals who want to ship — faster 🏎! ### (Almost) No External Dependencies We've implemented everything from scratch and only directly depend on 2 packages (we'll probably get rid of them as well in the future) - all our dependecy graph totals a mere 3 packages. We've reduced external dependencies for 2 main reasons: - avoid security issues with dependencies (or dependencies of dependencies...you know it) - remember left-pad? - keep the bundle size small ### Composable API - with a small surface When building a component of this scale, there are two major opposing forces: - adding functionality - keeping the component (and the API) simple We're continually trying to reconcile both with Infinite Table, so we've built everything with composition in mind. A practical example of composition is favouring function props instead of boolean flags or objects. Why implement a feature under a boolean flag or a static object when you can expose a functionality via a function prop? The function prop can be used to handle more cases than any boolean flag could ever handle! A good example of composability is the [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) prop which controls the columns that are generated for grouping. It can be either a column object or a function: - when it's a column object, it makes the table render a single column for grouping (as if [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) was set to `"single-column"`) - when it's a function, it behaves like [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is set to `"multi-column"` and it's being called for each of the generated columns. ```tsx title="Group_column_as_an_object" ``` vs ```tsx title="Group_column_as_a_function" { // this allows you to affect all generated group columns in a single place // especially useful when the generated columns are dynamic or generated via a pivot return {...} }} /> ``` Our experience with other DataGrid components taught us that the more features you add, the more complex your API becomes. So we tried to keep the API surface as small as possible, while still offering a rich set of declarative props as building blocks that can be composed to accomplish more complex functionalities. ### Built for the community, available on NPM We're thrilled to share Infinite Table with the world. We wanted to make it very easy for everyone to [get started](https://infinite-table.com/docs/learn/getting-started/index.md) with it, so all you require is just an npm install: npm i @infinite-table/infinite-react The component will show a footer with a [Powered by Infinite Table](https://infinite-table.com) link displayed. However, all the functionalities are still available and fully working. So if you keep the link visible, you can use the component for free in any setup! Although you can use Infinite Table for free, we encourage you to [purchase a license](https://infinite-table.com/pricing) - buying a license will remove the footer link. This will help us keep delivering new features and improvements to the component and support you and your team going forward! Get started with Infinite Table and learn how to use it in your project. Get Infinite Table for your project and team! --- # Why Another React DataGrid? > Why is another DataGrid needed? A short history of datagrids and why Infinite Table is different Published: 2022-11-08 Author: admin Tags: product Canonical page: https://infinite-table.com/blog/2022/11/08/why-another-datagrid We've been working on finding better ways to display tabular data for over 2 decades now and collectively we have 35+ years of experience working on this. It all began on the desktop with a great range of DataGrids and then we moved to the web and the `` component - yeah, we've been around for quite some while - all the while dealing with the same problems and requirements again and again. This is the story of how we got to where we are today.... ## A (personal) History of DataGrids This article is not meant to be a complete history of DataGrids. Rather, it's personal reflections on the long journey the Infinite Table team have experienced while using and building components for displaying tabular data, culminating in Infinite Table, the modern declarative DataGrid for React. ## Desktop Components DataGrids have been around as long as any of us can remember. They are a vital tool which allows business users to visualise, edit, manage and personalise their data. Before Tim Berners-Lee and his colleagues changed the world for ever (and for a couple of decades after), "serious" business applications lived on the desktop. This was accompanied and facilitated by a plethora of great DataGrids from the likes of DevExpress, Telerik, Syncfusion, Infragistics and others. These products defined the feature-set that users came to expect in a DataGrid - row grouping, formatting, multiple sorting, pivoting etc. And which any DataGrid worth its salt today needs to offer today. For 2 decades and more these DataGrid repeatedly proved their worth in multiple changing desktop formats - MFC, WinForms, WPF and others. ## Enter the Browser And then the browser came along and, in time, everything changed. While it really took until HTML5 to convince most power users to move from the desktop to the web, the need to display tabular data in the browser was there right from the start. Initially the only way to show tabular data in the browser was to use the `
` component, and it was this piece of code that made it happen: ```css table-layout: fixed; ``` this is telling the browser ([see MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout#values)) that it shouldn't compute the space available for all rows & cells in the table before rendering but instead size the columns based on the content of the first row. This is speeding up the rendering time by quite a lot, and it's the early solution to the problem of rendering large data-sets. However, it was not perfect, and rendering **large** datasets was still a **huge** problem. Also, no fancy resizable / reorderable / stackable columns were available - at least not by default. These shortcomings were obvious to developers dealing with massive datasets, so various groups and companies started coming up with solutions. One such solution came from Yahoo! as part of their larger widget library called `YUI` (it was back in the days when Y! was a big deal). ### YUI DataTable Enter YUI era - launched in 2006, the Yahoo! User Interface Library was a step forward in reusability and component architecture. With the release of YUI 3, it received a modernized set of components, and the [YUI DataTable](https://clarle.github.io/yui3/yui/docs/datatable/) was probably the most advanced DataGrid solution out there. The component had a templating engine under the hood and allowed developers to customize some parts of the table. For its time, it was packed with functionality and was a great solution for many use-cases. It had a rich API, exposing lots of events, callbacks and methods for things like moving columns around, getting the data record for a given row, adding rows and columns, etc - all imperative code. The API was powerful and allowed developers to build complex solutions, but it was all stateful and imperative - something very normal for its epoch, but something we've learned to avoid in the last few years. Here's some code showcasing the YUI DataTable ```js {6} title="YUI DataTable with sorting" var table = new Y.DataTable({ columns: [ { key: 'item', width: '125px' }, { key: 'cost', formatter: '£: {value}', sortable: true, }, ], sortable: true, data: data, }).render('#example'); // to programatically sort table.sort({ cost: 'asc' }); ``` Notice in the code above, the component had support for custom formatters via a template (in the style of Mustache templates). YUI DataTable was a great component, certainly lacking some features by modern standards, but it was amazingly rich for its time. In some respects, it's still better than some of the modern DataGrids out there. The major missing piece is virtualization for both rows and columns in the table. A nice feature YUI DataTable had was the ability to separate the DataSource component and the data loading into a separate abstraction layer, so it would be somewhat decoupled from the main UI component. ```js var dataSource = new Y.DataSource.IO({ source: '/restaurants/fetch.php?', }); dataSource.plug(Y.Plugin.DataSourceXMLSchema, { schema: { resultListLocator: 'Result', resultFields: [{ key: 'Title' }, { key: 'Phone' }, { key: 'Rating' }], }, }); var table = new Y.DataTable({ columns: ['Title', 'Phone', 'Rating'], summary: 'Chinese restaurants near 98089', }); table.plug(Y.Plugin.DataTableDataSource, { datasource: dataSource, initialRequest: 'zip=94089&query=chinese', }); ``` Infinite Table is getting this a step further and splitting the data loading and the rendering into two separate components - `` and ``: - the `` component is responsible for managing the data - fetching it, sorting, grouping, pivoting, filtering, etc and making it available via the React context to the UI component. - the `` component is responsible only for rendering the data. This means you can even use the `` component with another React component and implement your own rendering and virtualization. ```tsx primaryKey="id" data={...}> {/* if you wanted to, you can replace with your own custom component */} columns={...}> ``` This level of separation allows us to iterate more rapidly on new features and also makes testing 🧪 easier. ### ExtJS 3 The next solution we've worked with was [ExtJS version 3](https://docs.sencha.com/extjs/3.4.0/#!/api/Ext.grid.GridPanel), which was built on the legacy of YUI 3. At the time, back in 2010, it was the most advanced DataGrid solution out there - used for some of the most complex applications in the enterprise world, from CMSs to ERP systems. The ExtJS 3 DataGrid brought excellent product execution in a few areas: - the [documentation](https://docs.sencha.com/extjs/3.4.0/) was excellent for its time - very rich, easy to navigate and search, with useful examples. As a bonus, from the docs you had access to the source-code of all components, which was a nice addition. - it came together with a rich set of components for building complex UIs - grids, trees, combo-boxes, form inputs, menus, dialogs, etc. Powerful layout components were available, which allowed developers to build complex app layouts by composing components together - and everything felt like it was part of the same story, which it was. - enthusiastic community - the forums were very active and the community was writing lots of good plugins. ```js title="ExtJS 3 DataGrid code snippet" var grid = new Ext.grid.GridPanel({ // data fetching abstracted in a "Store" component store: new Ext.data.Store({ // ... }), // columns abstracted in a ColumnModel colModel: new Ext.grid.ColumnModel({ defaults: { width: 120, sortable: true, }, columns: [ { id: 'company', header: 'Company', width: 200, sortable: true, dataIndex: 'company', }, { header: 'Price', renderer: Ext.util.Format.usMoney, dataIndex: 'price', }, { header: 'Change', dataIndex: 'change' }, { header: '% Change', dataIndex: 'pctChange' }, { header: 'Last Updated', width: 135, dataIndex: 'lastChange', xtype: 'datecolumn', format: 'M d, Y', }, ], }), viewConfig: { forceFit: true, // Return CSS class to apply to rows depending upon data values getRowClass: function (record, index) { var c = record.get('change'); if (c < 0) { return 'price-fall'; } else if (c > 0) { return 'price-rise'; } }, }, sm: new Ext.grid.RowSelectionModel({ singleSelect: true }), // size need if not inside a layout width: 600, height: 300, }); ``` Building on the legacy of YUI 3, the ExtJS added virtualization to make the DataGrid perform well for large datasets - it really made the component fly - since there was no framework overhead, and ExtJS was working directly with the DOM, the scrolling experience was pretty smooth. Also ExtJS tried to make things declarative and you could describe most of your UI by nesting JavaScript objects into a root object. The idea was clever, but it was only applicable for the initial rendering and you had to write imperative code as soon as you wanted some changes after the initial render. It was while working on a project with ExtJS 3 and exploring everything it had to offer that we had the great idea 😅 that we should start writing a DataGrid component. We were digging deep into ExtJS source code, wrote a few plugins for it and then decided to take the challenge and build a brand new DataGrid 😱. It was supposed to take us just a few short months 😅... ## The React Revolution We were quite far in building the DataGrid component, with a dedicated templating engine under the hood (by the way, it was really good in comparison to similar solutions at that time), virtualization implemented and major functionalities finished ... when JSConf EU 2013 happened. ### JSConf EU 2013 We vividly remember [watching Pete Hunt talk about ReactJS and rethinking best practices](https://www.youtube.com/watch?v=x7cQ3mrcKaY) at JSConf EU 2013. [Watch video](https://www.youtube.com/embed/x7cQ3mrcKaY?start=25) By the time the presentation was finished we knew we had to do something. This declarative way of describing the UI got us hooked and we knew we had to **drop what we were doing and adopt React** for anything going forward. It proved to be the right decision and we were early adopters of [React](https://reactjs.org/). It was astonishing to us how easy it was to learn React at the time - only taking a few hours to fully grasp the mental model and start building reusable components. 2013 was the year we switched trajectory and went full-React with all our new projects. We went back to the drawing board and started our first experiments with a DataGrid component in React. While we were building the DataGrid in React we got side-tracked with other projects but we saw the same pattern again and again - people trying to implement the grid component again and again, in various projects. Most of those attempts either failed terribly or at best they were good-enough for a simple use-case. ### AG Grid It was around this time, in 2015, that [AG Grid](https://www.ag-grid.com/) was launched. And, wow, it was good - very good. We immediately adopted it in all kind of projects while still trying to find time on the side to build our own DataGrid solution, the React way, with a fully declarative API. We were inspired 🙏 by AG Grid, seeing the breadth of features it offers and its expansive growth. It is a feat of engineering which illustrates just how much the browser can be pushed by extensive use of virtualization - being able to render millions of rows and thousands of columns is no small feat. All this while keeping the performance similar as if it was rendering just a few rows and columns. [CodeSandbox demo](https://codesandbox.io/embed/infallible-waterfall-csjcns?fontsize=14&module=%2Findex.js&theme=dark) In the code above ([taken from AG Grid getting started page](https://www.ag-grid.com/javascript-data-grid/getting-started/#copy-in-application-code)), note that AG Grid is exposing its [API](https://www.ag-grid.com/javascript-data-grid/grid-api/) on the `gridOptions` object. The API is huge and allow you to do pretty much anything you want with the grid - in an imperative way, which is what you're probably looking for if you're not integrating with a library/framework like Angular or React. After vanilla JavaScript and Angular versions of AG Grid, a React version was finally released. It was a step in the right direction - to make AG Grid more declarative - though it was a thin wrapper around React, with all the renderers and API still being imperative and not feeling like the best fit inside a React app. A few years later, AG Grid finally released a `reactUI` [version](https://blog.ag-grid.com/react-ui-overview/), with tighter integration with React and a more declarative API ❤️ All this time other solutions popped up in the React community. ### React Table One such solution that got massive adoption from the community was [React Table](https://tanstack.com/table/v8/) - now rebranded as TanStack Table. It's growth began around 2018, around the time when headless UI components started to gain traction. React Table was one of the first popular headless UI components to be released - in the same category it's worth mentioning [Downshift](https://www.downshift-js.com/) (initially launched and popularized by [Kent C. Dodds](https://kentcdodds.com/)), which helped push headless UI components to the community. React Table is a great solution for people who want to build their own UI on top of it. Some of the benefits of headless UI approach you get from React Table are: - full control over markup and styles - supports all styling patterns (CSS, CSS-in-JS, UI libraries, etc) - smaller bundle-sizes. This flexibility and total control come with a cost of needing more setup, more code and more maintainance over time. Also complex features that might already be implemented in a full-featured DataGrid will need to be implemented again from scratch. However, we do think it's a great 💯 fit for some use-cases - we've used it ourselves successfully in some projects 🙏. But it's not for everyone, as in our experience, most teams today want to ship faster 🏎 and not spend time and mental energy on building their own UI. Notice in the code above how you're responsible for creating the markup for the table, the headers, column groups,the cells, etc. You have TOTAL control over every aspect of the component, but this means you have to own it! At the other end of the spectrum is AG Grid a full-featured DataGrid that offers all this out of the box. With Infinite Table, we're trying to strike a balance between these 2 very different approaches - by offering a declarative API that is easy to use and get started with, while still giving you the flexibility to customize the UI and the behavior of the component, via both controlled and uncontrolled props. Let's take a look at an example of a similar UI, this time built with Infinite Table. [CodeSandbox demo](https://codesandbox.io/s/infinite-table-with-column-groups-2nn8zc) ## Infinite Table All this time we kept an eye on other components out there to get inspired. We got fresh ideas from various teams and projects - either enterprise or open source - either full-fledged or headless components like [react-table](https://tanstack.com/table/v8/). We've learned a lot from all these projects we've worked with and we've put all the best ideas in Infinite Table. Infinite is the fruit of years of iteration, experimentation, failures and sweat on a product that we've poured our hearts in over the course of so many years. We've agonized over all our APIs and design decisions in order to make Infinite Table the best React DataGrid component out there. We're aware we're not there yet, but we're here to stay 👋 and keep getting better. We want to work closely with the community at large and get fresh ideas from other projects and teams. We can all be winners when we work together and respect each-other ❤️ It's amazing what happens when you focus on a problem for such a long time (yeah, we know 😱). We wanted to give up several times but kept pushing for over a decade. The result is a component that we're proud of and is already starting to be used by enterprise clients across many industries (more on that in a later blogpost). Here are some of the key areas where we believe Infinite Table shines: ### Ready to Use Infinite Table is ready to use out of the box - namely it's not headless. We target companies and individuals who want to ship — faster 🏎! We're aware you don't want to re-invent the wheel nor do you want to invest 6 months of your team to build a poor implementation of a DataGrid component that will be hard to maintain and will be a source of bugs and frustration. **You want to ship — and soon!**. If this is you and you are already using React then Infinite Table is written for you! ### Feels like React - Declarative API We want Infinite Table to feel at home in any React app. Everything about the DataGrid should be declarative - when you want to update the table, change a prop and the table will respond. No imperative API calls - we want you to be able to use Infinite Table in a way that feels natural to you and your team, so you can stay productive and use React everywhere in your frontend. Let's take for example how you would switch a column from a column group to another: ```tsx {35} title="Fully declarative way to update columns" function getColumns() { return { firstName: { field: 'firstName', width: 200, columnGroup: 'personalInfo', }, address: { field: 'address', width: 200, columnGroup: 'personalInfo', }, age: { field: 'age', columnGroup: 'about' } } as InfiniteTablePropColumns } const columnGroups = { personalInfo: { header: "Personal info" }, about: { header: "About" } }; function App() { const [columns, setColumns] = useState>(getColumns) const [colGroupForAddress, setColGroupForAddress] = useState('personalInfo') const toggle = () => { const cols = getColumns() const newColGroup = colGroupForAddress === 'personalInfo' ? 'about' : 'personalInfo' cols.address.columnGroup = newColGroup setColumns(cols) setColGroupForAddress(newColGroup) }} const btn = return <> {btn} data={...} primaryKey="id"> } ``` Note in the code above that in order to update the column group for the `address` column, we simply change the `columnGroup` prop of the column and then we update the state of the component. The table will automatically re-render and update the column group for the `address` column. This is a fully declarative way to update the table. You don't need to call any imperative API to update it - change the props and the table will reflect the changes. ### Fully Controlled React introduced controlled components to the wider community and we've been using them for years. It's were the power of React lies - it gives the developer the flexibility to fully control (when needed) every input point of an app or component. All the props Infinite Table is exposing have both controlled and uncontrolled versions. This allows you to start using the component very quickly and without much effort, but also gives you the flexibility to fully control the component when needed, as your app grows and you need more control over the DataGrid. ### Composable API with small API surface When building a complex component, there are two major opposing forces: - adding functionality and - keeping the component (and the API) simple. We're trying to reconcile both with Infinite Table so we've built everything with composition in mind. A practical example of composition is favoring function props instead of boolean flags or objects. Why implement a feature under a boolean flag or a static object when you can expose a functionality via a function prop? The function prop can be used to handle more cases than any boolean flag could ever handle! A good example of composability is the [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) prop - it can be a column object or a function. It control the columns that are generated for grouping: - when it's a column object, it makes the table render a single column for grouping (as if [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) was set to `"single-column"`) - when it's a function, it behaves like [`groupRenderStrategy`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is set to `"multi-column"` and it's being called for each of the generated columns. ```tsx title="Group column as an object" ``` vs ```tsx title="Group column as a function" { // this allows you to affect all generated group columns in a single place // especially useful when the generated columns are dynamic or generated via a pivot return {...} }} /> ``` We've learned from our experience with other DataGrid components that the more features you add, the more complex your API becomes. So we tried to keep the API surface as small as possible, while still offering a rich set of declarative props as building blocks that can be composed to accomplish more complex functionalities. ## Conclusion We're very excited to share our Infinite Table journey with you ❤️ 🤩 After years in the DataGrid space and working and agonizing on this component, we're happy to finally ship it 🛳 🚀. We're looking forward to receiving [your feedback](https://github.com/infinite-table/infinite-react/issues) and suggestions. We're here to stay and we're committed to improving Infinite Table and to make it your go-to React DataGrid component to help you ship — faster! All the while staying true to the community! --- # Quarterly Update - Autumn 2022 > Infinite Table update for Autumn 2022 - grid menus and new website Published: 2022-11-01 Author: admin Tags: product, menus Canonical page: https://infinite-table.com/blog/2022/11/01/infinite-table-monthly-update-october-2022 _In the autumn our focus was implementing a dedicated Menu component so it can be used for column menus and row context menu._ _In addition to that, we've been working on a new design for our website and getting everything ready for the release._ ## Summary Some new functionalities we added to InfiniteTable include: - column menus - support for tab navigation We redesigned our website in preparation for our **v1** release and public launch. To receive your free 3-month license, please email us at [admin@infinite-table.com](mailto:admin@infinite-table.com) while we're still working our way through to `1.0.0` ## New Features Here's what we worked on in the last two months: ### Menu component We've built a brand new Menu component for Infinite Table, which we're using as a column menu and in the very near future will be used for row context menus. ![Grid with menu](https://infinite-table.com/blogs/grid-with-menu.png) Our policy is to develop all our components in-house and own them in order not to introduce third-party dependencies and vulnerabilities. It also helps us keep the overall bundle size small (since we're sharing some utilities) so your apps are leaner. We have to confess menus are tricky - we made ours support any level of nesting. They're tricky because of the nesting, the smart alignment and containment they need to provide in order to be truly useful. The Infinite Menu can be aligned to different targets and using a multitude of anchoring positions, always taking into account the position with the most available space in relation to a container or a specified area. This makes it really flexible and powerful - we think you'll want to use it as standalone as well once it's documented. ### Tab navigation Previous versions of Infinite Table did not have support for tab navigation due to our heavy virtualized rendering (the visual order of the cells was not the same as the DOM order). With the latest release, Infinite Table can now handle tab navigation correctly. Column cells that render `` fields or any other focusable elements can now be reached with tab navigation if the column specifies a [`columns.contentFocusable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.contentFocusable) prop. --- # Quarterly Update - Summer 2022 > Infinite Table update for Summper 2022 - row selection, column rendering, group columns Published: 2022-09-01 Author: admin Tags: product Canonical page: https://infinite-table.com/blog/2022/09/01/infinite-table-monthly-update-august-2022 Over the summer, we continued our work on preparing for our official release, focusing mainly on adding new functionalities and documenting them thoroughly, together with enhancements to existing features. ## Summary We have implemented a few new functionalities, including: - [row selection is now available 🎉](#row-selection) - [column rendering pipeline](#column-rendering-pipeline) - [group columns are now sortable 🔃](#sortable-group-columns) And we have updated some of the existing features: - [group columns inherit](#enhanced-group-columns) styles and configuration - [column hiding when grouping](#column-hiding-when-grouping) - [group columns can be bound to a field](#group-columns-bound-to-a-field) - [using the column valueGetter in sorting](#column-valuegetter-in-sorting) We started working on column and context menus. We will first release fully customizable **column** menus to show/hide columns and to easily perform other operations on columns. This will be followed by **context** menus where you will be able to define your own custom actions on rows/cells in the table. --- Don't worry, the menus will be fully customizable, the menu items are fully replaceable with whatever you need, or you will be able to swap our menu component with a custom one of your own. ## New Features Here's what we shipped over the summer: ### Row Selection Row selection can be single or multiple, with or without a checkbox, with or without grouping and for a lazy or non-lazy `DataSource` - 😅 that was a long enumeration, but seriously, we think we got something great out there. You can specify the selection via the [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) (controlled) or [`defaultRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowSelection) (uncontrolled) props, and listen to changes via the [`onRowSelectionChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowSelectionChange) callback prop. [Multi row checkbox selection with grouping](https://codesandbox.io/s/infinite-table-multi-row-checkbox-selection-with-grouping-i9wi88) Single vs multiple selection, grouped or ungrouped data, checkbox selection, lazy selection - read about all the possible combinations you can use to fit your needs. ### Column Rendering Pipeline The rendering pipeline for columns is a series of functions defined on the column that are called while rendering. All the functions that have the word `render` in their name will be called with an object that has a `renderBag` property, which contains values that will be rendered. The default [`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) function (the last one in the pipeline) ends up rendering a few things: - a `value` - generally comes from the [field](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.field) the column is bound to - a `groupIcon` - for group columns - a `selectionCheckBox` - for columns that have [`columns.renderSelectionCheckBox`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) defined (combined with row selection) When the rendering process starts for a column cell, all the above end up in the `renderBag` object. For example: ```tsx {3,12} const column: InfiniteTableColumn = { valueGetter: () => 'world', renderValue: ({ value, renderBag, rowInfo }) => { // at this stage, `value` is 'world' and `renderBag.value` has the same value, 'world' return {value}; }, render: ({ value, renderBag, rowInfo }) => { // at this stage `value` is 'world' // but `renderBag.value` is world, as this was the value returned by `renderValue` return
Hello {renderBag.value}!
; }, }; ``` Read about how using the rendering pipeline helps your write less code. Here is the full list of the functions in the rendering pipeline, in order of invocation: 1.[`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) - doesn't have access to `renderBag` 2.[`columns.valueFormatter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueFormatter) - doesn't have access to `renderBag` 3.[`columns.renderGroupIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupIcon) - can use all properties in `renderBag` 4.[`columns.renderSelectionCheckBox`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox) - can use all properties in `renderBag` 5.[`columns.renderValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderValue) - can use all properties in `renderBag` 6.[`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue) - can use all properties in `renderBag` 7.[`columns.renderLeafValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderLeafValue) - can use all properties in `renderBag` 8.[`columns.render`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.render) - can use all properties in `renderBag` Additionally, the [`columns.components.ColumnCell`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.components.ColumnCell) custom component has access to the `renderBag` via [`useInfiniteColumnCell`](https://infinite-table.com/docs/reference/hooks/index.md#useInfiniteColumnCell) ### Sortable Group Columns When [groupRenderStrategy="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) is used, the group column is sortable by default if all the columns that are involved in grouping are sortable. Sorting the group column makes the `sortInfo` have a value that looks like this: ```ts const sortInfo = [{ field: ['stack', 'age'], dir: 1, id: 'group-by' }]; ``` When [groupRenderStrategy="multi-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy), each group column is sortable by default if the column with the corresponding field is sortable. In both single and multi group column render strategy, you can use the [`columns.sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortable) property to override the default behavior. ## Updated Features Here’s a list of Infinite Table functionalities that we enhanced in the last month: ### Enhanced Group Columns Group columns now inherit configuration from the columns bound to the field they are grouped by - if such columns exist. [Group column inherits style from related column](https://codesandbox.io/s/infinite-table-group-column-inherits-style-from-related-column-v16qfg) The generated group column(s) - can be one for all groups or one for each group - will inherit the `style`/`className`/renderers from the columns corresponding to the group fields themselves (if those columns exist). Additionally, there are other ways to override those inherited configurations, in order to configure the group columns: - use [`groupBy.column`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy.column) to specify how each grouping column should look for the respective field (in case of [groupRenderStrateg="multi-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy)) - use [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) prop - can be used as an object - ideal for when you have simple requirements and when [groupRenderStrateg="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy) - as a function that returns a column configuration - can be used like this in either single or multiple group render strategy ### Column Hiding when Grouping When grouping is enabled, you can choose to hide some columns. Here are the two main ways to do this: - use [`hideColumnWhenGrouped`](https://infinite-table.com/docs/reference/infinite-table-props.md#hideColumnWhenGrouped) - this will make columns bound to the group fields be hidden when grouping is active - use [`columns.defaultHiddenWhenGroupedBy`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultHiddenWhenGroupedBy) (also available on the column types, as [`columnTypes.defaultHiddenWhenGroupedBy`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnTypes.defaultHiddenWhenGroupedBy)) - this is a column-level property, so you have more fine-grained control over what is hidden and when. Valid values for [`columns.defaultHiddenWhenGroupedBy`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.defaultHiddenWhenGroupedBy) are: - `"*"` - when any grouping is active, hide the column that specifies this property - `true` - when the field this column is bound to is used in grouping, hides this column - `keyof DATA_TYPE` - specify an exact field that, when grouped by, makes this column be hidden - `{[k in keyof DATA_TYPE]: true}` - an object that can specify more fields. When there is grouping by any of those fields, the current column gets hidden. [Hide columns when grouping](https://codesandbox.io/s/infinite-table-hide-columns-when-grouping-41o64x) In addition, you can now use [`columns.renderGroupValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderGroupValue) and [`columns.renderLeafValue`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderLeafValue) for configuring the rendered value for grouped vs non-grouped rows. ### Column valueGetter in Sorting Columns allow you to define a [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) to change the value they are rendering (e.g. useful when the `DataSet` has nested objects). Previously, this value returned by [`columns.valueGetter`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) was not used when sorting the table. With the latest update, the value returned by [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter) is correctly used when sorting the grid locally. --- # Quarterly Update - Spring 2022 > Infinite Table update for Spring 2022 Published: 2022-08-01 Author: admin Tags: product Canonical page: https://infinite-table.com/blog/2022/08/01/infinite-table-monthly-update-july-2022 This spring, we've been hard at work preparing for our Autumn release. We have implemented a few new functionalities: - [column resizing](#column-resizing) is now available - [column reordering](#column-reordering) can be achieved via drag & drop - [keyboard navigation](#keyboard-navigation) with support for both row and cell navigation And we have updated some of the existing features: - [lazy grouping](#lazy-grouping) - expands lazy loaded rows correctly and - also the server response can contain multiple levels of `children`, which basically allows the backend to send more data for groups you don't want to load lazily - [column groups](#column-grouping) are now improved with support for proportional column resizing - [pivot columns](#pivoting) are now easier to style and customize At the end of the spring, we started working on row and cell selection and we've made good progress on it. Row selection is already implemented for non-lazy group data and we're working on integrating it with lazy group data (e.g groups lazily loaded from the server). Of course, it will have integration with checkbox selection. Multiple row selection will have 2 ways to select data: - via mouse/keyboard interaction - we've emulated the behavior you're used to from your Finder in MacOS. - via checkbox - this is especially useful when the table is configured with grouping. ## New Features ### Column Resizing By default columns are now resizable. You can control this at column level via [column.resizable](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.resizable) or at grid level via [`resizableColumns`](https://infinite-table.com/docs/reference/infinite-table-props.md#resizableColumns). Read more about how you can configure column resizing to fit your needs. [Resizable columns example](https://codesandbox.io/s/infinite-table-resizable-columns-example-gq0fnv) A nice feature is support for SHIFT resizing - which will share space on resize between adjacent columns - try it in the example above. ### Column Reordering Column order is a core functionality of `InfiniteTable` - read how you can leverage it in your app. The default column order is the order in which columns appear in the columns object, but you can specify a [`defaultColumnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultColumnOrder) or tightly control it via the controlled property [`columnOrder`](https://infinite-table.com/docs/reference/infinite-table-props.md#columnOrder) - use [`onColumnOrderChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onColumnOrderChange) to get notifications when columns are reordered by the user. [Column order](https://codesandbox.io/s/infinite-table-column-order-advanced-example-ro12mu) ### Keyboard Navigation Both cell and row navigation is supported - use [`keyboardNavigation`](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) to configure it. By default, cell navigation is enabled. [Keyboard navigation](https://codesandbox.io/s/infinite-table-keyboard-navigating-cells-uncontrolled-tvwkmq) ## Updated Features ### Lazy grouping Server side grouping has support for lazy loading - `InfiniteTable` will automatically load lazy rows that are configured as expanded. [Lazy loaded rows are properly expanded](https://codesandbox.io/s/infinite-table-lazy-grouping-with-expanded-rows-pkihtt) Another nice feature is the ability for a group node to also contain its direct children in the server response, which basically allows the backend to eagerly load data for certain groups. Lazy grouping (with or without batching) is an advanced feature that allows you to integrate with huge datasets without loading them into the browser. ### Column grouping Column grouping was enhanced with support for pinned columns. Now you can use them in combination. Column groups is a powerful way to arrange columns to fit your business requirements - read how easy it is to define them. [Column groups with pinning](https://codesandbox.io/s/infinite-table-column-groups-with-pinning-ks16dp) ### Pivoting Pivot columns are now easier to style and benefit from piped rendering to allow maximum customization. Pivoting is probably our most advanced use-case. We offer full support for server-side pivoting and aggregations. [Customized pivot columns](https://codesandbox.io/s/infinite-table-custom-rendering-for-pivot-p2ern7) --- # DataGrid Keyboard Navigation Published: 2022-06-24 Author: admin Tags: keyboard-navigation Canonical page: https://infinite-table.com/blog/2022/06/24/navigating-your-datagrid Using your keyboard to navigate around an app is crucial to moving fast and being productive. With version `0.3.6` Infinite Table added keyboard navigation to your favorite React DataGrid component. ## Navigating table cells By default, navigation is enabled for table cells - that means, as soon as the user clicks a cell, it becomes active and from that point on-wards, the user can use **arrow keys**, **page up/down** and **home/end** keys to navigate. Check out our [documentation for keyboard navigation](https://infinite-table.com/docs/learn/keyboard-navigation/navigating-cells.md) to see more demos and a complete reference guide. Pro tip: when in cell navigation mode, you can use the **`Shift` key** to navigate horizontally in combination with **page up/down** and **home/end keys**. In the example below, click a table cell and then use arrow keys to see keyboard navigation in action [Keyboard navigation is enabled by default](https://codesandbox.io/s/cell-keyboard-navigation-d3qrx1) Another nice feature of keyboard navigation for cells is that you can specify a default active cell - you do so by using `defaultActiveCell=[2,0]` - meaning the cell on row 2 and column 0 should be active initially. [Default cell selection](https://codesandbox.io/s/infinite-table-default-cell-selection-ohx8e3) ## Navigating table rows Besides cell navigation, row navigation is also available. Switch to row navigation mode by specifying `keyboardNavigation="row"` - the rest is similar: user clicks a row, which becomes the active row. Using arrow keys, page up/down and home/end works as expected. Having a default row set as active is also possible, via [defaultActiveRowIndex={2}](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultActiveRowIndex) - this means the row at index `2` should be initially rendered as active. [Keyboard navigation for rows with default selection](https://codesandbox.io/s/infinite-table-keyboard-navigation-for-rows-with-default-selection-ve1nbk) ## Controlling active row/cell Both cell and row navigation can be used as React uncontrolled and controlled behaviors. In the controlled version, you have to use [`onActiveCellIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveCellIndexChange) (or [`onActiveRowIndexChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onActiveRowIndexChange)) to respond to navigation changes and update the corresponding index. The example below demoes controlled cell navigation - initially starting with no active cell, and it updates the active cell as a result to user changes. This means you as a developer are responsible for updating the value when needed, as you no longer wish to leave this update to happen internally in the table. This makes controlled behavior excellent for advanced use-cases when you want to implement custom navigation logic. [Controlled cell navigation](https://codesandbox.io/s/infinite-table-controlled-cell-navigation-kjl4qx) ## Turning off keyboard navigation Disabling keyboard navigation is done by specifying [keyboardNavigation=false](https://infinite-table.com/docs/reference/infinite-table-props.md#keyboardNavigation) - this ensures the user can no longer interact with the table rows or cells via the keyboard. ## Theming There are a number of ways to customise the appearance of the element that highlights the active cell. The easiest is to override those three CSS variables: - `--infinite-active-cell-border-color--r` - the red component of the border color - `--infinite-active-cell-border-color--g` - the green component of the border color - `--infinite-active-cell-border-color--b` - the blue component of the border color The initial values for those are 77, 149 and215 respectively, so the border color is `rgb(77, 149, 215)`. In addition, the background color of the active cell highlight element is set to the same color as the border color (computed based on the above r, g and b variables), but with an opacity of `0.25`, configured via the `--infinite-active-cell-background-alpha` CSS variable. When the table is not focused, the opacity for the background color is set to `0.1`, which is the default value of the `--infinite-active-cell-background-alpha--table-unfocused` CSS variable. To summarize, use: - `--infinite-active-cell-border-color--r` - `--infinite-active-cell-border-color--g` - `--infinite-active-cell-border-color--b` to control border and background color of the active cell highlight element. See below a demo on how easy it is to customize the colors for the active element highlighter [Theming keyboard navigation](https://codesandbox.io/s/infinite-table-theming-keyboard-navigation-htukio) ## Enjoy Thanks for following us thus far - we appreciate feedback, so please to let us know if keyboard navigation is useful for you or how we could make it better. Please follow us [@get_infinite](https://twitter.com/get_infinite) to keep up-to-date with news about the product. Thank you. --- # Infinite Table Beta Launch 🚀 Published: 2022-06-15 Author: admin Tags: product Canonical page: https://infinite-table.com/blog/2022/06/15/infinite-launch-beta Today we are announcing the beta version (`0.3.12`) of `Infinite Table` ready to be used by early adopters. ## What the version offers - improved performance - improved light and dark themes - keyboard navigation for cells and rows - support for custom sorting - support for custom column rendering - and lots more! ### Future plans We intend to publish regular beta versions of `Infinite Table` in preparation for our formal launch planned for **September 2022**. # 🚀 --- # Infinite Table Alpha Launch 🚀 Published: 2021-12-10 Author: admin Tags: product Canonical page: https://infinite-table.com/blog/2021/12/10/infinite-launch Today we are announcing the alpha version (`0.0.7`) of `Infinite Table` ready to be used by early adopters - you can take it from npm npm i @infinite-table/infinite-react We're thrilled by the work done by the whole team and this is the result of years of their combined experience and passion 🎉! ### Future plans We have big plans for the future of `Infinite Table` - first we want to finish the current react implementation and see it widely used and wildly successful and then we can move on to other frontend libraries/frameworks. The **virtualization engine** we've built for this component is library agnostic so we'll have to port the **rendering** part to other platforms - which could prove to be a not-so-difficult task with all the experience we have in building this. # 🚀