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:
<ag-grid-vue
:rowModelType="rowModelType"
:treeData="treeData"
:isServerSideGroup="isServerSideGroup"
:getServerSideGroupKey="getServerSideGroupKey"
/* other grid options ... */>
</ag-grid-vue>
// choose Server-Side Row Model
this.rowModelType = 'serverSide';
// enable Tree Data
this.treeData = true;
// indicate if row is a group node
this.isServerSideGroup = dataItem => {
return dataItem.group;
};
// specify which group key to use
this.getServerSideGroupKey = dataItem => {
return dataItem.employeeId;
};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 {
createApp,
defineComponent,
onBeforeMount,
ref,
shallowRef,
} from "vue";
import { AgGridVue } from "ag-grid-vue3";
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();
}
ModuleRegistry.registerModules([
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
]);
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;
}
const VueExample = defineComponent({
template: `
<div style="height: 100%">
<ag-grid-vue
style="width: 100%; height: 100%;"
@grid-ready="onGridReady"
:columnDefs="columnDefs"
:defaultColDef="defaultColDef"
:autoGroupColumnDef="autoGroupColumnDef"
:rowModelType="rowModelType"
:treeData="true"
:isServerSideGroupOpenByDefault="isServerSideGroupOpenByDefault"
:isServerSideGroup="isServerSideGroup"
:getServerSideGroupKey="getServerSideGroupKey"
:rowData="rowData"></ag-grid-vue>
</div>
`,
components: {
"ag-grid-vue": AgGridVue,
},
setup(props) {
const gridApi = shallowRef<GridApi | null>(null);
const columnDefs = ref<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "jobTitle" },
{ field: "employmentType" },
]);
const defaultColDef = ref<ColDef>({
width: 240,
flex: 1,
sortable: false,
});
const autoGroupColumnDef = ref<AutoGroupColumnDef>({
field: "employeeName",
cellRendererParams: {
innerRenderer: (params: ICellRendererParams) => {
// display employeeName rather than group key (employeeId)
return params.data.employeeName;
},
},
});
const rowModelType = ref<RowModelType>("serverSide");
const isServerSideGroupOpenByDefault = ref<IsServerSideGroupOpenByDefault>(
(params: IsServerSideGroupOpenByDefaultParams) => {
// open first two levels by default
return params.rowNode.level < 2;
},
);
const isServerSideGroup = ref<IsServerSideGroup>((dataItem: any) => {
// indicate if node is a group
return dataItem.group;
});
const getServerSideGroupKey = ref<GetServerSideGroupKey>(
(dataItem: any) => {
// specify which group key to use
return dataItem.employeeId;
},
);
const rowData = ref<any[]>(null);
const onGridReady = (params: GridReadyEvent) => {
gridApi.value = params.api;
const updateData = (data) => {
const fakeServer = createFakeServer(data);
const datasource = createServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
};
fetch("https://www.ag-grid.com/example-assets/small-tree-data.json")
.then((resp) => resp.json())
.then((data) => updateData(data));
};
return {
gridApi,
columnDefs,
defaultColDef,
autoGroupColumnDef,
rowModelType,
isServerSideGroupOpenByDefault,
isServerSideGroup,
getServerSideGroupKey,
rowData,
onGridReady,
};
},
});
const app = createApp(VueExample);
app.mount("#app");
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 {
createApp,
defineComponent,
onBeforeMount,
ref,
shallowRef,
} from "vue";
import { AgGridVue } from "ag-grid-vue3";
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();
}
ModuleRegistry.registerModules([
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
TreeDataModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
]);
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;
}
const VueExample = defineComponent({
template: `
<div style="height: 100%">
<div class="example-wrapper">
<div style="margin-bottom: 5px">
<button v-on:click="refreshCache([])">Refresh Everything</button>
<button v-on:click="refreshCache(['Kathryn Powers', 'Mabel Ward'])">
Refresh ['Kathryn Powers','Mabel Ward']
</button>
</div>
<ag-grid-vue
style="width: 100%; height: 100%;"
@grid-ready="onGridReady"
:columnDefs="columnDefs"
:defaultColDef="defaultColDef"
:autoGroupColumnDef="autoGroupColumnDef"
:rowModelType="rowModelType"
:treeData="true"
:cacheBlockSize="cacheBlockSize"
:isServerSideGroupOpenByDefault="isServerSideGroupOpenByDefault"
:isServerSideGroup="isServerSideGroup"
:getServerSideGroupKey="getServerSideGroupKey"
:rowData="rowData"></ag-grid-vue>
</div>
</div>
`,
components: {
"ag-grid-vue": AgGridVue,
},
setup(props) {
const gridApi = shallowRef<GridApi | null>(null);
const columnDefs = ref<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
]);
const defaultColDef = ref<ColDef>({
width: 235,
flex: 1,
sortable: false,
});
const autoGroupColumnDef = ref<AutoGroupColumnDef>({
field: "employeeName",
});
const rowModelType = ref<RowModelType>("serverSide");
const cacheBlockSize = ref(10);
const isServerSideGroupOpenByDefault = ref<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;
},
);
const isServerSideGroup = ref<IsServerSideGroup>((dataItem: any) => {
// indicate if node is a group
return dataItem.group;
});
const getServerSideGroupKey = ref<GetServerSideGroupKey>(
(dataItem: any) => {
// specify which group key to use
return dataItem.employeeName;
},
);
const rowData = ref<any[]>(null);
function refreshCache(route: string[]) {
gridApi.value!.refreshServerSide({ route: route, purge: true });
}
const onGridReady = (params: GridReadyEvent) => {
gridApi.value = params.api;
const updateData = (data) => {
const fakeServer = createFakeServer(data);
const datasource = createServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
};
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data) => updateData(data));
};
return {
gridApi,
columnDefs,
defaultColDef,
autoGroupColumnDef,
rowModelType,
cacheBlockSize,
isServerSideGroupOpenByDefault,
isServerSideGroup,
getServerSideGroupKey,
rowData,
onGridReady,
refreshCache,
};
},
});
const app = createApp(VueExample);
app.mount("#app");
.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.
import {
createApp,
defineComponent,
onBeforeMount,
ref,
shallowRef,
} from "vue";
import { AgGridVue } from "ag-grid-vue3";
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();
}
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;
};
function getRouteToNode(rowNode: IRowNode): string[] {
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) => {
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;
}
const VueExample = defineComponent({
template: `
<div style="height: 100%">
<div class="example-wrapper">
<div style="margin-bottom: 5px">
<button v-on:click="addToSelected()">Add Child to Selected</button>
<button v-on:click="updateSelected()">Update Selected</button>
<button v-on:click="deleteSelected()">Delete Selected</button>
<button v-on:click="moveSelected()">Move Selected to Robert Peterson</button>
</div>
<ag-grid-vue
style="width: 100%; height: 100%;"
@grid-ready="onGridReady"
:columnDefs="columnDefs"
:defaultColDef="defaultColDef"
:autoGroupColumnDef="autoGroupColumnDef"
:rowModelType="rowModelType"
:treeData="true"
:cacheBlockSize="cacheBlockSize"
:rowSelection="rowSelection"
:isServerSideGroupOpenByDefault="isServerSideGroupOpenByDefault"
:getRowId="getRowId"
:isServerSideGroup="isServerSideGroup"
:getServerSideGroupKey="getServerSideGroupKey"
:rowData="rowData"></ag-grid-vue>
</div>
</div>
`,
components: {
"ag-grid-vue": AgGridVue,
},
setup(props) {
const gridApi = shallowRef<GridApi | null>(null);
const columnDefs = ref<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
]);
const defaultColDef = ref<ColDef>({
width: 235,
flex: 1,
sortable: false,
});
const autoGroupColumnDef = ref<AutoGroupColumnDef>({
field: "employeeName",
});
const rowModelType = ref<RowModelType>("serverSide");
const cacheBlockSize = ref(10);
const rowSelection = ref<RowSelectionOptions | "single" | "multiple">({
mode: "singleRow",
});
const isServerSideGroupOpenByDefault = ref<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;
},
);
const getRowId = ref<GetRowIdFunc>((row: GetRowIdParams) =>
String(row.data.employeeId),
);
const isServerSideGroup = ref<IsServerSideGroup>(
(dataItem: any) => dataItem.group,
);
const getServerSideGroupKey = ref<GetServerSideGroupKey>(
(dataItem: any) => dataItem.employeeName,
);
const rowData = ref<any[]>(null);
function addToSelected() {
const selected = gridApi.value!.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.value!.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.toggleEmployment(route);
}
function deleteSelected() {
const selected = gridApi.value!.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.removeEmployee(route);
}
function moveSelected() {
const selected = gridApi.value!.getSelectedNodes()[0];
if (!selected) {
console.log("No row was selected.");
return;
}
const route = getRouteToNode(selected);
fakeServer.moveEmployee(route, ["Robert Peterson"]);
}
const onGridReady = (params: GridReadyEvent) => {
gridApi.value = params.api;
const updateData = (data) => {
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);
};
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data) => updateData(data));
};
return {
gridApi,
columnDefs,
defaultColDef,
autoGroupColumnDef,
rowModelType,
cacheBlockSize,
rowSelection,
isServerSideGroupOpenByDefault,
getRowId,
isServerSideGroup,
getServerSideGroupKey,
rowData,
onGridReady,
addToSelected,
updateSelected,
deleteSelected,
moveSelected,
};
},
});
const app = createApp(VueExample);
app.mount("#app");
.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.
import {
createApp,
defineComponent,
onBeforeMount,
ref,
shallowRef,
} from "vue";
import { AgGridVue } from "ag-grid-vue3";
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();
}
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;
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;
}
const VueExample = defineComponent({
template: `
<div style="height: 100%">
<div class="example-wrapper">
<ag-grid-vue
style="width: 100%; height: 100%;"
@grid-ready="onGridReady"
:defaultColDef="defaultColDef"
:autoGroupColumnDef="autoGroupColumnDef"
:rowModelType="rowModelType"
:treeData="true"
:columnDefs="columnDefs"
:cacheBlockSize="cacheBlockSize"
:rowSelection="rowSelection"
:isServerSideGroupOpenByDefault="isServerSideGroupOpenByDefault"
:getRowId="getRowId"
:isServerSideGroup="isServerSideGroup"
:getServerSideGroupKey="getServerSideGroupKey"
:rowData="rowData"></ag-grid-vue>
</div>
</div>
`,
components: {
"ag-grid-vue": AgGridVue,
},
setup(props) {
const gridApi = shallowRef<GridApi | null>(null);
const defaultColDef = ref<ColDef>({
width: 235,
flex: 1,
sortable: false,
});
const autoGroupColumnDef = ref<AutoGroupColumnDef>({
field: "employeeName",
});
const rowModelType = ref<RowModelType>("serverSide");
const columnDefs = ref<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "employmentType" },
{ field: "startDate" },
]);
const cacheBlockSize = ref(10);
const rowSelection = ref<RowSelectionOptions | "single" | "multiple">({
mode: "multiRow",
groupSelects: "descendants",
});
const isServerSideGroupOpenByDefault = ref<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;
},
);
const getRowId = ref<GetRowIdFunc>((row) => String(row.data.employeeId));
const isServerSideGroup = ref<IsServerSideGroup>(
(dataItem) => dataItem.group,
);
const getServerSideGroupKey = ref<GetServerSideGroupKey>(
(dataItem) => dataItem.employeeName,
);
const rowData = ref<any[]>(null);
const onGridReady = (params: GridReadyEvent) => {
gridApi.value = params.api;
const updateData = (data) => {
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);
};
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data) => updateData(data));
};
return {
gridApi,
defaultColDef,
autoGroupColumnDef,
rowModelType,
columnDefs,
cacheBlockSize,
rowSelection,
isServerSideGroupOpenByDefault,
getRowId,
isServerSideGroup,
getServerSideGroupKey,
rowData,
onGridReady,
};
},
});
const app = createApp(VueExample);
app.mount("#app");
.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.
import {
createApp,
defineComponent,
onBeforeMount,
ref,
shallowRef,
} from "vue";
import { AgGridVue } from "ag-grid-vue3";
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();
}
ModuleRegistry.registerModules([
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;
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 VueExample = defineComponent({
template: `
<div style="height: 100%">
<ag-grid-vue
style="width: 100%; height: 100%;"
@grid-ready="onGridReady"
:columnDefs="columnDefs"
:defaultColDef="defaultColDef"
:autoGroupColumnDef="autoGroupColumnDef"
:rowModelType="rowModelType"
:treeData="true"
:isServerSideGroupOpenByDefault="isServerSideGroupOpenByDefault"
:isServerSideGroup="isServerSideGroup"
:getServerSideGroupKey="getServerSideGroupKey"
:rowData="rowData"></ag-grid-vue>
</div>
`,
components: {
"ag-grid-vue": AgGridVue,
},
setup(props) {
const gridApi = shallowRef<GridApi | null>(null);
const columnDefs = ref<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 = ref<ColDef>({
width: 240,
filter: "agTextColumnFilter",
floatingFilter: true,
flex: 1,
sortable: false,
});
const autoGroupColumnDef = ref<AutoGroupColumnDef>({
field: "employeeName",
filter: "agSetColumnFilter",
filterParams: {
treeList: true,
excelMode: "windows",
keyCreator: treeDataKeyCreator,
values: getEmployeesAsync,
} as ISetFilterParams<any, string[]>,
});
const rowModelType = ref<RowModelType>("serverSide");
const isServerSideGroupOpenByDefault = ref<IsServerSideGroupOpenByDefault>(
(params: IsServerSideGroupOpenByDefaultParams) => {
// open first level by default
return params.rowNode.level === 0;
},
);
const isServerSideGroup = ref<IsServerSideGroup>((dataItem: any) => {
// indicate if node is a group
return dataItem.underlings;
});
const getServerSideGroupKey = ref<GetServerSideGroupKey>(
(dataItem: any) => {
// specify which group key to use
return dataItem.employeeName;
},
);
const rowData = ref<any[]>(null);
const onGridReady = (params: GridReadyEvent) => {
gridApi.value = params.api;
const updateData = (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
params.api!.setGridOption("serverSideDatasource", datasource);
};
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data) => updateData(data));
};
return {
gridApi,
columnDefs,
defaultColDef,
autoGroupColumnDef,
rowModelType,
isServerSideGroupOpenByDefault,
isServerSideGroup,
getServerSideGroupKey,
rowData,
onGridReady,
};
},
});
const app = createApp(VueExample);
app.mount("#app");
// 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.
import {
createApp,
defineComponent,
onBeforeMount,
ref,
shallowRef,
} from "vue";
import { AgGridVue } from "ag-grid-vue3";
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();
}
ModuleRegistry.registerModules([
ColumnsToolPanelModule,
TreeDataModule,
ColumnMenuModule,
ContextMenuModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
]);
const VueExample = defineComponent({
template: `
<div style="height: 100%">
<ag-grid-vue
style="width: 100%; height: 100%;"
@grid-ready="onGridReady"
:columnDefs="columnDefs"
:defaultColDef="defaultColDef"
:autoGroupColumnDef="autoGroupColumnDef"
:rowModelType="rowModelType"
:treeData="true"
:isServerSideGroupOpenByDefault="isServerSideGroupOpenByDefault"
:isServerSideGroup="isServerSideGroup"
:getServerSideGroupKey="getServerSideGroupKey"
:rowData="rowData"></ag-grid-vue>
</div>
`,
components: {
"ag-grid-vue": AgGridVue,
},
setup(props) {
const gridApi = shallowRef<GridApi | null>(null);
const columnDefs = ref<ColDef[]>([
{ field: "employeeId", hide: true },
{ field: "employeeName", hide: true },
{ field: "jobTitle" },
{ field: "employmentType" },
]);
const defaultColDef = ref<ColDef>({
width: 240,
flex: 1,
sortable: false,
});
const autoGroupColumnDef = ref<AutoGroupColumnDef>({
field: "employeeName",
cellRendererParams: {
innerRenderer: (params: ICellRendererParams) => {
// display employeeName rather than group key (employeeId)
return params.data.employeeName;
},
},
});
const rowModelType = ref<RowModelType>("serverSide");
const isServerSideGroupOpenByDefault = ref<IsServerSideGroupOpenByDefault>(
(params: IsServerSideGroupOpenByDefaultParams) => {
// open first two levels by default
return params.rowNode.level < 2;
},
);
const isServerSideGroup = ref<IsServerSideGroup>((dataItem: any) => {
// indicate if node is a group
return !!dataItem.underlings;
});
const getServerSideGroupKey = ref<GetServerSideGroupKey>(
(dataItem: any) => {
// specify which group key to use
return dataItem.employeeId;
},
);
const rowData = ref<any[]>(null);
const onGridReady = (params: GridReadyEvent) => {
gridApi.value = params.api;
const updateData = (data) => {
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;
}
};
fetch("https://www.ag-grid.com/example-assets/tree-data.json")
.then((resp) => resp.json())
.then((data) => updateData(data));
};
return {
gridApi,
columnDefs,
defaultColDef,
autoGroupColumnDef,
rowModelType,
isServerSideGroupOpenByDefault,
isServerSideGroup,
getServerSideGroupKey,
rowData,
onGridReady,
};
},
});
const app = createApp(VueExample);
app.mount("#app");
Continue to the next section to learn how to combine Master Detail with the SSRM.