Master Detail
The React DataGrid that Infinite Table offers has native support for master-detail rows.
Note
The single most important property for the master-detail DataGrid configuration is the 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
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.
Note
It's very imporant that the rowDetailRenderer
function prop you pass into <InfiniteTable />
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.
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.
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<City> = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns<Developer> = { 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<City>) { console.log('rendering detail for master row', rowInfo.id); return ( <DataSource<Developer> data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > <InfiniteTable<Developer> columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> </DataSource> ); } export default () => { return ( <> <DataSource<City> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > <InfiniteTable<City> domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} rowDetailRenderer={renderDetail} /> </DataSource> </> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData<City> = () => { const cityNames = new Set<string>(); const result: City[] = []; return fetch('https://infinite-table.com/.netlify/functions/json-server' + `/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<Developer> = ({ 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, }
If you want to use a component instead of the rowDetailRenderer
function, you can use the components.RowDetail
property. This works similarly and makes the DataGrid be considered master-detail. Inside the component, you can use the 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
function for the detail <DataSource />
will be called with the masterRowInfo
as a property available in the object passed as argument.
const detailDataFn: DataSourceData<Developer> = ({
masterRowInfo,
sortInfo,
...
}) => {
return Promise.resolve([...])
}
<DataSource<Developer> data={detailDataFn}>
{...}
</DataSource>
You can see the live example above for more details.
Rendering a detail DataGrid
Using the 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.
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.
Configuring the master-detail height
In order to configure the height of the row details, you can use the rowDetailHeight
prop.
<InfiniteTable<City>
columns={masterColumns}
rowDetailHeight={500}
rowDetailRenderer={renderDetail}
The default value for the rowDetailHeight
is 300
px.
rowDetailHeight
can be one of the following:
number
- the height in pixelsstring
- 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 therowInfo object
.
This master-detail DataGrid is configured with a custom rowDetailHeight
of 200px
.
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<City> = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns<Developer> = { 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<City>) { console.log('rendering detail for master row', rowInfo.id); return ( <DataSource<Developer> data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > <InfiniteTable<Developer> columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> </DataSource> ); } const defaultRowDetailState = { collapsedRows: true as const, expandedRows: [39, 54], }; export default () => { return ( <> <DataSource<City> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > <InfiniteTable<City> domProps={domProps} columnDefaultWidth={150} defaultRowDetailState={defaultRowDetailState} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> </DataSource> </> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData<City> = () => { const cityNames = new Set<string>(); const result: City[] = []; return fetch('https://infinite-table.com/.netlify/functions/json-server' + `/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<Developer> = ({ 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, }
Conditional row details
Not all rows in a DataGrid need to have details. To configure which rows have details, you can use the isRowDetailEnabled
function prop.
<InfiniteTable<City>
columns={masterColumns}
rowDetailHeight={500}
rowDetailRenderer={renderDetail}
isRowDetailEnabled={(rowInfo) => rowInfo.data.cityName.contains('i')}
The isRowDetailEnabled
function prop is called with the rowInfo object
and is expected to return a boolean value.
This example shows a master DataGrid with cities & countries.
Not all rows have details - every other row is configured without details via the isRowDetailEnabled
function prop.
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<City> = { id: { field: 'id', header: 'ID', defaultWidth: 70, renderRowDetailIcon: true, }, country: { field: 'country', header: 'Country' }, city: { field: 'name', header: 'City', defaultFlex: 1 }, }; const detailColumns: InfiniteTablePropColumns<Developer> = { 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<City>) { console.log('rendering detail for master row', rowInfo.id); return ( <DataSource<Developer> data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > <InfiniteTable<Developer> columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> </DataSource> ); } const isRowDetailEnabled = (rowInfo: InfiniteTableRowInfo<City>) => { return rowInfo.indexInAll % 2 === 0; }; export default () => { return ( <> <DataSource<City> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > <InfiniteTable<City> domProps={domProps} columnDefaultWidth={150} columnMinWidth={50} columns={masterColumns} isRowDetailEnabled={isRowDetailEnabled} rowDetailRenderer={renderDetail} /> </DataSource> </> ); }; // fetch an array of cities from the server const citiesDataSource: DataSourceData<City> = () => { const cityNames = new Set<string>(); const result: City[] = []; return fetch('https://infinite-table.com/.netlify/functions/json-server' + `/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<Developer> = ({ 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, }