This section covers Row Grouping in the Server-Side Row Model (SSRM).
Enabling Row Grouping Copy Link
Row Grouping is enabled in the grid via the rowGroup column definition attribute. The example below shows how to group rows by 'country':
const [columnDefs, setColumnDefs] = useState([
{ field: 'country', rowGroup: true },
{ field: 'sport' },
{ field: 'year' },
]);
<AgGridReact columnDefs={columnDefs} />For more configuration details see the section on Row Grouping.
Server Side Row Grouping Copy Link
The actual grouping of rows is performed on the server when using the SSRM. When the grid needs more rows it makes a request via getRows(params) on the Server-Side Datasource with metadata containing grouping details.
The properties relevant to Row Grouping in the request are shown below:
type IServerSideGetRowsRequest = {
// row group columns
rowGroupCols: ColumnVO[];
// what groups the user is viewing
groupKeys: string[];
// ... // other params
}Note in the snippet above the property rowGroupCols contains all the columns (dimensions) the grid is grouping on, e.g. 'Country', 'Year'. The property groupKeys contains the list of group keys selected, e.g. ['Argentina', '2012'].
The example below demonstrates server-side Row Grouping. Note the following:
- Country and Sport columns have
rowGroup=truedefined on their column definitions. This tells the grid there are two levels of grouping, one for Country and one for Sport. - The
rowGroupColsandgroupKeysproperties in the request are used by the server to perform grouping. - Open the browser's dev console to view the request supplied to the datasource.
("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,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [RowGroupingModule, ServerSideRowModelModule];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 1000);
},
};
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, hide: true },
{ field: "sport", rowGroup: true, hide: true },
{ field: "year", minWidth: 100 },
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
field: "athlete",
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const 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<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
cacheBlockSize={5}
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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Open by Default Copy Link
It is possible to have rows open as soon as they are loaded. To do this implement the grid callback isServerSideGroupOpenByDefault.
Allows groups to be open by default. |
// Example implementation
function isServerSideGroupOpenByDefault(params) {
var rowNode = params.rowNode;
var isZimbabwe = rowNode.field == 'country' && rowNode.key == 'Zimbabwe';
return isZimbabwe;
}Server-Side Open By Default requires Row IDs to be supplied to the grid.
It may also be helpful to use the Row Node API getRoute() to inspect the route of a row node.
Returns the route of the row node. If the Row Node does not have a key (i.e it's a leaf row inside a row group) returns undefined |
Below shows isServerSideGroupOpenByDefault() and getRoute in action. Note the following:
- The callback opens the following routes as soon as those routes are loaded:
- [Zimbabwe]
- [Zimbabwe, Swimming]
- [United States, Swimming]
- Note [Zimbabwe] and [Zimbabwe, Swimming] are visibly open by default.
- Note [United States, Swimming] is not visibly open by default, as the parent group 'United States' is not open. However when 'United States' is opened, it's 'Swimming' group is opened by default.
- Selecting a group row and clicking 'Route of Selected' prints the route to the selected node to the developer console.
("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,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsParams,
IsServerSideGroupOpenByDefault,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
RowModelType,
RowSelectionOptions,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [RowGroupingModule, ServerSideRowModelModule];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params: IServerSideGetRowsParams) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 400);
},
};
};
const GridExample = () => {
const gridRef = useRef<AgGridReact<IOlympicData>>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", enableRowGroup: true, rowGroup: true, hide: true },
{ field: "sport", enableRowGroup: true, rowGroup: true, hide: true },
{ field: "year", minWidth: 100 },
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
};
}, []);
const rowSelection = useMemo<
RowSelectionOptions | "single" | "multiple"
>(() => {
return { mode: "multiRow" };
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const 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);
});
}, []);
const onBtRouteOfSelected = useCallback(() => {
const selectedNodes = gridRef.current!.api.getSelectedNodes();
selectedNodes.forEach(function (rowNode, index) {
const route = rowNode.getRoute();
const routeString = route ? route.join(",") : undefined;
console.log("#" + index + ", route = [" + routeString + "]");
});
}, []);
const getRowId = useCallback((params: GetRowIdParams) => {
return window.agRandom().toString();
}, []);
const isServerSideGroupOpenByDefault = useCallback(
(params: IsServerSideGroupOpenByDefaultParams) => {
const route = params.rowNode.getRoute();
if (!route) {
return false;
}
const routeAsString = route.join(",");
const routesToOpenByDefault = [
"Zimbabwe",
"Zimbabwe,Swimming",
"United States,Swimming",
];
return routesToOpenByDefault.indexOf(routeAsString) >= 0;
},
[],
);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<button onClick={onBtRouteOfSelected}>Route of Selected</button>
</div>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
rowSelection={rowSelection}
getRowId={getRowId}
isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
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 auto;
width: 100%;
}
// 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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Group Total Rows Copy Link
To enable Group Total Rows, set the groupTotalRow property to 'top' or 'bottom'.
Group total rows can also be used with groupDisplayType='multipleColumns', as demonstrated in the example below.
Grand Total Row Copy Link
To display a grand total row, set the grandTotalRow property to 'top', 'bottom', 'pinnedTop', or 'pinnedBottom'. The grand total row is supported for both flat grids and grids with row grouping.
Providing Grand Total Data Copy Link
When grandTotalRow is set, the needsGrandTotal hint on getRows params will be true for root-level requests that don't yet have cached grand total data — this happens on first load and after any filter or aggregation change that purges the cached data. The server may also always provide updated grand total data regardless of this hint. Sort changes do not invalidate the grand total (sorting does not affect the totals), so needsGrandTotal remains false after a sort-only change.
The grandTotalData field on the success callback params controls the grand total row:
- Pass the grand total data object to set or update the grand total row.
- Pass
nullto explicitly remove an existing grand total row. - Leave it
undefined(or omit the field entirely) to keep the grand total unchanged — the grid will continue to show whatever grand total is already cached. This lets paged block requests return data rows without having to re-send the grand total every time.
The example below shows a grouped grid with aggregations on the medal columns. Note how the grand total is recomputed server-side when filters change (reflecting the filtered totals), and that changing an aggregation function via the column menu triggers a fresh request for both data and grand total:
("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,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
NumberFilterModule,
RowModelType,
SideBarDef,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
RowGroupingModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ColumnMenuModule,
ColumnsToolPanelModule,
NumberFilterModule,
RowGroupingModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
TextFilterModule,
];
const getServerSideDatasource: (
server: ReturnType<typeof FakeServer>,
) => IServerSideDatasource = (server: ReturnType<typeof FakeServer>) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request, params.needsGrandTotal);
// Delay long enough for the loading rows to be clearly visible, simulating a remote call.
setTimeout(() => {
if (response.success) {
params.success({
rowData: response.rows,
rowCount: response.lastRow,
grandTotalData: response.grandTotalData,
});
} else {
params.fail();
}
}, 800);
},
};
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, hide: true },
{ field: "sport", rowGroup: true, hide: true },
{
field: "year",
minWidth: 100,
filter: "agNumberColumnFilter",
floatingFilter: true,
},
{
field: "gold",
aggFunc: "sum",
enableValue: true,
filter: "agNumberColumnFilter",
floatingFilter: true,
},
{
field: "silver",
aggFunc: "sum",
enableValue: true,
filter: "agNumberColumnFilter",
floatingFilter: true,
},
{
field: "bronze",
aggFunc: "sum",
enableValue: true,
filter: "agNumberColumnFilter",
floatingFilter: true,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 240,
field: "athlete",
};
}, []);
const sideBar = useMemo<
SideBarDef | string | string[] | boolean | null
>(() => {
return {
toolPanels: ["columns"],
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
const fakeServer = new FakeServer(data);
const datasource = getServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={gridStyle}>
<AgGridReact<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
grandTotalRow={"bottom"}
cacheBlockSize={20}
sideBar={sideBar}
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%;
}
// 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.
// The ID AG Grid uses for the grand total row. Exported as GRAND_TOTAL_ROW_ID from 'ag-grid-community'
const GRAND_TOTAL_ROW_ID = 'rowGroupFooter_ROOT_NODE_ID';
export function FakeServer(allData) {
alasql.options.cache = false;
return {
getData: function (request, needsGrandTotal) {
const rows = executeQuery(request);
const grandTotalData = needsGrandTotal ? computeGrandTotal(request) : undefined;
return {
success: true,
rows,
lastRow: getLastRowIndex(request),
grandTotalData,
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function computeGrandTotal(request) {
const valueCols = request.valueCols ?? [];
// If no value columns are configured there is nothing meaningful to aggregate.
if (valueCols.length === 0) {
return { id: GRAND_TOTAL_ROW_ID };
}
// Aggregate across the entire filtered dataset, ignoring group keys — the grand total
// represents the totals the user would see if all groups were expanded.
const selects = valueCols.map((col) => `${col.aggFunc}(${col.id}) AS ${col.id}`);
const sql = `SELECT ${selects.join(', ')} FROM ?` + whereSql({ ...request, groupKeys: [] });
console.log('[FakeServer] - about to execute grand total query:', sql);
const result = alasql(sql, [allData])[0] ?? {};
return { id: GRAND_TOTAL_ROW_ID, ...result };
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const filterModel = request.filterModel;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (filterModel) {
Object.keys(filterModel).forEach(function (key) {
const item = filterModel[key];
switch (item.filterType) {
case 'text':
whereParts.push(createFilterSql(textFilterMapper, key, item));
break;
case 'number':
whereParts.push(createFilterSql(numberFilterMapper, key, item));
break;
default:
console.log('unknown filter type: ' + item.filterType);
break;
}
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
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 groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Alternatively, if getRowId is configured, a row with ID GRAND_TOTAL_ROW_ID ('rowGroupFooter_ROOT_NODE_ID') included in rowData will also be treated as the grand total (the grandTotalData field takes priority).
Updating the Grand Total Row via Transactions Copy Link
The grand total row can be updated, added, or removed via applyServerSideTransaction:
- Update: Include the grand total data in the
updatearray. ThegetRowIdmust returnGRAND_TOTAL_ROW_ID('rowGroupFooter_ROOT_NODE_ID') for this row. - Add: Include the grand total data in the
addarray. If a grand total already exists, it will be updated. - Remove: Include a row whose
getRowIdreturnsGRAND_TOTAL_ROW_ID('rowGroupFooter_ROOT_NODE_ID') in theremovearray to remove it.
This is useful when the grand total comes from a different endpoint than the paged data — for example, when computing totals is more expensive than loading rows and should be done in a separate, independently cancellable request.
The example below demonstrates that pattern on a flat grid. Each getRows call fetches only the data rows; whenever the grid signals needsGrandTotal, a separate asynchronous request for the grand total is started in parallel. While it is in flight the current grand total is removed via transaction so stale values aren't shown, and when the response arrives the new total is applied via an add transaction. A monotonic request id ensures that if a newer grand-total fetch is started before an earlier one returns, the stale response is discarded rather than overwriting fresher data.
("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 {
ColDef,
ColGroupDef,
GRAND_TOTAL_ROW_ID,
GetRowIdFunc,
GetRowIdParams,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsParams,
ModuleRegistry,
NumberFilterModule,
RowModelType,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import {
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { OlympicRow } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
NumberFilterModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
TextFilterModule,
];
let fakeServer: ReturnType<typeof FakeServer>;
// Counter identifying the latest in-flight grand-total request. On arrival each fetch checks its
// captured id against the counter; if it's been superseded by a newer request (e.g. a second
// filter change before the first fetch returned), the stale response is discarded.
let latestGrandTotalRequestId = 0;
const getServerSideDatasource: (
server: ReturnType<typeof FakeServer>,
) => IServerSideDatasource = (server: ReturnType<typeof FakeServer>) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested:", params.request);
const response = server.getData(params.request, false);
const needsGrandTotal = params.needsGrandTotal;
setTimeout(() => {
if (!response.success) {
params.fail();
return;
}
// grandTotalData is deliberately omitted here — the async refresh below owns it.
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
// `refreshGrandTotalAsync`'s first act is a `remove` transaction, which sets
// store.grandTotalData = null. The grid treats null as "explicitly cleared" so
// `needsGrandTotal` stays false for subsequent block requests in the same store
// — this branch fires exactly once per logical query.
if (needsGrandTotal) {
void refreshGrandTotalAsync(params);
}
}, 800);
},
};
};
async function refreshGrandTotalAsync(
params: IServerSideGetRowsParams<OlympicRow>,
) {
const { api, request } = params;
const thisRequestId = ++latestGrandTotalRequestId;
console.log(`[GrandTotal] - request ${thisRequestId} started`);
// Clear the stale total immediately; we'll add the fresh one back when the fetch resolves.
api.applyServerSideTransaction({
remove: [{ id: GRAND_TOTAL_ROW_ID } as any],
});
// Simulate a separate, backend call for the grand total.
const grandTotalData = await new Promise<OlympicRow>((resolve) => {
setTimeout(() => {
resolve(fakeServer.getData(request, true).grandTotalData);
}, 1300);
});
if (thisRequestId !== latestGrandTotalRequestId) {
console.log(
`[GrandTotal] - request ${thisRequestId} ignored (superseded by ${latestGrandTotalRequestId})`,
);
return;
}
api.applyServerSideTransaction({ add: [grandTotalData] });
console.log(`[GrandTotal] - request ${thisRequestId} applied`);
}
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "athlete", minWidth: 170 },
{ field: "country" },
{ field: "sport" },
{ field: "year", filter: "agNumberColumnFilter", floatingFilter: true },
// aggFunc on a flat grid has no client-side effect, but the SSRM request's valueCols
// carries it to the server so our grand-total fetch uses the right aggregation.
{
field: "gold",
aggFunc: "sum",
filter: "agNumberColumnFilter",
floatingFilter: true,
},
{
field: "silver",
aggFunc: "sum",
filter: "agNumberColumnFilter",
floatingFilter: true,
},
{
field: "bronze",
aggFunc: "sum",
filter: "agNumberColumnFilter",
floatingFilter: true,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 120,
};
}, []);
const getRowId = useCallback(
(params: GetRowIdParams<OlympicRow>) => params.data.id,
[],
);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: OlympicRow[]) => {
// Olympic rows aren't unique by athlete/country/year/sport, so a composite natural
// key collides. Index-based ids guarantee uniqueness.
const dataWithIds: OlympicRow[] = data.map((row, i) => ({
...row,
id: `row-${i}`,
}));
fakeServer = new FakeServer(dataWithIds);
params.api!.setGridOption(
"serverSideDatasource",
getServerSideDatasource(fakeServer),
);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={gridStyle}>
<AgGridReact<OlympicRow>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
rowModelType={"serverSide"}
grandTotalRow={"bottom"}
cacheBlockSize={20}
getRowId={getRowId}
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%;
}
// 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.
// The ID AG Grid uses for the grand total row. Exported as GRAND_TOTAL_ROW_ID from 'ag-grid-community'
const GRAND_TOTAL_ROW_ID = 'rowGroupFooter_ROOT_NODE_ID';
export function FakeServer(allData) {
alasql.options.cache = false;
return {
getData: function (request, needsGrandTotal) {
const rows = executeQuery(request);
const grandTotalData = needsGrandTotal ? computeGrandTotal(request) : undefined;
return {
success: true,
rows,
lastRow: getLastRowIndex(request),
grandTotalData,
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function computeGrandTotal(request) {
const valueCols = request.valueCols ?? [];
// If no value columns are configured there is nothing meaningful to aggregate.
if (valueCols.length === 0) {
return { id: GRAND_TOTAL_ROW_ID };
}
// Aggregate across the entire filtered dataset, ignoring group keys — the grand total
// represents the totals the user would see if all groups were expanded.
const selects = valueCols.map((col) => `${col.aggFunc}(${col.id}) AS ${col.id}`);
const sql = `SELECT ${selects.join(', ')} FROM ?` + whereSql({ ...request, groupKeys: [] });
console.log('[FakeServer] - about to execute grand total query:', sql);
const result = alasql(sql, [allData])[0] ?? {};
return { id: GRAND_TOTAL_ROW_ID, ...result };
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const filterModel = request.filterModel;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (filterModel) {
Object.keys(filterModel).forEach(function (key) {
const item = filterModel[key];
switch (item.filterType) {
case 'text':
whereParts.push(createFilterSql(textFilterMapper, key, item));
break;
case 'number':
whereParts.push(createFilterSql(numberFilterMapper, key, item));
break;
default:
console.log('unknown filter type: ' + item.filterType);
break;
}
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
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 groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
Accessing Grand Total and Group Total Rows Copy Link
Both the grand total and individual group total rows can be retrieved by ID using api.getRowNode(). Two constants, exported from ag-grid-community, define the ID format:
GRAND_TOTAL_ROW_ID('rowGroupFooter_ROOT_NODE_ID') — the ID of the grand total row.GROUP_TOTAL_ROW_ID_PREFIX('rowGroupFooter_') — the prefix for group total row IDs. A group total row ID isGROUP_TOTAL_ROW_ID_PREFIX + groupRowNode.id.
// Retrieve the grand total row node
const grandTotalNode = api.getRowNode(GRAND_TOTAL_ROW_ID);
// Retrieve a group total row node (e.g. for group "Ireland")
const groupTotalNode = api.getRowNode(GROUP_TOTAL_ROW_ID_PREFIX + groupRowNode.id); Grand Total Row API Reference Copy Link
When provided, an extra grand total row will be inserted into the grid at the specified position.
This row displays the aggregate totals of all rows in the grid. |
Hide Open Parents Copy Link
In some configurations it may be desired for the group row to be hidden when expanded, this can be achieved by setting the groupHideOpenParents property to true.
The example below has been styled in a way that demonstrates the behaviour of the groups. Note how upon expanding a group, the group row is replaced by the first of its children, and only when collapsed is the group row is shown again.
("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,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [RowGroupingModule, ServerSideRowModelModule];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 500);
},
};
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, hide: true },
{ field: "sport", rowGroup: true, hide: true },
{ field: "year", minWidth: 100 },
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const 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 className="example-wrapper">
<div className="example-header">
<span className="legend-item ag-row-level-0"></span>
<span className="legend-label">Top Level Group</span>
<span className="legend-item ag-row-level-1"></span>
<span className="legend-label">Second Level Group</span>
<span className="legend-item ag-row-level-2"></span>
<span className="legend-label">Bottom Rows</span>
</div>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
groupHideOpenParents={true}
cacheBlockSize={5}
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%;
}
.example-header {
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 13px;
margin-bottom: 5px;
display: flex;
align-items: center;
}
.ag-row-level-0 {
background-color: #cc222244;
}
.ag-row-level-1 {
background-color: #33cc3344;
}
.ag-row-level-2 {
background-color: #2244cc44;
}
.legend-item {
display: inline-block;
width: 20px;
height: 20px;
border: 1px solid darkgrey;
}
.legend-label {
position: relative;
margin-right: 20px;
padding-left: 0.25rem;
}
// 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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} When groupHideOpenParents=true the Grid automatically disables the Sticky Groups behaviour of the rows as well as Full Width Loading.
Unbalanced Groups Copy Link
To enable unbalanced groups in the SSRM, set the groupAllowUnbalanced property to true. This causes any group with a key of '' to behave as if it is always expanded, and the group row to always be hidden.
("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,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [RowGroupingModule, ServerSideRowModelModule];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 2000);
},
};
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, hide: true },
{ field: "sport" },
{ field: "year", minWidth: 100 },
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/small-olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// add unbalanced data to the top of the dataset
const unbalancedData = data.map((item: IOlympicData) => ({
...item,
country: item.country === null ? "" : item.country,
}));
unbalancedData.sort((a: IOlympicData, b: IOlympicData) =>
a.country === "" ? -1 : 1,
);
// setup the fake server with entire dataset
const fakeServer = new FakeServer(unbalancedData);
// 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<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
groupAllowUnbalanced={true}
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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} When using groupAllowUnbalanced=true it is important to remember that a row group still exists to contain the unbalanced nodes, this can be an important consideration when working with selection state, refreshing, or group paths. This also means that there will be additional requests and delays in loading these unbalanced rows, as they do not belong to the parent row.
Expand All / Collapse All Copy Link
Group rows can be expanded or collapsed using the expandAll() and collapseAll() grid API's. By default, these operations apply only to loaded group rows (not all groups). To expand/collapse all groups, including those not yet loaded, set ssrmExpandAllAffectsAllRows: true in your grid options.
The example below demonstrates this feature, note the following:
- First button expands all loaded group rows
- Checking the checkbox enables
ssrmExpandAllAffectsAllRowsin the grid options - Now clicking the first button expands all group rows, including those not yet loaded
- Second button collapses all group rows
("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 {
ColDef,
ColGroupDef,
GetRowIdFunc,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
RowGroupingModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 100);
},
};
};
const GridExample = () => {
const gridRef = useRef<AgGridReact>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "90vh", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, hide: true },
{ field: "id", aggFunc: "sum", hide: true },
{ field: "sport", rowGroup: true, hide: true },
{ field: "year", rowGroup: true, hide: true },
{ field: "gold", aggFunc: "sum" },
{ field: "silver", aggFunc: "sum" },
{ field: "bronze", aggFunc: "sum" },
]);
const getRowId = useCallback((params) => {
const parentKeysJoined = (params.parentKeys || []).join("-");
if (params.data.id != null) {
return parentKeysJoined + params.data.id;
}
const rowGroupCols = params.api.getRowGroupColumns();
const thisGroupCol = rowGroupCols[params.level];
return parentKeysJoined + params.data[thisGroupCol.getColDef().field!];
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: any[]) => {
const newData = data.map((e: IOlympicData, i: number) => ({
...e,
id: i,
}));
// setup the fake server with entire dataset
const fakeServer = FakeServer(newData);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(fakeServer);
// register the datasource with the grid
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
const onExpandAll = useCallback(() => {
gridRef.current!.api.expandAll();
}, []);
const onCollapseAll = useCallback(() => {
gridRef.current!.api.collapseAll();
}, []);
const onOptionChange = useCallback(() => {
const ssrmExpandAllAffectsAllRows =
document.querySelector<HTMLInputElement>(
"#ssrmExpandAllAffectsAllRows",
)!.checked;
gridRef.current!.api.setGridOption(
"ssrmExpandAllAffectsAllRows",
ssrmExpandAllAffectsAllRows,
);
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper" style={{ height: "100vh" }}>
<div className="example-header" style={{ height: "10vh" }}>
<button id="expand" onClick={onExpandAll}>
Expand rows
</button>
<button id="collapse" onClick={onCollapseAll}>
Collapse rows
</button>
<label>
ssrmExpandAllAffectsAllRows:
<input
type="checkbox"
id="ssrmExpandAllAffectsAllRows"
onChange={onOptionChange}
/>
</label>
</div>
<div style={gridStyle}>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
getRowId={getRowId}
rowModelType={"serverSide"}
purgeClosedRowNodes={true}
onGridReady={onGridReady}
/>
</div>
</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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
To open only specific groups, e.g. only groups at the top level, then use the forEachNode() callback and open / close the row using setExpanded() as follows:
// Expand all top level row nodes
gridApi.forEachNode(node => {
if (node.group && node.level == 0) {
node.setExpanded(true);
}
});The example below demonstrates these techniques. Note the following:
Clicking 'Expand All' expands all loaded group rows. Doing this when the grid initially loads expands all Year groups. Clicking it a second time (after Year groups have loaded) causes all Year groups as well as their children Country groups to be expanded - this is a heavier operation with 100's of rows to expand.
Clicking 'Collapse All' collapses all rows.
Clicking 'Expand Top Level Only' expands Years only, even if more group rows are loaded.
("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,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsParams,
ModuleRegistry,
RowApiModule,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
RowApiModule,
RowGroupingModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params: IServerSideGetRowsParams) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
groupLevelInfo: {
lastLoadedTime: new Date().toLocaleString(),
randomValue: window.agRandom(),
},
});
} else {
// inform the grid request failed
params.fail();
}
}, 200);
},
};
};
const GridExample = () => {
const gridRef = useRef<AgGridReact<IOlympicData>>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{
field: "year",
enableRowGroup: true,
rowGroup: true,
hide: true,
minWidth: 100,
},
{ field: "country", enableRowGroup: true, rowGroup: true, hide: true },
{ field: "sport", enableRowGroup: true, rowGroup: true, hide: true },
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const 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);
});
}, []);
const onBtExpandAll = useCallback(() => {
gridRef.current!.api.expandAll();
}, []);
const onBtCollapseAll = useCallback(() => {
gridRef.current!.api.collapseAll();
}, []);
const onBtExpandTopLevel = useCallback(() => {
gridRef.current!.api.forEachNode(function (node) {
if (node.group && node.level == 0) {
node.setExpanded(true);
}
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<button onClick={onBtExpandAll}>Expand All</button>
<button onClick={onBtCollapseAll}>Collapse All</button>
<button onClick={onBtExpandTopLevel}>Expand Top Level Only</button>
</div>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
maxConcurrentDatasourceRequests={1}
rowModelType={"serverSide"}
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 auto;
width: 100%;
}
// 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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Providing Child Counts Copy Link
By default, the grid does not show row counts beside the group names. If you do want row counts, you need to implement the getChildCount(dataItem) callback for the grid. The callback provides you with the row data; it is your application's responsibility to know what the child row count is. The suggestion is you set this information into the row data item you provide to the grid.
Allows setting the child count for a group row. |
const getChildCount = data => {
// here child count is stored in the 'childCount' property
return data.childCount;
};
<AgGridReact getChildCount={getChildCount} />("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,
GetChildCount,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [RowGroupingModule, ServerSideRowModelModule];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 200);
},
};
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, hide: true },
{ field: "sport", rowGroup: true, hide: true },
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 150,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
};
}, []);
const getChildCount = useCallback((data: any) => {
return data ? data.childCount : undefined;
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const 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<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
getChildCount={getChildCount}
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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const groupByResult = executeRowGroupQuery(request);
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (!isDoingGrouping(rowGroupCols, groupKeys)) {
return groupByResult;
}
const groupsToUse = request.rowGroupCols.slice(groupKeys.length, groupKeys.length + 1);
const groupColId = groupsToUse[0].id;
const childCountResult = executeGroupChildCountsQuery(request, groupColId);
// add 'childCount' to group results
return groupByResult.map(function (group) {
group['childCount'] = childCountResult[group[groupColId]];
return group;
});
}
function executeRowGroupQuery(request) {
const groupByQuery = buildGroupBySql(request);
console.log('[FakeServer] - about to execute row group query:', groupByQuery);
return alasql(groupByQuery, [allData]);
}
function executeGroupChildCountsQuery(request, groupId) {
const SQL = interpolate('SELECT {0} FROM ? pivot (count({0}) for {0})' + whereSql(request), [groupId]);
console.log('[FakeServer] - about to execute group child count query:', SQL);
return alasql(SQL, [allData])[0];
}
function buildGroupBySql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
// IE Workaround - as templates literals are not supported
function interpolate(str, o) {
return str.replace(/{([^{}]*)}/g, function (a, b) {
const r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Group via Value Getter Copy Link
It is possible the data provided has composite objects, in which case it's more difficult for the grid to extract group names. This can be worked with using value getters or embedded fields (i.e. the field attribute has dot notation).
In the example below, all rows are modified so that the rows look something like this:
// sample contents of row data
const rowData = {
// country field is complex object
country: {
name: 'Ireland',
code: 'IRE'
},
// other fields as normal
...
}Then the columns are set up so that country uses a valueGetter that uses the field with dot notation, i.e. data.country.name
("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,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [RowGroupingModule, ServerSideRowModelModule];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// convert country to a complex object
const resultsWithComplexObjects = response.rows.map(function (row: any) {
row.country = {
name: row.country,
code: row.country.substring(0, 3).toUpperCase(),
};
return row;
});
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: resultsWithComplexObjects,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 200);
},
};
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
// here we are using a valueGetter to get the country name from the complex object
{
colId: "country",
valueGetter: "data.country.name",
rowGroup: true,
hide: true,
},
{ field: "gold", aggFunc: "sum", enableValue: true },
{ field: "silver", aggFunc: "sum", enableValue: true },
{ field: "bronze", aggFunc: "sum", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 150,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const 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<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Filters Copy Link
By default, changing filters fully purges the grid. Though, it can be configured to only refresh when the group has been directly impacted by enabling serverSideOnlyRefreshFilteredGroups. Be aware, this can mean your grid may have empty group rows. This is because the grid does not refresh the groups above the groups it deems impacted by the filter.
In the example below, note the following:
- Filtering by
Gold,SilverorBronzefully purges the grid, this is because they have aggregations applied. - Applying a filter to the
Yearcolumn does not purge the entire grid, and instead only refreshes theYeargroup rows. - The example enables
serverSideOnlyRefreshFilteredGroups, note that if you apply a filter toYearwith the value1900, no leaf rows exist in any group.
("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,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
NumberFilterModule,
RowModelType,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
TextFilterModule,
RowGroupingModule,
ServerSideRowModelModule,
NumberFilterModule,
];
const getServerSideDatasource: (server: any) => IServerSideDatasource = (
server: any,
) => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 1000);
},
};
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", rowGroup: true, hide: true },
{ field: "sport", rowGroup: true, hide: true },
{
field: "year",
minWidth: 100,
filter: "agNumberColumnFilter",
floatingFilter: true,
},
{
field: "gold",
aggFunc: "sum",
filter: "agNumberColumnFilter",
floatingFilter: true,
enableValue: true,
},
{
field: "silver",
aggFunc: "sum",
filter: "agNumberColumnFilter",
floatingFilter: true,
enableValue: true,
},
{
field: "bronze",
aggFunc: "sum",
filter: "agNumberColumnFilter",
floatingFilter: true,
enableValue: true,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
field: "athlete",
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
// setup the fake server with entire dataset
const 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<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
serverSideOnlyRefreshFilteredGroups={true}
rowModelType={"serverSide"}
cacheBlockSize={5}
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) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const filterModel = request.filterModel;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (filterModel) {
Object.keys(filterModel).forEach(function (key) {
const item = filterModel[key];
switch (item.filterType) {
case 'text':
whereParts.push(createFilterSql(textFilterMapper, key, item));
break;
case 'number':
whereParts.push(createFilterSql(numberFilterMapper, key, item));
break;
default:
console.log('unknown filter type: ' + item.filterType);
break;
}
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
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 groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
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) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Deferred Column Configuration Copy Link
You can configure the Columns Tool Panel to stage changes and require an explicit Apply action before they are committed. This allows multiple configuration changes to be made and applied in a single update, avoiding unnecessary intermediate recomputations or requests.
Deferred Updates are enabled by including the Apply button in toolPanelParams.buttons.
Note that in the example below:
- Changes made in the Columns Tool Panel are staged as pending changes.
- Apply commits all pending changes in a single operation.
- Cancel discards all pending changes and restores the last applied 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,
GetChildCount,
GridApi,
GridOptions,
GridReadyEvent,
ModuleRegistry,
RowModelType,
SideBarDef,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
PivotModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { createFakeServer, createServerSideDatasource } from "./fakeServer";
import { IOlympicData } from "./interfaces";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
PivotModule,
RowGroupingPanelModule,
ServerSideRowModelModule,
];
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{
field: "athlete",
minWidth: 200,
enableRowGroup: true,
enablePivot: true,
},
{
field: "age",
enableValue: true,
},
{
field: "country",
minWidth: 200,
enableRowGroup: true,
enablePivot: true,
rowGroupIndex: 1,
},
{
field: "year",
enableRowGroup: true,
enablePivot: true,
},
{
field: "date",
minWidth: 180,
enableRowGroup: true,
enablePivot: true,
},
{
field: "sport",
minWidth: 200,
enableRowGroup: true,
enablePivot: true,
rowGroupIndex: 2,
},
{ field: "gold", hide: true, enableValue: true },
{ field: "silver", hide: true, enableValue: true, aggFunc: "sum" },
{ field: "bronze", hide: true, enableValue: true, aggFunc: "sum" },
{ headerName: "Total", field: "total", enableValue: true },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 150,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
minWidth: 250,
};
}, []);
const sideBar = useMemo<
SideBarDef | string | string[] | boolean | null
>(() => {
return {
toolPanels: [
{
id: "columns",
labelDefault: "Columns",
labelKey: "columns",
iconKey: "columns",
toolPanel: "agColumnsToolPanel",
toolPanelParams: {
buttons: ["cancel", "apply"],
},
},
],
defaultToolPanel: "columns",
};
}, []);
const getChildCount = useCallback(
(data: any) =>
typeof data?.childCount === "number" ? data.childCount : undefined,
[],
);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => {
const fakeServer = createFakeServer(data);
const datasource = createServerSideDatasource(fakeServer);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={gridStyle}>
<AgGridReact<IOlympicData>
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
rowModelType={"serverSide"}
rowGroupPanelShow={"always"}
pivotPanelShow={"always"}
sideBar={sideBar}
getChildCount={getChildCount}
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%;
}
import type { IServerSideDatasource, IServerSideGetRowsRequest } from 'ag-grid-community';
type ServerResponse = {
success: boolean;
rows: IOlympicData[];
lastRow: number;
pivotResultFields?: string[];
};
export function createServerSideDatasource(server: {
getData: (request: IServerSideGetRowsRequest) => ServerResponse;
}): IServerSideDatasource {
return {
getRows: (params) => {
console.log('server request', {
rowGroups: params.request.rowGroupCols?.map((col) => col.id) ?? [],
groupKeys: params.request.groupKeys ?? [],
pivots: params.request.pivotCols?.map((col) => col.id) ?? [],
values: params.request.valueCols?.map((col) => `${col.id}:${col.aggFunc ?? 'sum'}`) ?? [],
sortModel: params.request.sortModel ?? [],
});
const response = server.getData(params.request);
setTimeout(() => {
if (response.success) {
params.success({
rowData: response.rows,
rowCount: response.lastRow,
pivotResultFields: response.pivotResultFields,
});
} else {
params.fail();
}
}, window.agRandom() * 1000);
},
};
}
export function createFakeServer(allData: IOlympicData[]) {
const matchesPivotKey = (row: IOlympicData, pivotColIds: string[], pivotKey: string[]) => {
for (let i = 0; i < pivotColIds.length; i++) {
if (String((row as any)[pivotColIds[i]]) !== pivotKey[i]) {
return false;
}
}
return true;
};
const createPivotField = (pivotKey: string[], valueColId: string) => `${pivotKey.join('_')}_${valueColId}`;
const toNumber = (value: unknown): number | null => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
};
const aggregateValues = (rows: IOlympicData[], field: string, aggFunc: string): number | string | null => {
const values = rows.map((row) => (row as any)[field]);
switch (aggFunc) {
case 'sum': {
return values.reduce((sum, value) => sum + (toNumber(value) ?? 0), 0);
}
case 'min': {
let min: number | null = null;
for (const value of values) {
const num = toNumber(value);
if (num == null) {
continue;
}
min = min == null ? num : Math.min(min, num);
}
return min;
}
case 'max': {
let max: number | null = null;
for (const value of values) {
const num = toNumber(value);
if (num == null) {
continue;
}
max = max == null ? num : Math.max(max, num);
}
return max;
}
case 'avg': {
let sum = 0;
let count = 0;
for (const value of values) {
const num = toNumber(value);
if (num == null) {
continue;
}
sum += num;
count++;
}
return count > 0 ? sum / count : null;
}
case 'count': {
return values.length;
}
case 'first': {
return values.length ? ((values[0] as any) ?? null) : null;
}
case 'last': {
return values.length ? ((values[values.length - 1] as any) ?? null) : null;
}
default:
return values.reduce((sum, value) => sum + (toNumber(value) ?? 0), 0);
}
};
const sortRows = (rows: IOlympicData[], sortModel: IServerSideGetRowsRequest['sortModel']) => {
if (!sortModel?.length) {
return rows;
}
return [...rows].sort((a: any, b: any) => {
for (const sort of sortModel) {
const left = a[sort.colId];
const right = b[sort.colId];
if (left === right) {
continue;
}
const compare = left > right ? 1 : -1;
return sort.sort === 'asc' ? compare : -compare;
}
return 0;
});
};
const compareKeys = (left: string, right: string): number => {
const leftNum = toNumber(left);
const rightNum = toNumber(right);
if (leftNum != null && rightNum != null) {
return leftNum - rightNum;
}
return left.localeCompare(right, undefined, { numeric: true, sensitivity: 'base' });
};
return {
getData: (request: IServerSideGetRowsRequest): ServerResponse => {
let rows = allData;
const { rowGroupCols = [], pivotCols = [], groupKeys = [], valueCols = [], sortModel = [] } = request;
for (let i = 0; i < groupKeys.length; i++) {
const key = groupKeys[i];
const rowGroupCol = rowGroupCols[i];
if (!rowGroupCol) {
continue;
}
rows = rows.filter((row) => String((row as any)[rowGroupCol.id]) === String(key));
}
const isGrouping = rowGroupCols.length > groupKeys.length;
let resultRows: IOlympicData[];
let pivotResultFields: string[] | undefined;
const hasPivot = pivotCols.length > 0 && valueCols.length > 0;
const pivotColIds = pivotCols.map((col) => col.id);
const pivotKeys = hasPivot
? Array.from(new Set(rows.map((row) => pivotColIds.map((id) => String((row as any)[id])).join('|'))))
.map((key) => key.split('|'))
.sort((a, b) => {
const len = Math.max(a.length, b.length);
for (let i = 0; i < len; i++) {
const cmp = compareKeys(a[i] ?? '', b[i] ?? '');
if (cmp !== 0) {
return cmp;
}
}
return 0;
})
: [];
const buildPivotedRow = (targetRows: IOlympicData[], base: any = {}) => {
const result: any = { ...base };
for (const pivotKey of pivotKeys) {
const matchingRows = targetRows.filter((row) => matchesPivotKey(row, pivotColIds, pivotKey));
for (const valueCol of valueCols) {
const field = createPivotField(pivotKey, valueCol.id);
result[field] = aggregateValues(matchingRows, valueCol.id, valueCol.aggFunc!);
}
}
return result as IOlympicData;
};
if (isGrouping) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const groupField = rowGroupCol.id;
const grouped = new Map<string, IOlympicData[]>();
for (const row of rows) {
const key = String((row as any)[groupField]);
const bucket = grouped.get(key);
if (bucket) {
bucket.push(row);
} else {
grouped.set(key, [row]);
}
}
resultRows = Array.from(grouped.entries())
.sort(([leftKey, leftRows], [rightKey, rightRows]) => {
if (groupField === 'country') {
const countDiff = rightRows.length - leftRows.length;
if (countDiff !== 0) {
return countDiff;
}
}
return compareKeys(leftKey, rightKey);
})
.map(([key, groupRows]) => {
if (hasPivot) {
return buildPivotedRow(groupRows, { [groupField]: key, childCount: groupRows.length });
}
const groupRow: any = { [groupField]: key, childCount: groupRows.length };
for (const valueCol of valueCols) {
groupRow[valueCol.id] = aggregateValues(groupRows, valueCol.id, valueCol.aggFunc!);
}
return groupRow as IOlympicData;
});
} else {
resultRows = hasPivot ? [buildPivotedRow(rows)] : [...rows];
}
if (hasPivot) {
pivotResultFields = pivotKeys.flatMap((pivotKey) =>
valueCols.map((valueCol) => createPivotField(pivotKey, valueCol.id))
);
}
const sortedRows = sortRows(resultRows, sortModel);
const start = request.startRow ?? 0;
const end = request.endRow ?? sortedRows.length;
const requestedRows = sortedRows.slice(start, end);
return {
success: true,
rows: requestedRows,
lastRow: sortedRows.length,
pivotResultFields,
};
},
};
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} const sideBar = useMemo(() => {
return {
toolPanels: [
{
id: 'columns',
labelDefault: 'Columns',
labelKey: 'columns',
iconKey: 'columns',
toolPanel: 'agColumnsToolPanel',
toolPanelParams: {
buttons: ['cancel', 'apply'],
},
},
],
defaultToolPanel: 'columns',
};
}, []);
<AgGridReact sideBar={sideBar} />Changes made outside the Columns Tool Panel — such as dragging columns into the Row Group or Pivot Panels, using the Column Menu, or calling the Grid / Column API — are applied immediately and clear any pending changes. Column pinning, resizing, and group expansion do not clear pending changes.