Master Detail
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
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.
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.
View Mode
Fork Forkimport * 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. Loading the detail DataGrid data
const detailDataFn: DataSourceData<Developer> = ({ masterRowInfo, sortInfo, ... }) => { return Promise.resolve([...]) } <DataSource<Developer> data={detailDataFn}> {...} </DataSource>
COPY
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.
View Mode
Fork Forkimport * 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>) { return ( <div style={{ padding: 10, color: 'var(--infinite-cell-color)', background: 'var(--infinite-background)', height: '100%', display: 'flex', flexDirection: 'column', }} > <h3> Developers in {rowInfo.data?.name}, {rowInfo.data?.country} </h3> <DataSource<Developer> data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > <InfiniteTable<Developer> columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> </DataSource> </div> ); } 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={320} 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, }
Configuring the master-detail height#
In order to configure the height of the row details, you can use the
rowDetailHeight
prop. Configuring the row detail height
<InfiniteTable<City> columns={masterColumns} rowDetailHeight={500} rowDetailRenderer={renderDetail}
COPY
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
.View Mode
Fork Forkimport * 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. Using conditional row details
<InfiniteTable<City> columns={masterColumns} rowDetailHeight={500} rowDetailRenderer={renderDetail} isRowDetailEnabled={(rowInfo) => rowInfo.data.cityName.contains('i')}
COPY
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.View Mode
Fork Forkimport * 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, }