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 prop. Specyfing the default row detail state
const defaultRowDetailState = { collapsedRows: true, expandedRows: [39, 54], }; <InfiniteTable columns={...} defaultRowDetailState={defaultRowDetailState} rowDetailRenderer={...}
COPY
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.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, }
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 prop, or its controlled counterpart - the rowDetailState prop.The row detail state can be defined in two ways:
- either specify
collapsedRows: true(which means all rows are collapsed by default) and specify an array ofexpandedRows, which will contain the ids of the rows that should be rendered as expanded.
const defaultRowDetailState = {
collapsedRows: true,
expandedRows: ['id-1', 'id-2', 'id-56'],
}; COPY
- or specify
expandedRows: true(which means all rows are expanded by default) and specify an array ofcollapsedRows, which will contain the ids of the rows that should be rendered as collapsed.
const rowDetailState = {
expandedRows: true,
collapsedRows: ['id-1', 'id-2', 'id-56'],
}; COPY
You can pass these objects into either the
defaultRowDetailState (uncontrolled) or the rowDetailState (controlled).If you're using the controlled
rowDetailState prop, you'll need to respond to user interaction by listening to onRowDetailStateChange and updating the value of 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 or rowDetailState props. 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],
});
<InfiniteTable<DATA_TYPE> rowDetailState={rowDetailState} />; COPY
Passing an object literal to the InfiniteTable
<InfiniteTable<DATA_TYPE>
rowDetailState={{
collapsedRows: true,
expandedRows: [2, 3, 4],
}}
/> COPY
See our type definitions for more details on row detail state.
Some of the rows in the master DataGrid are expanded by default.
We use the controlled
rowDetailState prop to manage the state of the row details and update it by using onRowDetailStateChange.View Mode
Fork Forkimport * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, InfiniteTableRowInfo, RowDetailStateObject, InfiniteTableApi, } 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 () => { const [rowDetailState, setRowDetailState] = React.useState<RowDetailStateObject>({ collapsedRows: true as const, expandedRows: [39, 54], }); const [api, setApi] = React.useState<InfiniteTableApi<City> | null>(null); return ( <> <div style={{ display: 'flex', flexFlow: 'row', color: 'var(--infinite-cell-color)', background: 'var(--infinite-background)', maxHeight: 200, overflow: 'auto', gap: 10, }} > <code style={{ paddingInline: 10 }}> <pre>Row detail state: {JSON.stringify(rowDetailState, null, 2)}</pre> </code> <div style={{ display: 'flex', gap: 10, padding: 10, alignItems: 'flex-start', }} > <button onClick={() => api?.rowDetailApi.expandAllDetails()}> Expand All </button> <button onClick={() => { // we could use the api to collapse all details // api?.rowDetailApi.collapseAllDetails(); // but we can also use the controlled prop setRowDetailState({ collapsedRows: true, expandedRows: [], }); }} > Collapse All </button> </div> </div> <DataSource<City> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > <InfiniteTable<City> domProps={domProps} onReady={({ api }) => { setApi(api); }} columnDefaultWidth={150} rowDetailState={rowDetailState} onRowDetailStateChange={(rowDetailState) => { setRowDetailState(rowDetailState.getState()); }} 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, }
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 prop.This function is called with only one argument - the new
rowDetailState. Please note this is an instance of RowDetailState. If you want to use the object literal, make sure you call getState() on the instance of RowDetailState. Using the onRowDetailStateChange listener
function App() { const [rowDetailState, setRowDetailState] = React.useState<RowDetailStateObject>({ collapsedRows: true as const, expandedRows: [39, 54], }); return <DataSource<DATA_TYPE> {...}> <InfiniteTable<DATA_TYPE> rowDetailState={rowDetailState} onRowDetailStateChange={(rowDetailStateInstance) => { setRowDetailState(rowDetailStateInstance.getState()); }} columnMinWidth={50} columns={masterColumns} rowDetailHeight={200} rowDetailRenderer={renderDetail} /> </DataSource> }
COPY
When using the controlled
rowDetailState, you'll need to respond to the user interaction by using the onRowDetailStateChange listener, in order to update the controlled 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 prop.const [rowDetailState, setRowDetailState] =
React.useState<RowDetailStateObject>({
collapsedRows: true,
expandedRows: [39, 54],
});
const expandAll = () => {
setRowDetailState({
collapsedRows: [],
expandedRows: true,
});
};
const collapseAll = () => {
setRowDetailState({
collapsedRows: true,
expandedRows: [],
});
};
return (
<>
<button onClick={expandAll}>Expand All</button>
<button onClick={collapseAll}>Collapse All</button>
<InfiniteTable<DATA_TYPE> rowDetailState={rowDetailState} />
</>
COPY
If you prefer the more imperative approach, you can still use the Row Detail API to
expand or collapse details for rows.Single row expand#
Using the controlled
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.
In this demo we allow only one row to be expanded at any given time.
We use the controlled
rowDetailState prop to manage the state of the row details and update it by using onRowDetailStateChange.View Mode
Fork Forkimport * as React from 'react'; import { DataSourceData, InfiniteTable, InfiniteTablePropColumns, DataSource, RowDetailStateObject, 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<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 RowDetail() { const masterRowInfo = useMasterRowInfo<City>(); if (!masterRowInfo) { return null; } console.log('rendering detail for master row', masterRowInfo.id); return ( <DataSource<Developer> data={detailDataSource} primaryKey="id" shouldReloadData={shouldReloadData} > <InfiniteTable<Developer> columnDefaultWidth={150} columnMinWidth={50} columns={detailColumns} /> </DataSource> ); } const components = { RowDetail, }; export default () => { const [rowDetailState, setRowDetailState] = React.useState< RowDetailStateObject<any> >({ collapsedRows: true as const, expandedRows: [39], }); return ( <> <code style={{ padding: 10, color: 'var(--infinite-cell-color)', background: 'var(--infinite-background)', maxHeight: 200, overflow: 'auto', }} > <pre> Current expanded row:{' '} {rowDetailState.expandedRows === true ? '---' : rowDetailState.expandedRows[0] ?? 'none'} </pre> </code> <DataSource<City> data={citiesDataSource} primaryKey="id" defaultSortInfo={[ { field: 'country', dir: 1, }, { field: 'name', dir: 1, }, ]} > <InfiniteTable<City> domProps={domProps} onReady={({ api }) => { console.log(api.rowDetailApi); }} columnDefaultWidth={150} rowDetailState={rowDetailState} onRowDetailStateChange={(newRowDetailState) => { const newState = newRowDetailState.getState(); const expandedRows = newState.expandedRows === true ? [] : newState.expandedRows; const oldExpandedRows = new Set( rowDetailState.expandedRows === true ? [] : rowDetailState.expandedRows, ); for (const row of expandedRows) { if (!oldExpandedRows.has(row)) { setRowDetailState({ collapsedRows: true, expandedRows: [row], }); return; } } setRowDetailState(newState); }} columnMinWidth={50} columns={masterColumns} components={components} /> </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, }