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:
const gridOptions = {
// choose Server-Side Row Model
rowModelType: 'serverSide',
// enable Tree Data
treeData: true,
// indicate if row is a group node
isServerSideGroup: dataItem => {
return dataItem.group;
},
// specify which group key to use
getServerSideGroupKey: dataItem => {
return dataItem.employeeId;
},
// other grid options ...
}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).
import {
ColDef,
GridApi,
GridOptions,
ICellRendererParams,
IServerSideDatasource,
IServerSideGetRowsParams,
IServerSideGetRowsRequest,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
]);
const columnDefs: ColDef[] = [
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "jobTitle" },
{ field: "employmentType" },
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
defaultColDef: {
width: 240,
flex: 1,
sortable: false,
},
autoGroupColumnDef: {
field: "employeeName",
cellRendererParams: {
innerRenderer: (params: ICellRendererParams) => {
// display employeeName rather than group key (employeeId)
return params.data.employeeName;
},
},
},
rowModelType: "serverSide",
treeData: true,
columnDefs: columnDefs,
isServerSideGroupOpenByDefault: (
params: IsServerSideGroupOpenByDefaultParams,
) => {
// open first two levels by default
return params.rowNode.level < 2;
},
isServerSideGroup: (dataItem: any) => {
// indicate if node is a group
return dataItem.group;
},
getServerSideGroupKey: (dataItem: any) => {
// specify which group key to use
return dataItem.employeeId;
},
};
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/small-tree-data.json")
.then((response) => response.json())
.then(function (data) {
const fakeServer = createFakeServer(data);
const datasource = createServerSideDatasource(fakeServer);
gridApi!.setGridOption("serverSideDatasource", datasource);
});
function createFakeServer(fakeServerData: any[]) {
const fakeServer = {
data: fakeServerData,
getData: function (request: IServerSideGetRowsRequest) {
function extractRowsFromData(groupKeys: string[], data: any[]): 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;
}
<div id="myGrid" style="height: 100%"></div>
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 }).
import {
ColDef,
GridApi,
GridOptions,
IServerSideDatasource,
IServerSideGetRowsParams,
IServerSideGetRowsRequest,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
]);
const columnDefs: ColDef[] = [
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
defaultColDef: {
width: 235,
flex: 1,
sortable: false,
},
autoGroupColumnDef: {
field: "employeeName",
},
rowModelType: "serverSide",
treeData: true,
columnDefs: columnDefs,
cacheBlockSize: 10,
isServerSideGroupOpenByDefault: (
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;
},
isServerSideGroup: (dataItem: any) => {
// indicate if node is a group
return dataItem.group;
},
getServerSideGroupKey: (dataItem: any) => {
// specify which group key to use
return dataItem.employeeName;
},
};
function refreshCache(route: string[]) {
gridApi!.refreshServerSide({ route: route, purge: true });
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((response) => response.json())
.then(function (data) {
const fakeServer = createFakeServer(data);
const datasource = createServerSideDatasource(fakeServer);
gridApi!.setGridOption("serverSideDatasource", datasource);
});
function createFakeServer(fakeServerData: any[]) {
const fakeServer = {
getData: (request: IServerSideGetRowsRequest) => {
function extractRowsFromData(groupKeys: string[], data: any[]): 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;
}
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).refreshCache = refreshCache;
}
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
<div class="example-wrapper">
<div style="margin-bottom: 5px">
<button onclick="refreshCache([])">Refresh Everything</button>
<button onclick="refreshCache(['Kathryn Powers', 'Mabel Ward'])">
Refresh ['Kathryn Powers','Mabel Ward']
</button>
</div>
<div id="myGrid"></div>
</div>
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.
import {
ColDef,
GetRowIdParams,
GridApi,
GridOptions,
IRowNode,
IServerSideDatasource,
IServerSideGetRowsParams,
IServerSideGetRowsRequest,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
TextFilterModule,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
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 columnDefs: ColDef[] = [
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
defaultColDef: {
width: 235,
flex: 1,
sortable: false,
},
autoGroupColumnDef: {
field: "employeeName",
},
rowModelType: "serverSide",
treeData: true,
columnDefs,
cacheBlockSize: 10,
rowSelection: { mode: "singleRow" },
isServerSideGroupOpenByDefault: (
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;
},
getRowId: (row: GetRowIdParams) => String(row.data.employeeId),
isServerSideGroup: (dataItem: any) => dataItem.group,
getServerSideGroupKey: (dataItem: any) => dataItem.employeeName,
};
function getRouteToNode(rowNode: IRowNode): string[] {
if (!rowNode.parent) {
return [];
}
return [
...getRouteToNode(rowNode.parent),
rowNode.key ? rowNode.key : rowNode.data.employeeName,
];
}
let latestId = 100000;
function addToSelected() {
const selected = gridApi!.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);
}
function updateSelected() {
const selected = gridApi!.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.toggleEmployment(route);
}
function deleteSelected() {
const selected = gridApi!.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.removeEmployee(route);
}
function moveSelected() {
const selected = gridApi!.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.moveEmployee(route, ["Robert Peterson"]);
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((response) => response.json())
.then(function (data) {
const adjustedData = [
{
employeeId: -1,
employeeName: "Robert Peterson",
employmentType: "Founder",
startDate: "24/01/1990",
},
...data,
];
const fakeServer = createFakeServer(adjustedData, gridApi!);
const datasource = createServerSideDatasource(fakeServer);
gridApi!.setGridOption("serverSideDatasource", datasource);
});
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) => {
function extractRowsFromData(groupKeys: string[], data: any[]): 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;
}
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).addToSelected = addToSelected;
(<any>window).updateSelected = updateSelected;
(<any>window).deleteSelected = deleteSelected;
(<any>window).moveSelected = moveSelected;
}
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
<div class="example-wrapper">
<div style="margin-bottom: 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 id="myGrid"></div>
</div>
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.
import {
GridApi,
GridOptions,
IServerSideDatasource,
IServerSideGetRowsRequest,
ModuleRegistry,
TextFilterModule,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
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;
let gridApi: GridApi;
const gridOptions: GridOptions = {
defaultColDef: {
width: 235,
flex: 1,
sortable: false,
},
autoGroupColumnDef: {
field: "employeeName",
},
rowModelType: "serverSide",
treeData: true,
columnDefs: [
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
],
cacheBlockSize: 10,
rowSelection: {
mode: "multiRow",
groupSelects: "descendants",
},
isServerSideGroupOpenByDefault: (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;
},
getRowId: (row) => String(row.data.employeeId),
isServerSideGroup: (dataItem) => dataItem.group,
getServerSideGroupKey: (dataItem) => dataItem.employeeName,
};
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((response) => response.json())
.then(function (data) {
const adjustedData = [
{
employeeId: -1,
employeeName: "Robert Peterson",
employmentType: "Founder",
startDate: "24/01/1990",
},
...data,
];
const fakeServer = createFakeServer(adjustedData, gridApi!);
const datasource = createServerSideDatasource(fakeServer);
gridApi!.setGridOption("serverSideDatasource", datasource);
});
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 => {
function extractRowsFromData(groupKeys: string[], data: any[]): 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;
}
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
<div class="example-wrapper">
<div id="myGrid"></div>
</div>
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.
import {
ColDef,
GridApi,
GridOptions,
IServerSideDatasource,
ISetFilterParams,
IsServerSideGroupOpenByDefaultParams,
KeyCreatorParams,
ModuleRegistry,
SetFilterValuesFuncParams,
TextFilterModule,
ValueFormatterParams,
ValueGetterParams,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelModule,
SetFilterModule,
TreeDataModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
SetFilterModule,
TextFilterModule,
]);
const columnDefs: 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>,
},
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
defaultColDef: {
width: 240,
filter: "agTextColumnFilter",
floatingFilter: true,
flex: 1,
sortable: false,
},
autoGroupColumnDef: {
field: "employeeName",
filter: "agSetColumnFilter",
filterParams: {
treeList: true,
excelMode: "windows",
keyCreator: treeDataKeyCreator,
values: getEmployeesAsync,
} as ISetFilterParams<any, string[]>,
},
rowModelType: "serverSide",
treeData: true,
columnDefs: columnDefs,
isServerSideGroupOpenByDefault: (
params: IsServerSideGroupOpenByDefaultParams,
) => {
// open first level by default
return params.rowNode.level === 0;
},
isServerSideGroup: (dataItem: any) => {
// indicate if node is a group
return dataItem.underlings;
},
getServerSideGroupKey: (dataItem: any) => {
// specify which group key to use
return dataItem.employeeName;
},
};
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;
function getServerSideDatasource(server: any): IServerSideDatasource {
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 gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((response) => response.json())
.then(function (data) {
// 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
gridApi!.setGridOption("serverSideDatasource", datasource);
});
// 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);
}
}
<div id="myGrid" style="height: 100%"></div>
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.
import {
ColDef,
GridApi,
GridOptions,
ICellRendererParams,
IServerSideDatasource,
IServerSideGetRowsParams,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TreeDataModule,
} from "ag-grid-enterprise";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ColumnsToolPanelModule,
TreeDataModule,
ColumnMenuModule,
ContextMenuModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
]);
const columnDefs: ColDef[] = [
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "jobTitle" },
{ field: "employmentType" },
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
defaultColDef: {
width: 240,
flex: 1,
sortable: false,
},
autoGroupColumnDef: {
field: "employeeName",
cellRendererParams: {
innerRenderer: (params: ICellRendererParams) => {
// display employeeName rather than group key (employeeId)
return params.data.employeeName;
},
},
},
rowModelType: "serverSide",
treeData: true,
columnDefs: columnDefs,
isServerSideGroupOpenByDefault: (
params: IsServerSideGroupOpenByDefaultParams,
) => {
// open first two levels by default
return params.rowNode.level < 2;
},
isServerSideGroup: (dataItem: any) => {
// indicate if node is a group
return !!dataItem.underlings;
},
getServerSideGroupKey: (dataItem: any) => {
// specify which group key to use
return dataItem.employeeId;
},
};
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((response) => response.json())
.then(function (data) {
const datasource = createServerSideDatasource(data);
gridApi!.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) {
gridApi!.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;
}
});
<div id="myGrid" style="height: 100%"></div>
Continue to the next section to learn how to combine Master Detail with the SSRM.