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.
Note
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.
type Person = {
name: string;
age: number;
country: string;
id: string;
}
const groupBy = [{field: 'country'}]
<DataSource<Person> groupBy={groupBy}>
<InfiniteTable<Person> />
</DataSource>
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.
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 to find out more.
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<Developer> = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}</>, }, }, ]; const columns: InfiniteTablePropColumns<Developer> = { 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 ( <DataSource<Developer> data={dataSource} primaryKey="id" groupBy={groupBy}> <InfiniteTable<Developer> columns={columns} /> </DataSource> ); } const dataSource = () => { return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id
In groupBy.column
you can use any column property - so, for example, you can define a custom renderValue
function to customize the rendering.
const groupBy = [
{
field: 'country',
column: {
renderValue: ({ value }) => <>Country: {value}</>,
},
},
];
Note
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
to specify how each grouping column should look for the respective field (in case ofgroupRenderStrateg="multi-column"
) - use
groupColumn
prop- can be used as an object - ideal for when you have simple requirements and when
groupRenderStrateg="single-column"
- as a function that returns a column configuration - can be used like this in either single or multiple group render strategy
- can be used as an object - ideal for when you have simple requirements and when
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
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
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.
import { InfiniteTable, DataSource, DataSourcePropGroupBy, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy<Developer> = [ { field: 'age', column: { renderGroupValue: ({ value }: { value: any }) => `Age: ${value}`, }, }, { field: 'stack', }, ]; const columns: InfiniteTablePropColumns<Developer> = { country: { field: 'country', }, firstName: { field: 'firstName' }, age: { field: 'age' }, salary: { field: 'salary', type: 'number', }, canDesign: { field: 'canDesign' }, stack: { field: 'stack' }, }; export default function App() { return ( <DataSource<Developer> data={dataSource} primaryKey="id" groupBy={groupBy}> <InfiniteTable<Developer> columns={columns} columnDefaultWidth={150} /> </DataSource> ); } const dataSource = () => { return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers10k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id
For the multi-column
strategy, you can use hideEmptyGroupColumns
in order to hide columns for groups which are currently not visible.
import { InfiniteTable, DataSource, DataSourcePropGroupBy, InfiniteTablePropColumns, DataSourceProps, } from '@infinite-table/infinite-react'; import * as React from 'react'; const groupBy: DataSourcePropGroupBy<Developer> = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const domProps = { style: { flex: 1 }, }; const groupRowsState: DataSourceProps<Developer>['groupRowsState'] = { expandedRows: [], collapsedRows: true, }; const columns: InfiniteTablePropColumns<Developer> = { 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 [hideEmptyGroupColumns, setHideEmptyGroupColumns] = React.useState(true); return ( <div style={{ display: 'flex', flex: 1, color: 'var(--infinite-cell-color)', flexFlow: 'column', background: 'var(--infinite-background)', }} > <div style={{ padding: 10 }}> <label> <input type="checkbox" checked={hideEmptyGroupColumns} onChange={() => { setHideEmptyGroupColumns(!hideEmptyGroupColumns); }} /> Hide Empty Group Columns (make sure all `Stack` groups are collapsed to see it in action). Try to expand the group column to see the new group column being added on-the-fly. </label> </div> <DataSource<Developer> data={dataSource} primaryKey="id" defaultGroupRowsState={groupRowsState} groupBy={groupBy} > <InfiniteTable<Developer> domProps={domProps} hideEmptyGroupColumns={hideEmptyGroupColumns} groupRenderStrategy={'multi-column'} columns={columns} columnDefaultWidth={250} /> </DataSource> </div> ); } const dataSource = () => { return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers10') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id
Gotcha
You can specify an id
for group columns. This is helpful if you want to size those columns (via columnSizing
) or pin them (via 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
property to be single-column
(or specify 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
property to customize the generated column.
Note
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).
import { InfiniteTable, DataSource, DataSourcePropGroupBy, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { columns, Employee } from './columns'; const groupBy: DataSourcePropGroupBy<Employee> = [ { field: 'age', }, { field: 'companyName', }, ]; const groupColumn: InfiniteTableColumn<Employee> = { header: 'Grouping', defaultWidth: 250, // in this function we have access to collapsed info // and grouping info about the current row - see rowInfo.groupBy renderGroupValue: ({ value, rowInfo }) => { const groupBy = rowInfo.groupBy || []; const collapsed = rowInfo.collapsed; const currentGroupBy = groupBy[groupBy.length - 1]; if (currentGroupBy?.field === 'age') { return `🥳 ${value}${collapsed ? ' 🤷♂️' : ''}`; } return `🎉 ${value}`; }, }; export default function App() { return ( <DataSource<Employee> data={dataSource} primaryKey="id" groupBy={groupBy}> <InfiniteTable<Employee> groupRenderStrategy="single-column" groupColumn={groupColumn} columns={columns} columnDefaultWidth={150} /> </DataSource> ); } const dataSource = () => {
Note
If groupColumn
is specified to an object and no groupRenderStrategy
is passed, the render strategy will be single-column
.
groupColumn
can also be a function, which allows you to individually customize each group column - in case the multi-column
strategy is used.
Gotcha
You can specify an id
for the single groupColumn
. This is helpful if you want to size this column (via columnSizing
) or pin it (via 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 of the DATA_TYPE
?
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.
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<Developer> = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns<Developer> = { 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 ( <DataSource<Developer> data={dataSource} primaryKey="id" groupBy={groupBy}> <InfiniteTable<Developer> groupColumn={groupColumn} columns={columns} columnDefaultWidth={150} /> </DataSource> ); } const dataSource = () => { return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id
Use groupColumn
to customize rendering
The groupColumn
will inherit its own rendering and styling from the columns that are bound to the fields used in groupBy.field
. However, you can override any of those properties so you have full control over the rendering process.
const groupColumn = {
field: 'firstName',
renderGroupValue: ({ value }) => {
return `Group: ${value}`;
},
renderLeafValue: ({ value }) => {
return `First name: ${value}`;
},
};
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.
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<Developer> = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns<Developer> = { 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<Developer> = { field: 'firstName', renderValue: ({ value }) => { return `First name: ${value}`; }, }; export default function App() { return ( <DataSource<Developer> data={dataSource} primaryKey="id" groupBy={groupBy}> <InfiniteTable<Developer> groupColumn={groupColumn} columns={columns} columnDefaultWidth={250} /> </DataSource> ); } const dataSource = () => { return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id
Column rendering
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
- this will make columns bound to the group fields be hidden when grouping is active - use
columns.defaultHiddenWhenGroupedBy
(also available on the column types, ascolumnTypes.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
are:
"*"
- when any grouping is active, hide the column that specifies this propertytrue
- when the field this column is bound to is used in grouping, hides this columnkeyof 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.
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
is set to true
, so the stack
and preferredLanguage
columns are also hidden, since they are grouped by.
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<Developer> = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns<Developer> = { 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 ( <DataSource<Developer> data={dataSource} primaryKey="id" groupBy={groupBy}> <InfiniteTable<Developer> groupColumn={groupColumn} columns={columns} hideColumnWhenGrouped columnDefaultWidth={250} /> </DataSource> ); } const dataSource = () => { return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers1k') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id
Sorting the group column
When groupRenderStrategy="single-column"
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:
const sortInfo = [
{
dir: 1,
id: 'group-by',
field: ['stack', 'age'],
type: ['string', 'number'],
},
];
groupRenderStrategy="multi-column"
, each group column is sortable by default if the column with the corresponding field is sortable.
Note
The columnDefaultSortable
property can be used to override the default behavior.
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<Developer> = [ { field: 'stack', }, { field: 'preferredLanguage', }, ]; const columns: InfiniteTablePropColumns<Developer> = { 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<Developer> = [ { field: ['stack', 'preferredLanguage'], dir: -1, id: 'group-by', }, ]; export default function App() { return ( <DataSource<Developer> data={dataSource} primaryKey="id" defaultSortInfo={defaultSortInfo} groupBy={groupBy} > <InfiniteTable<Developer> groupColumn={groupColumn} columns={columns} hideColumnWhenGrouped columnDefaultWidth={250} /> </DataSource> ); } const dataSource = () => { return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers10') .then((r) => r.json()) .then((data: Developer[]) => data); }; type Developer = { id
Note
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
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
property. See the example below
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<Developer> = { salary: { field: 'salary', ...avgReducer, }, age: { field: 'age', ...avgReducer, }, }; const columns: InfiniteTablePropColumns<Developer> = { 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<Developer> = { 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<Developer>) => { 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<Developer>[] = React.useMemo( () => [ { field: 'country', }, { field: 'stack' }, ], [], ); return ( <DataSource<Developer> data={dataSource} primaryKey="id" defaultGroupRowsState={defaultGroupRowsState} aggregationReducers={aggregationReducers} groupBy={groupBy} > <InfiniteTable<Developer> groupRenderStrategy="single-column" groupColumn={groupColumn} columns={columns} columnDefaultWidth={150} /> </DataSource> ); } const dataSource = () => {
Each reducer from the aggregationReducers
map can have the following properties:
field
- the field to aggregate ongetter(data)
- a value-getter function, if the aggregation values are are not mapped directly to afield
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 astring
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 aggregationname
- useful especially in combination withpivotBy
, 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.
Gotcha
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
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<Developer> = { salary: { field: 'salary', ...avgReducer, }, age: { field: 'age', ...avgReducer, }, }; const columns: InfiniteTablePropColumns<Developer> = { 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<Developer> = (arg) => { const column = {} as Partial<InfiniteTableGroupColumnBase<Developer>>; if (arg.groupIndexForColumn === arg.groupBy.length - 1) { column.render = (param: InfiniteTableColumnCellContextType<Developer>) => { 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<Developer>[] = React.useMemo( () => [ { field: 'country', }, { field: 'stack' }, ], [], ); return ( <DataSource<Developer> data={dataSource} primaryKey="id" defaultGroupRowsState={defaultGroupRowsState} aggregationReducers={aggregationReducers} groupBy={groupBy} > <InfiniteTable<Developer> groupRenderStrategy="multi-column" groupColumn={groupColumn} columns={columns} columnDefaultWidth={250} /> </DataSource> ); } const dataSource = () => {
Aggregations
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
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 groupdata
- 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 groupdata
- an object that describes the common properties of the groupaggregations
- 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 rowstotalCount
- 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):
//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:
//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:
//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",
//...
},
//...
]
}
Note
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.
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<Developer> = { salary: { name: 'Salary (avg)', field: 'salary', reducer: 'avg', }, age: { name: 'Age (avg)', field: 'age', reducer: 'avg', }, }; const columns: InfiniteTablePropColumns<Developer> = { 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<Developer>[] = React.useMemo( () => [ { field: 'country', }, { field: 'city' }, { field: 'stack' }, ], [], ); return ( <DataSource<Developer> primaryKey="id" data={dataSource} groupBy={groupBy} aggregationReducers={aggregationReducers} defaultGroupRowsState={groupRowsState} lazyLoad={true} > <InfiniteTable<Developer> scrollStopDelay={10} hideEmptyGroupColumns columns={columns} columnDefaultWidth={220} /> </DataSource> ); } const dataSource: DataSourceData<Developer> = ({ 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
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.
Note
This can be useful in combination with using dataParams.groupRowsState
from the data
function - so your datasource can know which groups are expanded, and thus it can serve those groups already loaded with children.
//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"],
}
//...
]
}