The Server-Side Row Model displays loading rows while it requests data from the datasource, including when scrolling or expanding groups. Full-width loading rows are displayed by default; skeleton loading displays an indicator in each cell instead.
These loading rows are managed by the row model, not by the loading or loadingRows grid options. For application-controlled loading with the Client-Side Row Model, see Loading Rows.
Full Width Loading Row Copy Link
The example below demonstrates replacing the Provided Loading Component with a Custom Loading Component.
- Custom Loading Component is supplied via
gridOptions.loadingCellRenderer. - Custom Loading Component Parameters are supplied using
gridOptions.loadingCellRendererParams. - Example simulates a long delay to display the spinner clearly.
- Scrolling the grid will request more rows and again display the loading cell renderer.
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsRequest,
ModuleRegistry,
NumberEditorModule,
NumberFilterModule,
RowModelType,
TextEditorModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import { ServerSideRowModelModule } from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
NumberEditorModule,
TextEditorModule,
TextFilterModule,
NumberFilterModule,
ServerSideRowModelModule,
]);
import { CustomLoadingCellRenderer } from "./custom-loading-cell-renderer.component";
import { IOlympicData } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular, CustomLoadingCellRenderer],
template: `<div
style="height: 100%; padding-top: 25px; box-sizing: border-box"
>
<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[loadingCellRenderer]="loadingCellRenderer"
[loadingCellRendererParams]="loadingCellRendererParams"
[rowModelType]="rowModelType"
[cacheBlockSize]="cacheBlockSize"
[maxBlocksInCache]="maxBlocksInCache"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
/>
</div> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "id" },
{ field: "athlete", width: 150 },
{ field: "age" },
{ field: "country" },
{ field: "year" },
{ field: "sport" },
{ field: "gold" },
{ field: "silver" },
{ field: "bronze" },
];
defaultColDef: ColDef = {
editable: true,
flex: 1,
minWidth: 100,
filter: true,
};
loadingCellRenderer: any = CustomLoadingCellRenderer;
loadingCellRendererParams: any = {
loadingMessage: "One moment please...",
};
rowModelType: RowModelType = "serverSide";
cacheBlockSize = 20;
maxBlocksInCache = 10;
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) => {
// add id to data
let idSequence = 0;
data.forEach((item: any) => {
item.id = idSequence++;
});
const server: any = getFakeServer(data);
const datasource: IServerSideDatasource =
getServerSideDatasource(server);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}
}
function getServerSideDatasource(server: any): IServerSideDatasource {
return {
getRows: (params) => {
// adding delay to simulate real server call
setTimeout(() => {
const response = server.getResponse(params.request);
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 4000);
},
};
}
function getFakeServer(allData: any[]): any {
return {
getResponse: (request: IServerSideGetRowsRequest) => {
console.log(
"asking for rows: " + request.startRow + " to " + request.endRow,
);
// take a slice of the total rows
const rowsThisPage = allData.slice(request.startRow, request.endRow);
// if on or after the last page, work out the last row.
const lastRow =
allData.length <= (request.endRow || 0) ? allData.length : -1;
return {
success: true,
rows: rowsThisPage,
lastRow: lastRow,
};
},
};
}
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import type { ILoadingCellRendererAngularComp } from 'ag-grid-angular';
import type { ILoadingCellRendererParams } from 'ag-grid-community';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="ag-custom-loading-cell" style="padding-left: 10px; line-height: 25px;">
<i class="fas fa-spinner fa-pulse"></i>
<span> {{ loadingMessage() }} </span>
</div>
`,
})
export class CustomLoadingCellRenderer implements ILoadingCellRendererAngularComp {
loadingMessage = signal('');
agInit(params: ILoadingCellRendererParams & { loadingMessage: string }): void {
this.loadingMessage.set(params.loadingMessage);
}
}
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
} See Loading Cell Component for component interfaces, parameters and dynamic component selection.
Failed Loading Copy Link
When using a Custom Loading Component, you can add handling for loading failures in the component directly.
In the example below, note that:
- Custom Loading Component is supplied via
gridOptions.loadingCellRenderer. - Custom Loading Component Parameters are supplied using
gridOptions.loadingCellRendererParams. - The example simulates a long delay to display the spinner clearly and simulates a loading failure.
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsRequest,
ModuleRegistry,
NumberEditorModule,
NumberFilterModule,
RowModelType,
TextEditorModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import { ServerSideRowModelModule } from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
NumberEditorModule,
TextEditorModule,
TextFilterModule,
NumberFilterModule,
ServerSideRowModelModule,
]);
import { CustomLoadingCellRenderer } from "./custom-loading-cell-renderer.component";
import { IOlympicData } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular, CustomLoadingCellRenderer],
template: `<div
style="height: 100%; padding-top: 25px; box-sizing: border-box"
>
<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[loadingCellRenderer]="loadingCellRenderer"
[loadingCellRendererParams]="loadingCellRendererParams"
[rowModelType]="rowModelType"
[cacheBlockSize]="cacheBlockSize"
[serverSideInitialRowCount]="serverSideInitialRowCount"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
/>
</div> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "id" },
{ field: "athlete", width: 150 },
{ field: "age" },
{ field: "country" },
{ field: "year" },
{ field: "sport" },
{ field: "gold" },
{ field: "silver" },
{ field: "bronze" },
];
defaultColDef: ColDef = {
editable: true,
flex: 1,
minWidth: 100,
filter: true,
};
loadingCellRenderer: any = CustomLoadingCellRenderer;
loadingCellRendererParams: any = {
loadingMessage: "One moment please...",
};
rowModelType: RowModelType = "serverSide";
cacheBlockSize = 10;
serverSideInitialRowCount = 10;
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) => {
// add id to data
let idSequence = 0;
data.forEach((item: any) => {
item.id = idSequence++;
});
const server: any = getFakeServer(data);
const datasource: IServerSideDatasource =
getServerSideDatasource(server);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}
}
function getServerSideDatasource(server: any): IServerSideDatasource {
return {
getRows: (params) => {
// adding delay to simulate real server call
setTimeout(() => {
// Fail loading to display failed loading cell renderer
params.fail();
}, 4000);
},
};
}
function getFakeServer(allData: any[]): any {
return {
getResponse: (request: IServerSideGetRowsRequest) => {
console.log(
"asking for rows: " + request.startRow + " to " + request.endRow,
);
// take a slice of the total rows
const rowsThisPage = allData.slice(request.startRow, request.endRow);
// if on or after the last page, work out the last row.
const lastRow =
allData.length <= (request.endRow || 0) ? allData.length : -1;
return {
success: true,
rows: rowsThisPage,
lastRow: lastRow,
};
},
};
}
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import type { ILoadingCellRendererAngularComp } from 'ag-grid-angular';
import type { ILoadingCellRendererParams } from 'ag-grid-community';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="ag-custom-loading-cell" style="padding-left: 10px; line-height: 25px;">
@if (failedLoad()) {
<i class="fas fa-times"></i>
<span> Data failed to load </span>
} @else {
<i class="fas fa-spinner fa-pulse"></i>
<span> {{ loadingMessage() }} </span>
}
</div>
`,
})
export class CustomLoadingCellRenderer implements ILoadingCellRendererAngularComp {
failedLoad = signal<boolean | undefined>(false);
loadingMessage = signal('');
agInit(params: ILoadingCellRendererParams & { loadingMessage: string }): void {
this.failedLoad.set(params.node.failedLoad);
this.loadingMessage.set(params.loadingMessage);
}
}
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
} For retrying failed datasource requests, see Load Retry.
Skeleton Loading Copy Link
The Server-Side Row Model can display loading indicators in cells by enabling suppressServerSideFullWidthLoadingRow.
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsRequest,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([ServerSideRowModelModule, RowGroupingModule]);
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"
[rowModelType]="rowModelType"
[suppressServerSideFullWidthLoadingRow]="true"
[cacheBlockSize]="cacheBlockSize"
[maxBlocksInCache]="maxBlocksInCache"
[rowBuffer]="rowBuffer"
[maxConcurrentDatasourceRequests]="maxConcurrentDatasourceRequests"
[blockLoadDebounceMillis]="blockLoadDebounceMillis"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
/> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "country", flex: 4 },
{ field: "sport", flex: 4 },
{ field: "year", flex: 3 },
{ field: "gold", aggFunc: "sum", flex: 2 },
{ field: "silver", aggFunc: "sum", flex: 2 },
{ field: "bronze", aggFunc: "sum", flex: 2 },
];
defaultColDef: ColDef = {
minWidth: 75,
};
rowModelType: RowModelType = "serverSide";
cacheBlockSize = 5;
maxBlocksInCache = 0;
rowBuffer = 0;
maxConcurrentDatasourceRequests = 1;
blockLoadDebounceMillis = 200;
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) => {
// add id to data
let idSequence = 0;
data.forEach((item: any) => {
item.id = idSequence++;
});
const server: any = getFakeServer(data);
const datasource: IServerSideDatasource =
getServerSideDatasource(server);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}
}
function getServerSideDatasource(server: any): IServerSideDatasource {
return {
getRows: (params) => {
// adding delay to simulate real server call
setTimeout(() => {
const response = server.getResponse(params.request);
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 4000);
},
};
}
function getFakeServer(allData: any[]): any {
return {
getResponse: (request: IServerSideGetRowsRequest) => {
console.log(
"[Datasource] asking for rows: " +
request.startRow +
" to " +
request.endRow,
);
// take a slice of the total rows
const rowsThisPage = allData.slice(request.startRow, request.endRow);
const lastRow = allData.length;
return {
success: true,
rows: rowsThisPage,
lastRow: lastRow,
};
},
};
}
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
} const gridOptions = {
suppressServerSideFullWidthLoadingRow: true,
}; Custom Loading Cells Copy Link
Set loadingCellRenderer on a column definition to customise its loading cells.
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
IServerSideDatasource,
IServerSideGetRowsRequest,
ModuleRegistry,
RowModelType,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([ServerSideRowModelModule, RowGroupingModule]);
import { CustomLoadingCellRenderer } from "./custom-loading-cell-renderer.component";
import { IOlympicData } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular, CustomLoadingCellRenderer],
template: `<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[rowModelType]="rowModelType"
[cacheBlockSize]="cacheBlockSize"
[maxBlocksInCache]="maxBlocksInCache"
[rowBuffer]="rowBuffer"
[maxConcurrentDatasourceRequests]="maxConcurrentDatasourceRequests"
[blockLoadDebounceMillis]="blockLoadDebounceMillis"
[suppressServerSideFullWidthLoadingRow]="true"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
/> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{
field: "country",
flex: 4,
loadingCellRenderer: CustomLoadingCellRenderer,
},
{ field: "sport", flex: 4 },
{ field: "year", flex: 3 },
{ field: "gold", aggFunc: "sum", flex: 2 },
{ field: "silver", aggFunc: "sum", flex: 2 },
{ field: "bronze", aggFunc: "sum", flex: 2 },
];
defaultColDef: ColDef = {
loadingCellRenderer: () => "",
minWidth: 75,
};
rowModelType: RowModelType = "serverSide";
cacheBlockSize = 5;
maxBlocksInCache = 0;
rowBuffer = 0;
maxConcurrentDatasourceRequests = 1;
blockLoadDebounceMillis = 200;
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) => {
// add id to data
let idSequence = 0;
data.forEach((item: any) => {
item.id = idSequence++;
});
const server: any = getFakeServer(data);
const datasource: IServerSideDatasource =
getServerSideDatasource(server);
params.api!.setGridOption("serverSideDatasource", datasource);
});
}
}
function getServerSideDatasource(server: any): IServerSideDatasource {
return {
getRows: (params) => {
// adding delay to simulate real server call
setTimeout(() => {
const response = server.getResponse(params.request);
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 1000);
},
};
}
function getFakeServer(allData: any[]): any {
return {
getResponse: (request: IServerSideGetRowsRequest) => {
console.log(
"asking for rows: " + request.startRow + " to " + request.endRow,
);
// take a slice of the total rows
const rowsThisPage = allData.slice(request.startRow, request.endRow);
const lastRow = allData.length;
return {
success: true,
rows: rowsThisPage,
lastRow: lastRow,
};
},
};
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<img src="https://www.ag-grid.com/example-assets/loading.gif" />`,
})
export class CustomLoadingCellRenderer {
agInit(): void {}
}
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
} const gridOptions = {
suppressServerSideFullWidthLoadingRow: true,
columnDefs: [
{ field: 'country', loadingCellRenderer: CustomLoadingCellRenderer },
// More columns, with no load renderer...
],
defaultColDef: {
loadingCellRenderer: () => '',
},
};The above example demonstrates the following:
suppressServerSideFullWidthLoadingRowis enabled, preventing the grid from defaulting to full width loading.loadingCellRendereris configured on the Country column, allowing a loading spinner to be displayed for just this column.loadingCellRendereris configured ondefaultColDefto leave loading cells empty in the other columns.
See Loading Cell Component for the shared component API.