If you are dealing with large amounts of data, your applications may decide to use pagination to help the user navigate through the data.
Enabling Pagination Copy Link
Pagination is enabled in the grid via the pagination grid option. The pagination page size is typically set alongside this using the paginationPageSize option. These options are shown below:
<ag-grid-angular
[pagination]="pagination"
[paginationPageSize]="paginationPageSize"
[paginationPageSizeSelector]="paginationPageSizeSelector"
/* other grid options ... */ />
// enables pagination in the grid
this.pagination = true;
// sets 10 rows per page (default is 100)
this.paginationPageSize = 10;
// allows the user to select the page size from a predefined list of page sizes
this.paginationPageSizeSelector = [10, 20, 50, 100];For more configuration details see the section on Pagination.
Server-Side Pagination Copy Link
The actual pagination of rows is performed on the server when using the Server-Side Row Model. When the grid needs more rows it makes a request via getRows(params) on the Server-Side Datasource with metadata containing pagination details.
The properties relevant to pagination in the request are shown below:
// IServerSideGetRowsRequest
{
// first row requested
startRow: number,
// index after last row requested
endRow: number,
... // other params
}The endRow requested by the grid may not actually exist in the data so the correct lastRowIndex should be supplied in the response to the grid. See Server-Side Datasource for more details.
Example: Server-Side Pagination Copy Link
The example below demonstrates server-side Pagination. Note the following:
- Pagination is enabled using the grid option
pagination=true. - A pagination page size of 20 (default is 100) is set using the grid option
paginationPageSize=20. - The number of rows returned per request is set to 10 (default is 100) using
cacheBlockSize=10. - Use the arrows in the pagination panel to traverse the data. Note the last page arrow is greyed out as the last row index is only supplied to the grid when the last row has been reached.
- Open the browser's dev console to view the request supplied to the datasource.
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
PaginationModule,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
PaginationModule,
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
ServerSideRowModelModule,
]);
import { IOlympicDataWithId } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[rowModelType]="rowModelType"
[pagination]="true"
[paginationPageSize]="paginationPageSize"
[cacheBlockSize]="cacheBlockSize"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
/> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "id", maxWidth: 75 },
{ field: "athlete", minWidth: 190 },
{ field: "age" },
{ field: "year" },
{ field: "gold" },
{ field: "silver" },
{ field: "bronze" },
];
defaultColDef: ColDef = {
flex: 1,
minWidth: 90,
};
rowModelType: RowModelType = "serverSide";
paginationPageSize = 20;
cacheBlockSize = 10;
rowData!: IOlympicDataWithId[];
constructor(private http: HttpClient) {}
onGridReady(params: GridReadyEvent<IOlympicDataWithId>) {
this.http
.get<
IOlympicDataWithId[]
>("https://www.ag-grid.com/example-assets/olympic-winners.json")
.subscribe((data) => {
// add id to data
let idSequence = 1;
data.forEach(function (item: any) {
item.id = idSequence++;
});
// 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);
});
}
}
function getServerSideDatasource(server: any): IServerSideDatasource {
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);
},
};
}
// 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),
};
},
getCountries: function () {
const SQL = 'SELECT DISTINCT country FROM ? ORDER BY country ASC';
return alasql(SQL, [allData]).map(function (x) {
return x.country;
});
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return 'SELECT * FROM ?' + whereSql(request) + orderBySql(request) + limitSql(request);
}
function whereSql(request) {
const whereParts = [];
const filterModel = request.filterModel;
if (filterModel) {
Object.keys(filterModel).forEach(function (columnKey) {
const filter = filterModel[columnKey];
if (filter.filterType === 'set') {
whereParts.push(columnKey + " IN ('" + filter.values.join("', '") + "')");
return;
}
console.log('unsupported filter type: ' + filter.filterType);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
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 getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
if (new URLSearchParams(window.location.search).get('prod') !== 'false') {
enableProdMode();
}
const app = bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
});
export interface IOlympicDataWithId extends IOlympicData {
id: number;
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Pagination with Groups Copy Link
When grouping, pagination splits rows according to top-level groups only. This has the following implications:
- The number of pages is determined by the number of top-level rows and not children
- When groups are expanded, the number of pagination pages does not change.
- When groups are expanded, all children rows appear on the same page as the parent row.
The example below demonstrates pagination with grouping. Note the following:
- No block size is specified so 100 rows per block is used.
- Grid property
paginationAutoPageSize=trueis set. This means the number of displayed rows is automatically set to the number of rows that fit the vertical scroll, so no vertical scroll is present. - As rows are expanded, the number of visible rows in a page grows. The children appear on the same row as the parent and no rows are pushed to the next page.
- For example, expand 'Australia' which will result in a large list for which vertical scrolling will be needed to view all children.
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
PaginationModule,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
PaginationModule,
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
RowGroupingModule,
ServerSideRowModelModule,
]);
import { IOlympicData } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[autoGroupColumnDef]="autoGroupColumnDef"
[rowModelType]="rowModelType"
[pagination]="true"
[paginationAutoPageSize]="true"
[suppressAggFuncInHeader]="true"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
/> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "country", rowGroup: true, hide: true },
{ field: "athlete", minWidth: 190 },
{ field: "gold", aggFunc: "sum" },
{ field: "silver", aggFunc: "sum" },
{ field: "bronze", aggFunc: "sum" },
];
defaultColDef: ColDef = {
flex: 1,
minWidth: 90,
};
autoGroupColumnDef: AutoGroupColumnDef = {
flex: 1,
minWidth: 180,
};
rowModelType: RowModelType = "serverSide";
rowData!: IOlympicData[];
constructor(private http: HttpClient) {}
onGridReady(params: GridReadyEvent<IOlympicData>) {
this.http
.get<
IOlympicData[]
>("https://www.ag-grid.com/example-assets/olympic-winners.json")
.subscribe((data) => {
// 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);
});
}
}
function getServerSideDatasource(server: any): IServerSideDatasource {
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);
},
};
}
// 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;
}
}
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
if (new URLSearchParams(window.location.search).get('prod') !== 'false') {
enableProdMode();
}
const app = bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
});
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} Pagination with Child Rows Copy Link
If it is desired to keep the row count exactly at the page size, then set grid property paginateChildRows=true.
This will have the effect that child rows will get included in the pagination calculation. This will mean if a group is expanded, the pagination will split the child rows across pages and also possibly push later groups into later pages.
The example below demonstrates pagination with grouping and paginateChildRows=true. Note the following:
No block size is specified thus 100 rows per block is used.
Grid property
paginationAutoPageSize=trueis set. This means the number of displayed rows is automatically set to the number of rows that fit the vertical scroll.As rows are expanded, the number of visible rows in each page is fixed. This means expanding groups will push rows to the next page. This includes later group rows and also its own child rows (if the child rows don't fit on the current page).
If the last visible row is expanded, the grid gives a confusing user experience, as the rows appear on the next page. So the user will have to click 'expand' and then click 'next page' to see the child rows. This is the desired behaviour as the grid keeps the number of rows on one page consistent. If this behaviour is not desired, then do not use
paginationAutoPageSize=true.
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
AutoGroupColumnDef,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
ModuleRegistry,
PaginationModule,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
PaginationModule,
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
RowGroupingModule,
ServerSideRowModelModule,
]);
import { IOlympicData } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[autoGroupColumnDef]="autoGroupColumnDef"
[rowModelType]="rowModelType"
[cacheBlockSize]="cacheBlockSize"
[pagination]="true"
[paginationAutoPageSize]="true"
[paginateChildRows]="true"
[suppressAggFuncInHeader]="true"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
/> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "country", rowGroup: true, hide: true },
{ field: "athlete" },
{ field: "gold", aggFunc: "sum" },
{ field: "silver", aggFunc: "sum" },
{ field: "bronze", aggFunc: "sum" },
];
defaultColDef: ColDef = {
flex: 1,
minWidth: 100,
};
autoGroupColumnDef: AutoGroupColumnDef = {
flex: 1,
minWidth: 180,
};
rowModelType: RowModelType = "serverSide";
cacheBlockSize = 100;
rowData!: IOlympicData[];
constructor(private http: HttpClient) {}
onGridReady(params: GridReadyEvent<IOlympicData>) {
this.http
.get<
IOlympicData[]
>("https://www.ag-grid.com/example-assets/olympic-winners.json")
.subscribe((data) => {
// 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);
});
}
}
function getServerSideDatasource(server: any): IServerSideDatasource {
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);
},
};
}
// 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;
}
}
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
if (new URLSearchParams(window.location.search).get('prod') !== 'false') {
enableProdMode();
}
const app = bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
});
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
}