Master Rows are the rows inside the Master Grid that can be expanded to display Detail Grids.
Static Master Rows Copy Link
Once a Master Grid is configured with masterDetail=true, all rows in the Master Grid behave as Master Rows, in that they can be expanded to display Detail Grids.
const gridOptions = {
// by itself, all rows will be expandable
masterDetail: true,
// other grid options ...
}Because Static Master Rows are used in all the basic examples of Master / Detail, another example is not given here.
Dynamic Master Rows Copy Link
Dynamic Master Rows allows specifically deciding what rows in the Master Grid can be expanded. This can be useful if, for example, a Master Row has no child records, then it may not be desirable to allow expanding the Master Row.
To specify which rows should expand, provide the grid callback isRowMaster. The callback will be called once for each row. Return true to allow expanding and false to disallow expanding for that row.
Callback to be used with Master Detail to determine if a row should be a master row. If false is returned no detail row will exist for this row. |
const gridOptions = {
// turn on master detail
masterDetail: true,
// specify which rows to expand
isRowMaster: dataItem => {
return expandThisRow ? true : false;
},
// other grid options ...
}The following example only shows detail rows when there are corresponding child records.
import {
ClientSideRowModelModule,
FirstDataRenderedEvent,
GridApi,
GridOptions,
IDetailCellRendererParams,
ModuleRegistry,
RowApiModule,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
MasterDetailModule,
} from "ag-grid-enterprise";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
RowApiModule,
ClientSideRowModelModule,
ColumnsToolPanelModule,
MasterDetailModule,
ColumnMenuModule,
ContextMenuModule,
]);
let gridApi: GridApi;
const gridOptions: GridOptions = {
masterDetail: true,
isRowMaster: (dataItem: any) => {
return dataItem ? dataItem.callRecords.length > 0 : false;
},
columnDefs: [
// group cell renderer needed for expand / collapse icons
{ field: "name", cellRenderer: "agGroupCellRenderer" },
{ field: "account" },
{ field: "calls" },
{ field: "minutes", valueFormatter: "x.toLocaleString() + 'm'" },
],
defaultColDef: {
flex: 1,
},
detailCellRendererParams: {
detailGridOptions: {
columnDefs: [
{ field: "callId" },
{ field: "direction" },
{ field: "number", minWidth: 150 },
{ field: "duration", valueFormatter: "x.toLocaleString() + 's'" },
{ field: "switchCode", minWidth: 150 },
],
defaultColDef: {
flex: 1,
},
},
getDetailRowData: function (params) {
params.successCallback(params.data.callRecords);
},
} as IDetailCellRendererParams<IAccount, ICallRecord>,
onFirstDataRendered: onFirstDataRendered,
};
function onFirstDataRendered(params: FirstDataRenderedEvent) {
// arbitrarily expand a row for presentational purposes
setTimeout(() => {
params.api.getDisplayedRowAtIndex(1)!.setExpanded(true);
}, 0);
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/master-detail-dynamic-data.json")
.then((response) => response.json())
.then(function (data) {
gridApi!.setGridOption("rowData", data);
});
<div id="myGrid" style="height: 100%"></div>
Changing Dynamic Master Rows Copy Link
The callback isRowMaster is re-called after data changes in the row as a result of a Transaction Update. This gives the opportunity to change whether the row is expandable or not.
// to get isRowMaster called again, update the row using a Transaction Update
const transaction = { update: [ updatedRecord1, updatedRecord2 ] };
gridApi.applyTransaction(transaction);In the example below, only Master Rows that have data to show are expandable. Note the following:
- Row 'Nora Thomas' has no detail records, thus is not expandable.
- Row 'Mila Smith' has detail records, thus is expandable.
- Clicking 'Clear Mila Calls' removes detail records from Mila Smith which results in the Mila Smith row no longer being a Master Row.
- Clicking 'Set Mila Calls' sets detail records from Mila Smith which results in the Mila Smith becoming a Master Row.
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
FirstDataRenderedEvent,
GetRowIdParams,
GridApi,
GridOptions,
IDetailCellRendererParams,
ModuleRegistry,
RowApiModule,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
MasterDetailModule,
} from "ag-grid-enterprise";
import { IAccount, ICallRecord } from "./interfaces";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelApiModule,
RowApiModule,
ClientSideRowModelModule,
ColumnsToolPanelModule,
MasterDetailModule,
ColumnMenuModule,
ContextMenuModule,
]);
let gridApi: GridApi<IAccount>;
const gridOptions: GridOptions<IAccount> = {
masterDetail: true,
isRowMaster: (dataItem: any) => {
return dataItem ? dataItem.callRecords.length > 0 : false;
},
columnDefs: [
// group cell renderer needed for expand / collapse icons
{ field: "name", cellRenderer: "agGroupCellRenderer" },
{ field: "account" },
{ field: "calls" },
{ field: "minutes", valueFormatter: "x.toLocaleString() + 'm'" },
],
defaultColDef: {
flex: 1,
},
getRowId: (params: GetRowIdParams) => String(params.data.account),
detailCellRendererParams: {
detailGridOptions: {
columnDefs: [
{ field: "callId" },
{ field: "direction" },
{ field: "number", minWidth: 150 },
{ field: "duration", valueFormatter: "x.toLocaleString() + 's'" },
{ field: "switchCode", minWidth: 150 },
],
defaultColDef: {
flex: 1,
},
},
getDetailRowData: (params) => {
params.successCallback(params.data.callRecords);
},
} as IDetailCellRendererParams<IAccount, ICallRecord>,
onFirstDataRendered: onFirstDataRendered,
};
function onFirstDataRendered(params: FirstDataRenderedEvent) {
// arbitrarily expand a row for presentational purposes
setTimeout(() => {
params.api.getDisplayedRowAtIndex(1)!.setExpanded(true);
}, 0);
}
function onBtClearMilaCalls() {
const milaSmithRowNode = gridApi!.getRowNode("177001")!;
const milaSmithData = milaSmithRowNode.data!;
milaSmithData.callRecords = [];
milaSmithData.calls = milaSmithData.callRecords.length;
gridApi!.applyTransaction({ update: [milaSmithData] });
}
function onBtSetMilaCalls() {
const milaSmithRowNode = gridApi!.getRowNode("177001")!;
const milaSmithData = milaSmithRowNode.data!;
milaSmithData.callRecords = [
{
name: "susan",
callId: 579,
duration: 23,
switchCode: "SW5",
direction: "Out",
number: "(02) 47485405",
},
{
name: "susan",
callId: 580,
duration: 52,
switchCode: "SW3",
direction: "In",
number: "(02) 32367069",
},
];
milaSmithData.calls = milaSmithData.callRecords.length;
gridApi!.applyTransaction({ update: [milaSmithData] });
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/master-detail-dynamic-data.json")
.then((response) => response.json())
.then(function (data) {
gridApi!.setGridOption("rowData", data);
});
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).onBtClearMilaCalls = onBtClearMilaCalls;
(<any>window).onBtSetMilaCalls = onBtSetMilaCalls;
}
<div style="display: flex; flex-direction: column; height: 100%">
<div style="padding-bottom: 4px">
<button onclick="onBtClearMilaCalls()">Clear Mila Calls</button>
<button onclick="onBtSetMilaCalls()">Set Mila Calls</button>
</div>
<div id="myGrid" style="flex: 1 1 0px"></div>
</div>
export interface ICallRecord {
name: string;
callId: number;
duration: number;
switchCode: string;
direction: string;
number: string;
}
export interface IAccount {
name: string;
account: number;
calls: number;
minutes: number;
callRecords: ICallRecord[];
} The example below extends the previous example. It demonstrates a common scenario of the Master Row controlling the Detail Rows. Note the following:
Each Master Row has buttons to add or remove one detail row.
Clicking 'Add' will:
- Add one detail row.
- Ensure the Master Row is expandable.
- Ensure the Master Row is expanded (i.e. the Detail Grid is visible).
Clicking 'Remove' will:
- Remove one detail row.
- If no detail rows exist, ensure Master Row is not expandable
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
FirstDataRenderedEvent,
GetRowIdParams,
GridApi,
GridOptions,
IDetailCellRendererParams,
ModuleRegistry,
RowApiModule,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
MasterDetailModule,
} from "ag-grid-enterprise";
import { CallsCellRenderer } from "./callsCellRenderer";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
RowApiModule,
ClientSideRowModelModule,
ClientSideRowModelApiModule,
ColumnsToolPanelModule,
MasterDetailModule,
ColumnMenuModule,
ContextMenuModule,
]);
let gridApi: GridApi;
const gridOptions: GridOptions = {
masterDetail: true,
isRowMaster: (dataItem: any) => {
return dataItem ? dataItem.callRecords.length > 0 : false;
},
columnDefs: [
// group cell renderer needed for expand / collapse icons
{ field: "name", cellRenderer: "agGroupCellRenderer" },
{ field: "account" },
{ field: "calls", cellRenderer: CallsCellRenderer },
{ field: "minutes", valueFormatter: "x.toLocaleString() + 'm'" },
],
defaultColDef: {
flex: 1,
},
getRowId: (params: GetRowIdParams) => String(params.data.account),
detailCellRendererParams: {
detailGridOptions: {
columnDefs: [
{ field: "callId" },
{ field: "direction" },
{ field: "number", minWidth: 150 },
{ field: "duration", valueFormatter: "x.toLocaleString() + 's'" },
{ field: "switchCode", minWidth: 150 },
],
defaultColDef: {
flex: 1,
},
},
getDetailRowData: (params) => {
params.successCallback(params.data.callRecords);
},
} as IDetailCellRendererParams<IAccount, ICallRecord>,
onFirstDataRendered: onFirstDataRendered,
};
function onFirstDataRendered(params: FirstDataRenderedEvent) {
// arbitrarily expand a row for presentational purposes
setTimeout(() => {
params.api.getDisplayedRowAtIndex(1)!.setExpanded(true);
}, 0);
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/master-detail-dynamic-data.json")
.then((response) => response.json())
.then(function (data) {
gridApi!.setGridOption("rowData", data);
});
.calls-cell-renderer button {
margin: 2px;
}
import type { ICellRendererComp, ICellRendererParams } from 'ag-grid-community';
export class CallsCellRenderer implements ICellRendererComp {
eGui!: HTMLElement;
eValue: any;
init(params: ICellRendererParams) {
const eTemp = document.createElement('div');
eTemp.innerHTML =
'<span class="calls-cell-renderer">' +
'<button data-ref="btAdd">+</button>' +
'<button data-ref="btRemove">-</button>' +
'<span data-ref="eValue"></span>' +
'</span>';
this.eGui = eTemp.firstChild as HTMLElement;
this.eValue = this.eGui.querySelector('[data-ref="eValue"]');
const btAdd = this.eGui.querySelector('[data-ref="btAdd"]')!;
const btRemove = this.eGui.querySelector('[data-ref="btRemove"]')!;
btAdd.addEventListener('click', this.onBtAdd.bind(this, params));
btRemove.addEventListener('click', this.onBtRemove.bind(this, params));
this.refresh(params);
}
onBtRemove(params: ICellRendererParams) {
const oldData = params.node.data;
const oldCallRecords = oldData.callRecords;
if (oldCallRecords.length == 0) {
return;
}
const newCallRecords = oldCallRecords.slice(0); // make a copy
newCallRecords.pop(); // remove one item
let minutes = 0;
newCallRecords.forEach(function (r: any) {
minutes += r.duration;
});
const newData = {
name: oldData.name,
account: oldData.account,
calls: newCallRecords.length,
minutes: minutes,
callRecords: newCallRecords,
};
params.api.applyTransaction({ update: [newData] });
}
onBtAdd(params: ICellRendererParams) {
const oldData = params.node.data;
const oldCallRecords = oldData.callRecords;
const newCallRecords = oldCallRecords.slice(0); // make a copy
newCallRecords.push({
name: ['Bob', 'Paul', 'David', 'John'][Math.floor(window.agRandom() * 4)],
callId: Math.floor(window.agRandom() * 1000),
duration: Math.floor(window.agRandom() * 100) + 1,
switchCode: 'SW5',
direction: 'Out',
number: '(02) ' + Math.floor(window.agRandom() * 1000000),
}); // add one item
let minutes = 0;
newCallRecords.forEach(function (r: any) {
minutes += r.duration;
});
const newData = {
name: oldData.name,
account: oldData.account,
calls: newCallRecords.length,
minutes: minutes,
callRecords: newCallRecords,
};
params.api.applyTransaction({ update: [newData] });
params.node.setExpanded(true);
}
refresh(params: ICellRendererParams) {
this.eValue.innerHTML = params.value;
return true;
}
getGui() {
return this.eGui;
}
}
<div id="myGrid" style="height: 100%"></div>
Opening Master Rows by Default Copy Link
Master Rows can be expanded by default using either masterDefaultExpanded or isMasterOpenByDefault.
Master Detail: set to the number of levels of master rows to expand by default, e.g. 0 for none, 1 for first level only, etc. Set to -1 to expand everything. If not set, falls back to groupDefaultExpanded. |
(Client-side Row Model only) Master Detail: allows master rows to be open by default. |
Set masterDefaultExpanded to the number of levels of Master Rows to expand by default, or -1 to expand all Master Rows. If not set, it falls back to groupDefaultExpanded.
const gridOptions = {
// expand all master rows by default
masterDefaultExpanded: -1,
};For finer control, provide the isMasterOpenByDefault callback. It is called once for each Master Row; return true to expand that row by default.
const gridOptions = {
// expand specific master rows by default
isMasterOpenByDefault: (params) => {
return params.data.shouldExpand;
},
};isMasterOpenByDefault applies to Master Rows, whereas isGroupOpenByDefault applies to group rows. When combining Master Detail with row grouping, each callback controls only its own row type.