This section shows how Tree Data can be used with the Server-Side Row Model.
Tree Data Copy Link
Tree Data is defined as data that has parent / child relationships where the parent / child relationships are provided as part of the data. This is in contrast to Row Grouping where the parent / child relationships are the result of grouping. When working with Tree Data, there are no columns getting grouped.
An example of a Tree Data JSON structure is shown below:
[{
"employeeId": 101,
"employeeName": "Erica Rogers",
"jobTitle": "CEO",
"employmentType": "Permanent",
"children": [{
"employeeId": 102,
"employeeName": "Malcolm Barrett",
"jobTitle": "Exec. Vice President",
"employmentType": "Permanent",
"children": [
{
"employeeId": 103,
"employeeName": "Leah Flowers",
"jobTitle": "Parts Technician",
"employmentType": "Contract"
},
{
"employeeId": 104,
"employeeName": "Tammy Sutton",
"jobTitle": "Service Technician",
"employmentType": "Contract"
}
]
}]
}]It is expected that the data set will be too large to send over the network, hence the SSRM is used to lazy-load child row as the parent rows are expanded.
Tree Data Mode Copy Link
In order to set the grid to work with Tree Data, simply enable Tree Data mode via the Grid Options using: gridOptions.treeData = true.
Supplying Tree Data Copy Link
Tree Data is supplied via the Server-Side Datasource just like flat data, however there are two additional gridOptions callbacks: isServerSideGroup(dataItem) and getServerSideGroupKey(dataItem).
SSRM Tree Data: Allows specifying which rows are expandable. |
SSRM Tree Data: Allows specifying group keys. |
The following code snippet shows the relevant gridOptions entries for configuring tree data with the server-side row model:
// choose Server-Side Row Model
const rowModelType = 'serverSide';
// enable Tree Data
const treeData = true;
// indicate if row is a group node
const isServerSideGroup = dataItem => {
return dataItem.group;
};
// specify which group key to use
const getServerSideGroupKey = dataItem => {
return dataItem.employeeId;
};
<AgGridReact
rowModelType={rowModelType}
treeData={treeData}
isServerSideGroup={isServerSideGroup}
getServerSideGroupKey={getServerSideGroupKey}
/>Be careful not to get mixed up with the Client-Side Tree Data configurations by mistake.
The example below shows this in action where the following can be noted:
- Tree Data is enabled with the Server-Side Row Model using
gridOptions.treeData = true. - Group nodes are determined using the callback
gridOptions.isServerSideGroup(dataItem). - Group keys are returned from the callback
gridOptions.getServerSideGroupKey(dataItem).
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GetServerSideGroupKey,
GridApi,
GridOptions,
GridReadyEvent,
ICellRendererParams,
IServerSideDatasource,
IServerSideGetRowsParams,
IServerSideGetRowsRequest,
IsServerSideGroup,
IsServerSideGroupOpenByDefault,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
];
function createFakeServer(fakeServerData: any[]) {
const fakeServer = {
data: fakeServerData,
getData: function (request: IServerSideGetRowsRequest) {
const extractRowsFromData: (groupKeys: string[], data: any[]) => any = (
groupKeys: string[],
data: any[],
) => {
if (groupKeys.length === 0) {
return data.map(function (d) {
return {
group: !!d.children,
employeeId: d.employeeId,
employeeName: d.employeeName,
employmentType: d.employmentType,
jobTitle: d.jobTitle,
};
});
}
const key = groupKeys[0];
for (let i = 0; i < data.length; i++) {
if (data[i].employeeId === key) {
return extractRowsFromData(
groupKeys.slice(1),
data[i].children.slice(),
);
}
}
};
return extractRowsFromData(request.groupKeys, this.data);
},
};
return fakeServer;
}
function createServerSideDatasource(fakeServer: any) {
const dataSource: IServerSideDatasource = {
getRows: (params: IServerSideGetRowsParams) => {
console.log("ServerSideDatasource.getRows: params = ", params);
const allRows = fakeServer.getData(params.request);
const request = params.request;
const doingInfinite = request.startRow != null && request.endRow != null;
const result = doingInfinite
? {
rowData: allRows.slice(request.startRow, request.endRow),
rowCount: allRows.length,
}
: { rowData: allRows };
console.log("getRows: result = ", result);
setTimeout(() => {
params.success(result);
}, 200);
},
};
return dataSource;
}
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "jobTitle" },
{ field: "employmentType" },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 240,
flex: 1,
sortable: false,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
field: "employeeName",
cellRendererParams: {
innerRenderer: (params: ICellRendererParams) => {
// display employeeName rather than group key (employeeId)
return params.data.employeeName;
},
},
};
}, []);
const isServerSideGroupOpenByDefault = useCallback(
(params: IsServerSideGroupOpenByDefaultParams) => {
// open first two levels by default
return params.rowNode.level < 2;
},
[],
);
const isServerSideGroup = useCallback((dataItem: any) => {
// indicate if node is a group
return dataItem.group;
}, []);
const getServerSideGroupKey = useCallback((dataItem: any) => {
// specify which group key to use
return dataItem.employeeId;
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/small-tree-data.json")
.then((resp) => resp.json())
.then((data: any[]) => {
const fakeServer = createFakeServer(data);
const datasource = createServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
treeData={true}
isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
isServerSideGroup={isServerSideGroup}
getServerSideGroupKey={getServerSideGroupKey}
onGridReady={onGridReady}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Refreshing Tree Data Copy Link
Tree Data can be refreshed in the same way as groups are refreshed when not using Tree Data. This is explained in the SSRM Refresh.
The example below shows this in action where the following can be noted:
- Click Refresh Everything to clear all caches by calling
api.refreshServerSide({ route: [], purge: true }). - Click Refresh ['Kathryn Powers','Mabel Ward'] to clear a single cache by calling
api.refreshServerSide({ route: ['Kathryn Powers','Mabel Ward'], purge: true }).
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GetServerSideGroupKey,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsParams,
IServerSideGetRowsRequest,
IsServerSideGroup,
IsServerSideGroupOpenByDefault,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
];
function createFakeServer(fakeServerData: any[]) {
const fakeServer = {
getData: (request: IServerSideGetRowsRequest) => {
const extractRowsFromData: (groupKeys: string[], data: any[]) => any = (
groupKeys: string[],
data: any[],
) => {
if (groupKeys.length === 0) {
return data.map(function (d) {
return {
group: !!d.underlings,
employeeId: d.employeeId + "",
employeeName: d.employeeName,
employmentType: d.employmentType,
startDate: d.startDate,
};
});
}
const key = groupKeys[0];
for (let i = 0; i < data.length; i++) {
if (data[i].employeeName === key) {
return extractRowsFromData(
groupKeys.slice(1),
data[i].underlings.slice(),
);
}
}
};
return extractRowsFromData(request.groupKeys, fakeServerData);
},
};
return fakeServer;
}
function createServerSideDatasource(fakeServer: any) {
const dataSource: IServerSideDatasource = {
getRows: (params: IServerSideGetRowsParams) => {
console.log("ServerSideDatasource.getRows: params = ", params);
const request = params.request;
const allRows = fakeServer.getData(request);
const doingInfinite = request.startRow != null && request.endRow != null;
const result = doingInfinite
? {
rowData: allRows.slice(request.startRow, request.endRow),
rowCount: allRows.length,
}
: { rowData: allRows };
console.log("getRows: result = ", result);
setTimeout(() => {
params.success(result);
}, 500);
},
};
return dataSource;
}
const GridExample = () => {
const gridRef = useRef<AgGridReact>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 235,
flex: 1,
sortable: false,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
field: "employeeName",
};
}, []);
const isServerSideGroupOpenByDefault = useCallback(
(params: IsServerSideGroupOpenByDefaultParams) => {
const isKathrynPowers =
params.rowNode.level == 0 &&
params.data.employeeName == "Kathryn Powers";
const isMabelWard =
params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
return isKathrynPowers || isMabelWard;
},
[],
);
const isServerSideGroup = useCallback((dataItem: any) => {
// indicate if node is a group
return dataItem.group;
}, []);
const getServerSideGroupKey = useCallback((dataItem: any) => {
// specify which group key to use
return dataItem.employeeName;
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data: any[]) => {
const fakeServer = createFakeServer(data);
const datasource = createServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
const refreshCache = useCallback((route: string[]) => {
gridRef.current!.api.refreshServerSide({ route: route, purge: true });
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<button onClick={() => refreshCache([])}>Refresh Everything</button>
<button
onClick={() => refreshCache(["Kathryn Powers", "Mabel Ward"])}
>
Refresh ['Kathryn Powers','Mabel Ward']
</button>
</div>
<div style={gridStyle}>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
treeData={true}
cacheBlockSize={10}
isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
isServerSideGroup={isServerSideGroup}
getServerSideGroupKey={getServerSideGroupKey}
onGridReady={onGridReady}
/>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
Transactions with Tree Data Copy Link
Tree Data can have transactions applied in the same way as row groups. This is explained in the SSRM Transactions section.
The example below demonstrates transactions with Tree Data. Note the following:
- Add Child to Selected adds a child under the selected row, even if it wasn't originally a group.
- Update Selected changes the selected row's
Employment Type. - Delete Selected removes the selected row, and all of its child rows.
- Move Selected to Robert Peterson moves the selected row and its children directly under
Robert Peterson.
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GetRowIdFunc,
GetRowIdParams,
GetServerSideGroupKey,
GridApi,
GridOptions,
GridReadyEvent,
IRowNode,
IServerSideDatasource,
IServerSideGetRowsParams,
IServerSideGetRowsRequest,
IsServerSideGroup,
IsServerSideGroupOpenByDefault,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
RowModelType,
RowSelectionOptions,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
TextFilterModule,
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
];
let fakeServer: {
getData: (request: IServerSideGetRowsRequest) => void;
addChildRow: (route: string[], newRow: any) => void;
toggleEmployment: (route: string[]) => void;
removeEmployee: (route: string[]) => void;
moveEmployee: (from: string[], to: string[]) => void;
};
const getRouteToNode: (rowNode: IRowNode) => string[] = (rowNode: IRowNode) => {
if (!rowNode.parent) {
return [];
}
return [
...getRouteToNode(rowNode.parent),
rowNode.key ? rowNode.key : rowNode.data.employeeName,
];
};
let latestId = 100000;
function createFakeServer(fakeServerData: any[], api: GridApi) {
const getDataAtRoute = (route: string[]) => {
let mutableRoute = [...route];
let target: any = { underlings: fakeServerData };
while (mutableRoute.length) {
const nextRoute = mutableRoute[0];
mutableRoute = mutableRoute.slice(1);
target = target.underlings.find((e: any) => e.employeeName === nextRoute);
}
return target;
};
const sanitizeRowForGrid = (d: any) => {
return {
group: !!d.underlings && !!d.underlings.length,
employeeId: d.employeeId,
employeeName: d.employeeName,
employmentType: d.employmentType,
startDate: d.startDate,
};
};
fakeServer = {
getData: (request: IServerSideGetRowsRequest) => {
const extractRowsFromData: (groupKeys: string[], data: any[]) => any = (
groupKeys: string[],
data: any[],
) => {
if (groupKeys.length === 0) {
return data.map(sanitizeRowForGrid);
}
const key = groupKeys[0];
for (let i = 0; i < data.length; i++) {
if (data[i].employeeName === key) {
return extractRowsFromData(
groupKeys.slice(1),
data[i].underlings.slice(),
);
}
}
};
return extractRowsFromData(request.groupKeys, fakeServerData);
},
addChildRow: (route: string[], newRow: any) => {
const target = getDataAtRoute(route);
if (!target.underlings || target.underlings.length === 0) {
target.underlings = [newRow];
// update the parent row via transaction
api.applyServerSideTransaction({
route: route.slice(0, route.length - 1),
update: [sanitizeRowForGrid(target)],
});
} else {
target.underlings.push(newRow);
// add the child row via transaction
api.applyServerSideTransaction({
route,
add: [sanitizeRowForGrid(newRow)],
});
}
},
toggleEmployment: (route: string[]) => {
const target = getDataAtRoute(route);
// update the data at the source
target.employmentType =
target.employmentType === "Contract" ? "Permanent" : "Contract";
// inform the grid of the changes
api.applyServerSideTransaction({
route: route.slice(0, route.length - 1),
update: [sanitizeRowForGrid(target)],
});
},
removeEmployee: (route: string[]) => {
const target = getDataAtRoute(route);
const parent = getDataAtRoute(route.slice(0, route.length - 1));
parent.underlings = parent.underlings.filter(
(child: any) => child.employeeName !== target.employeeName,
);
if (parent.underlings.length === 0) {
// update the parent row via transaction, as it's no longer a group
api.applyServerSideTransaction({
route: route.slice(0, route.length - 2),
update: [sanitizeRowForGrid(parent)],
});
} else {
// inform the grid of the changes
api.applyServerSideTransaction({
route: route.slice(0, route.length - 1),
remove: [sanitizeRowForGrid(target)],
});
}
},
moveEmployee: (route: string[], to: string[]) => {
const target = getDataAtRoute(route);
// remove employee from old group
fakeServer.removeEmployee(route);
// add employee to new group
fakeServer.addChildRow(to, target);
},
};
return fakeServer;
}
function createServerSideDatasource(fakeServer: any) {
const dataSource: IServerSideDatasource = {
getRows: (params: IServerSideGetRowsParams) => {
console.log("ServerSideDatasource.getRows: params = ", params);
const request = params.request;
const allRows = fakeServer.getData(request);
const doingInfinite = request.startRow != null && request.endRow != null;
const result = doingInfinite
? {
rowData: allRows.slice(request.startRow, request.endRow),
rowCount: allRows.length,
}
: { rowData: allRows };
console.log("getRows: result = ", result);
setTimeout(() => {
params.success(result);
}, 500);
},
};
return dataSource;
}
const GridExample = () => {
const gridRef = useRef<AgGridReact>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 235,
flex: 1,
sortable: false,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
field: "employeeName",
};
}, []);
const rowSelection = useMemo<
RowSelectionOptions | "single" | "multiple"
>(() => {
return { mode: "singleRow" };
}, []);
const isServerSideGroupOpenByDefault = useCallback(
(params: IsServerSideGroupOpenByDefaultParams) => {
const isKathrynPowers =
params.rowNode.level == 0 &&
params.data.employeeName == "Kathryn Powers";
const isMabelWard =
params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
return isKathrynPowers || isMabelWard;
},
[],
);
const getRowId = useCallback(
(row: GetRowIdParams) => String(row.data.employeeId),
[],
);
const isServerSideGroup = useCallback((dataItem: any) => dataItem.group, []);
const getServerSideGroupKey = useCallback(
(dataItem: any) => dataItem.employeeName,
[],
);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data: any[]) => {
const adjustedData = [
{
employeeId: -1,
employeeName: "Robert Peterson",
employmentType: "Founder",
startDate: "24/01/1990",
},
...data,
];
const fakeServer = createFakeServer(adjustedData, params.api!);
const datasource = createServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
const addToSelected = useCallback(() => {
const selected = gridRef.current!.api.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
const newRow = {
employeeId: ++latestId,
employeeName: "Bertrand Parker " + latestId,
employmentType: "Permanent",
startDate: "20/01/1999",
};
fakeServer.addChildRow(route, newRow);
}, [latestId, fakeServer]);
const updateSelected = useCallback(() => {
const selected = gridRef.current!.api.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.toggleEmployment(route);
}, [fakeServer]);
const deleteSelected = useCallback(() => {
const selected = gridRef.current!.api.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.removeEmployee(route);
}, [fakeServer]);
const moveSelected = useCallback(() => {
const selected = gridRef.current!.api.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.moveEmployee(route, ["Robert Peterson"]);
}, [fakeServer]);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<button onClick={addToSelected}>Add Child to Selected</button>
<button onClick={updateSelected}>Update Selected</button>
<button onClick={deleteSelected}>Delete Selected</button>
<button onClick={moveSelected}>
Move Selected to Robert Peterson
</button>
</div>
<div style={gridStyle}>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
treeData={true}
cacheBlockSize={10}
rowSelection={rowSelection}
isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
getRowId={getRowId}
isServerSideGroup={isServerSideGroup}
getServerSideGroupKey={getServerSideGroupKey}
onGridReady={onGridReady}
/>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
Selection with Tree Data Copy Link
Tree Data can have row selection applied in the same way as row groups. This is explained in the SSRM Row Selection section.
The example below demonstrates row selection with Tree Data where the grid has been configured with rowSelection.groupSelects = 'descendants'. This means that selecting a row with descendants selects all of its descendants. Selecting some, but not all, of the descendants of a row places that row in the indeterminate state.
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GetRowIdFunc,
GetServerSideGroupKey,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsRequest,
IsServerSideGroup,
IsServerSideGroupOpenByDefault,
ModuleRegistry,
RowModelType,
RowSelectionOptions,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ServerSideRowModelApiModule,
TextFilterModule,
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
];
interface FakeServer {
getData: (request: IServerSideGetRowsRequest) => any;
addChildRow: (route: string[], newRow: any) => void;
toggleEmployment: (route: string[]) => void;
removeEmployee: (route: string[]) => void;
moveEmployee: (from: string[], to: string[]) => void;
}
let fakeServer: FakeServer;
function createFakeServer(fakeServerData: any[], api: GridApi) {
const getDataAtRoute = (route: string[]) => {
let mutableRoute = [...route];
let target: any = { underlings: fakeServerData };
while (mutableRoute.length) {
const nextRoute = mutableRoute[0];
mutableRoute = mutableRoute.slice(1);
target = target.underlings.find((e: any) => e.employeeName === nextRoute);
}
return target;
};
const sanitizeRowForGrid = (d: any) => {
return {
group: !!d.underlings && !!d.underlings.length,
employeeId: d.employeeId,
employeeName: d.employeeName,
employmentType: d.employmentType,
startDate: d.startDate,
};
};
fakeServer = {
getData: (request: IServerSideGetRowsRequest): any => {
const extractRowsFromData: (groupKeys: string[], data: any[]) => any = (
groupKeys: string[],
data: any[],
) => {
if (groupKeys.length === 0) {
return data.map(sanitizeRowForGrid);
}
const key = groupKeys[0];
for (let i = 0; i < data.length; i++) {
if (data[i].employeeName === key) {
return extractRowsFromData(
groupKeys.slice(1),
data[i].underlings.slice(),
);
}
}
};
return extractRowsFromData(request.groupKeys, fakeServerData);
},
addChildRow: (route: string[], newRow: any) => {
const target = getDataAtRoute(route);
if (!target.underlings || target.underlings.length === 0) {
target.underlings = [newRow];
// update the parent row via transaction
api.applyServerSideTransaction({
route: route.slice(0, route.length - 1),
update: [sanitizeRowForGrid(target)],
});
} else {
target.underlings.push(newRow);
// add the child row via transaction
api.applyServerSideTransaction({
route,
add: [sanitizeRowForGrid(newRow)],
});
}
},
toggleEmployment: (route: string[]) => {
const target = getDataAtRoute(route);
// update the data at the source
target.employmentType =
target.employmentType === "Contract" ? "Permanent" : "Contract";
// inform the grid of the changes
api.applyServerSideTransaction({
route: route.slice(0, route.length - 1),
update: [sanitizeRowForGrid(target)],
});
},
removeEmployee: (route: string[]) => {
const target = getDataAtRoute(route);
const parent = getDataAtRoute(route.slice(0, route.length - 1));
parent.underlings = parent.underlings.filter(
(child: any) => child.employeeName !== target.employeeName,
);
if (parent.underlings.length === 0) {
// update the parent row via transaction, as it's no longer a group
api.applyServerSideTransaction({
route: route.slice(0, route.length - 2),
update: [sanitizeRowForGrid(parent)],
});
} else {
// inform the grid of the changes
api.applyServerSideTransaction({
route: route.slice(0, route.length - 1),
remove: [sanitizeRowForGrid(target)],
});
}
},
moveEmployee: (route: string[], to: string[]) => {
const target = getDataAtRoute(route);
// remove employee from old group
fakeServer.removeEmployee(route);
// add employee to new group
fakeServer.addChildRow(to, target);
},
};
return fakeServer;
}
function createServerSideDatasource(fakeServer: FakeServer) {
const dataSource: IServerSideDatasource = {
getRows: (params) => {
console.log("ServerSideDatasource.getRows: params = ", params);
const request = params.request;
const allRows = fakeServer.getData(request);
const doingInfinite = request.startRow != null && request.endRow != null;
const result = doingInfinite
? {
rowData: allRows.slice(request.startRow, request.endRow),
rowCount: allRows.length,
}
: { rowData: allRows };
console.log("getRows: result = ", result);
setTimeout(() => {
params.success(result);
}, 500);
},
};
return dataSource;
}
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 235,
flex: 1,
sortable: false,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
field: "employeeName",
};
}, []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
]);
const rowSelection = useMemo<
RowSelectionOptions | "single" | "multiple"
>(() => {
return {
mode: "multiRow",
groupSelects: "descendants",
};
}, []);
const isServerSideGroupOpenByDefault = useCallback((params) => {
const isKathrynPowers =
params.rowNode.level == 0 && params.data.employeeName == "Kathryn Powers";
const isMabelWard =
params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
return isKathrynPowers || isMabelWard;
}, []);
const getRowId = useCallback((row) => String(row.data.employeeId), []);
const isServerSideGroup = useCallback((dataItem) => dataItem.group, []);
const getServerSideGroupKey = useCallback(
(dataItem) => dataItem.employeeName,
[],
);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data: any[]) => {
const adjustedData = [
{
employeeId: -1,
employeeName: "Robert Peterson",
employmentType: "Founder",
startDate: "24/01/1990",
},
...data,
];
const fakeServer = createFakeServer(adjustedData, params.api!);
const datasource = createServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={gridStyle}>
<AgGridReact
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
treeData={true}
columnDefs={columnDefs}
cacheBlockSize={10}
rowSelection={rowSelection}
isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
getRowId={getRowId}
isServerSideGroup={isServerSideGroup}
getServerSideGroupKey={getServerSideGroupKey}
onGridReady={onGridReady}
/>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
Filtering Tree Data Copy Link
Server-Side Tree Data Filtering should behave the same as Client-Side Tree Data Filtering. A group will be included if:
it has any children that pass the filter, or
it has a parent that passes the filter, or
its own data passes the filter
The following example demonstrates Server-Side Tree Data Filtering using the Set Filter Tree List, which replicates the Tree Data structure in the filter.
- The Group column has the Set Filter Tree List enabled via
filterParams.treeList = true. A Key Creator is specified to convert the path into a string. - The Group column has the filter values supplied asynchronously as a nested array of strings that matches the data paths. The supplied values are at the child level only.
- The Date column has the Set Filter Tree List enabled via
filterParams.treeList = true, and is grouped by year -> month -> day. - The Date column has the filter values supplied asynchronously as an array of
Dateobjects. - The Date column has a
filterParams.keyCreatorprovided to convert theDatevalues into the (string) format the server is expecting in the Filter Model. - The Group and Date columns both have
filterParams.excelMode = 'windows', which allows changes to the tree filter to be applied in batches.
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GetServerSideGroupKey,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ISetFilterParams,
IsServerSideGroup,
IsServerSideGroupOpenByDefault,
IsServerSideGroupOpenByDefaultParams,
KeyCreatorParams,
ModuleRegistry,
RowModelType,
SetFilterValuesFuncParams,
TextFilterModule,
ValueFormatterParams,
ValueGetterParams,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelModule,
SetFilterModule,
TreeDataModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
SetFilterModule,
TextFilterModule,
];
function valueGetter(params: ValueGetterParams) {
// server is returning a string, so need to convert to `Date`.
// could instead do this inside `IServerSideDatasource.getRows`
return params.data.startDate ? new Date(params.data.startDate) : null;
}
function cellValueFormatter(params: ValueFormatterParams) {
return params.value ? params.value.toLocaleDateString() : null;
}
function floatingFilterValueFormatter(params: ValueFormatterParams) {
return params.value ? params.value.toLocaleDateString() : "(Blanks)";
}
function dateKeyCreator(params: KeyCreatorParams) {
// this is what is being sent in the Filter Model to the server, so want the matching format
return params.value ? params.value.toISOString() : null;
}
function treeDataKeyCreator(params: KeyCreatorParams) {
// tree data group filter value is a string[], so convert to a unique string
return params.value ? params.value.join(",") : null;
}
let fakeServer: any;
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
// simulating real server call with a 500ms delay
setTimeout(() => {
// get data for request from our fake server
const response = server.getData(params.request);
if (response.success) {
// supply rows for requested block to grid
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
params.fail();
}
}, 500);
},
};
};
function getDatesAsync(params: SetFilterValuesFuncParams<any, Date>) {
if (!fakeServer) {
// wait for init
setTimeout(() => getDatesAsync(params), 500);
return;
}
let dates = fakeServer.getDates();
if (dates) {
// values need to match the cell value (what the `valueGetter` returns)
dates = dates.map((isoDateString: string) =>
isoDateString ? new Date(isoDateString) : isoDateString,
);
}
// simulating real server call with a 500ms delay
setTimeout(() => {
params.success(dates);
}, 500);
}
function getEmployeesAsync(params: SetFilterValuesFuncParams<any, string[]>) {
if (!fakeServer) {
// wait for init
setTimeout(() => getEmployeesAsync(params), 500);
return;
}
const employees = fakeServer.getEmployees();
// simulating real server call with a 500ms delay
setTimeout(() => {
params.success(employees);
}, 500);
}
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{
field: "startDate",
valueGetter: valueGetter,
valueFormatter: cellValueFormatter,
filter: "agSetColumnFilter",
filterParams: {
treeList: true,
excelMode: "windows",
keyCreator: dateKeyCreator,
valueFormatter: floatingFilterValueFormatter,
values: getDatesAsync,
} as ISetFilterParams<any, Date>,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 240,
filter: "agTextColumnFilter",
floatingFilter: true,
flex: 1,
sortable: false,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
field: "employeeName",
filter: "agSetColumnFilter",
filterParams: {
treeList: true,
excelMode: "windows",
keyCreator: treeDataKeyCreator,
values: getEmployeesAsync,
} as ISetFilterParams<any, string[]>,
};
}, []);
const isServerSideGroupOpenByDefault = useCallback(
(params: IsServerSideGroupOpenByDefaultParams) => {
// open first level by default
return params.rowNode.level === 0;
},
[],
);
const isServerSideGroup = useCallback((dataItem: any) => {
// indicate if node is a group
return dataItem.underlings;
}, []);
const getServerSideGroupKey = useCallback((dataItem: any) => {
// specify which group key to use
return dataItem.employeeName;
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data: any[]) => {
// setup the fake server with entire dataset
fakeServer = new FakeServer(data);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(fakeServer);
// register the datasource with the grid
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
treeData={true}
isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
isServerSideGroup={isServerSideGroup}
getServerSideGroupKey={getServerSideGroupKey}
onGridReady={onGridReady}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
// This fake server uses http://alasql.org/ to mimic how a real server
// might generate sql queries from the Server-Side Row Model request.
// To keep things simple it does the bare minimum to support the example.
export function FakeServer(allData) {
const processedData = processData(allData);
alasql.options.cache = false;
return {
getData: function (request) {
const hasFilter = request.filterModel && Object.keys(request.filterModel).length;
let results = executeQuery(request, hasFilter);
if (hasFilter) {
results = recursiveFilter(request, results);
}
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
getDates: function () {
const sql = 'SELECT DISTINCT startDate FROM ? ORDER BY startDate ASC';
return alasql(sql, [processedData]).map((row) => row.startDate);
},
getEmployees: function () {
// get children only
const sql = 'SELECT DISTINCT dataPath FROM ? WHERE underlings = FALSE ORDER BY dataPath ASC';
return alasql(sql, [processedData]).map((row) => (row.dataPath ? row.dataPath.split(',') : null));
},
};
function executeQuery(request, ignoreLimit) {
const sql = buildSql(request, ignoreLimit);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [processedData]);
}
function buildSql(request, ignoreLimit) {
return 'SELECT * FROM ?' + whereSql(request) + orderBySql(request) + limitSql(request, ignoreLimit);
}
function whereSql(request) {
const whereParts = [];
const filterModel = request.filterModel;
if (filterModel && Object.keys(filterModel).length) {
Object.keys(filterModel).forEach(function (key) {
const item = filterModel[key];
if (key === 'ag-Grid-AutoColumn') {
key = 'dataPath';
}
switch (item.filterType) {
case 'text':
whereParts.push(createFilterSql(textFilterMapper, key, item));
break;
case 'number':
whereParts.push(createFilterSql(numberFilterMapper, key, item));
break;
case 'set':
whereParts.push(createSetFilterSql(key, item.values));
break;
default:
console.log('unknown filter type: ' + item.filterType);
break;
}
});
} else {
whereParts.push("(parentPath = '" + request.groupKeys.join(',') + "')");
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function createSetFilterSql(key, values) {
return key + " IN ('" + values.join("', '") + "')";
}
function createFilterSql(mapper, key, item) {
if (item.operator) {
const conditions = item.conditions.map((condition) => mapper(key, condition));
return '(' + conditions.join(' ' + item.operator + ' ') + ')';
}
return mapper(key, item);
}
function textFilterMapper(key, item) {
switch (item.type) {
case 'equals':
return key + " = '" + item.filter + "'";
case 'notEqual':
return key + " != '" + item.filter + "'";
case 'contains':
return key + " LIKE '%" + item.filter + "%'";
case 'notContains':
return key + " NOT LIKE '%" + item.filter + "%'";
case 'startsWith':
return key + " LIKE '" + item.filter + "%'";
case 'endsWith':
return key + " LIKE '%" + item.filter + "'";
case 'blank':
return key + ' IS NULL or ' + key + " = ''";
case 'notBlank':
return key + ' IS NOT NULL and ' + key + " != ''";
default:
console.log('unknown text filter type: ' + item.type);
}
}
function numberFilterMapper(key, item) {
switch (item.type) {
case 'equals':
return key + ' = ' + item.filter;
case 'notEqual':
return key + ' != ' + item.filter;
case 'greaterThan':
return key + ' > ' + item.filter;
case 'greaterThanOrEqual':
return key + ' >= ' + item.filter;
case 'lessThan':
return key + ' < ' + item.filter;
case 'lessThanOrEqual':
return key + ' <= ' + item.filter;
case 'inRange':
return '(' + key + ' >= ' + item.filter + ' and ' + key + ' <= ' + item.filterTo + ')';
case 'blank':
return key + ' IS NULL';
case 'notBlank':
return key + ' IS NOT NULL';
default:
console.log('unknown number filter type: ' + item.type);
}
}
function orderBySql(request) {
const sortModel = request.sortModel;
if (sortModel.length === 0) return '';
const sorts = sortModel.map(function (s) {
return s.colId + ' ' + s.sort.toUpperCase();
});
return ' ORDER BY ' + sorts.join(', ');
}
function limitSql(request, ignoreLimit) {
if (ignoreLimit || request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function getLastRowIndex(request) {
const hasFilter = request.filterModel && Object.keys(request.filterModel).length;
let results = executeQuery(request, hasFilter);
if (hasFilter) {
results = recursiveFilter(request, results);
}
return results.length;
}
function processData(data) {
const flattenedData = [];
const flattenRowRecursive = (row, parentPath) => {
let startDate = null;
if (row.startDate) {
const dateParts = row.startDate.split('/');
startDate = new Date(parseInt(dateParts[2]), dateParts[1] - 1, dateParts[0]).toISOString();
}
const dataPath = [...parentPath, row.employeeName];
flattenedData.push({
...row,
dataPath: dataPath.join(','),
parentPath: parentPath.join(','),
startDate,
underlings: !!row.underlings,
});
if (row.underlings) {
row.underlings.forEach((underling) => flattenRowRecursive(underling, dataPath));
}
};
data.forEach((row) => flattenRowRecursive(row, []));
return flattenedData;
}
function recursiveFilter(request, results) {
// tree data filter returns rows where the row itself matches, parent matches, or a child matches.
// matches for row itself
const allResults = [...results];
// parents of matching rows
recursiveFilterParentMatches(allResults, results);
// children of matching rows
recursiveFilterChildMatches(allResults, results);
const requestPath = request.groupKeys.join(',');
const sql =
"SELECT DISTINCT processedData.* FROM ? processedData INNER JOIN ? allResults ON processedData.dataPath = allResults.dataPath WHERE parentPath = '" +
requestPath +
"'" +
orderBySql(request) +
limitSql(request);
return alasql(sql, [processedData, allResults]);
}
function recursiveFilterParentMatches(allResults, childResults) {
if (!childResults.length) {
return;
}
const sql =
'SELECT DISTINCT processedData.* FROM ? processedData INNER JOIN ? parentResults ON processedData.dataPath = parentResults.parentPath';
const newMatches = alasql(sql, [processedData, childResults]).filter(
(newResult) => !allResults.some((existingResult) => newResult.dataPath === existingResult.dataPath)
);
allResults.push(...newMatches);
recursiveFilterParentMatches(allResults, newMatches);
}
function recursiveFilterChildMatches(allResults, parentResults) {
if (!parentResults.length) {
return;
}
const sql =
'SELECT DISTINCT processedData.* FROM ? processedData INNER JOIN ? parentResults ON processedData.parentPath = parentResults.dataPath';
const newMatches = alasql(sql, [processedData, parentResults]).filter(
(newResult) => !allResults.some((existingResult) => newResult.dataPath === existingResult.dataPath)
);
allResults.push(...newMatches);
recursiveFilterChildMatches(allResults, newMatches);
}
}
Providing Hierarchical Data via API Copy Link
Below is an example of utilising api.applyServerSideRowData to populate data. The datasource is only used to provide root level rows, and when these are loaded the grid is provided the child row data via the API.
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GetServerSideGroupKey,
GridApi,
GridOptions,
GridReadyEvent,
ICellRendererParams,
IServerSideDatasource,
IServerSideGetRowsParams,
IsServerSideGroup,
IsServerSideGroupOpenByDefault,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ColumnsToolPanelModule,
TreeDataModule,
ColumnMenuModule,
ContextMenuModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
];
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "jobTitle" },
{ field: "employmentType" },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 240,
flex: 1,
sortable: false,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
field: "employeeName",
cellRendererParams: {
innerRenderer: (params: ICellRendererParams) => {
// display employeeName rather than group key (employeeId)
return params.data.employeeName;
},
},
};
}, []);
const isServerSideGroupOpenByDefault = useCallback(
(params: IsServerSideGroupOpenByDefaultParams) => {
// open first two levels by default
return params.rowNode.level < 2;
},
[],
);
const isServerSideGroup = useCallback((dataItem: any) => {
// indicate if node is a group
return !!dataItem.underlings;
}, []);
const getServerSideGroupKey = useCallback((dataItem: any) => {
// specify which group key to use
return dataItem.employeeId;
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data: any[]) => {
const datasource = createServerSideDatasource(data);
params.api!.setGridOption("serverSideDatasource", datasource);
function createServerSideDatasource(data: any) {
const dataSource: IServerSideDatasource = {
getRows: (params: IServerSideGetRowsParams) => {
console.log("ServerSideDatasource.getRows: params = ", params);
const request = params.request;
if (request.groupKeys.length) {
// this example doesn't need to support lower levels.
params.fail();
return;
}
const result = {
rowData: data.slice(request.startRow, request.endRow),
};
console.log("getRows: result = ", result);
setTimeout(() => {
params.success(result);
const recursivelyPopulateHierarchy = (
route: string[],
node: any,
) => {
if (node.underlings) {
params.api!.applyServerSideRowData({
route,
successParams: {
rowData: node.underlings,
rowCount: node.underlings.length,
},
});
node.underlings.forEach((child: any) => {
recursivelyPopulateHierarchy(
[...route, child.employeeId],
child,
);
});
}
};
result.rowData.forEach((topLevelNode: any) => {
recursivelyPopulateHierarchy(
[topLevelNode.employeeId],
topLevelNode,
);
});
}, 200);
},
};
return dataSource;
}
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
treeData={true}
isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
isServerSideGroup={isServerSideGroup}
getServerSideGroupKey={getServerSideGroupKey}
onGridReady={onGridReady}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Continue to the next section to learn how to combine Master Detail with the SSRM.