Pagination and Scrolling
Pagination and vertical scrolling are currently strongly coupled concepts in ngx-datatable.
For the user, pagination can occur in two ways:
- Continuous pagination — Pages form one continuous scroll range. Scrolling across a page
boundary updates
offsetand emits(page). The pager and scrollbar control the same position. - Discrete pagination — Only the current page is presented. Scrolling stays within that page; another page is reached through the pager or application logic.
Neither mode is enabled by default. Without scrollbarV or limit, the page size is the number of
supplied rows. A pager can be displayed for both modes.
Continuous pagination
Setting scrollbarV="true" enables continuous pagination. The table uses its available height as a
viewport and adds a vertical scrollbar when the rows exceed that height. Give the table, or its
containing element, an explicit height for this calculation. Set scrollbarVDynamic="true"
together with scrollbarV if the table should reclaim the scrollbar width when no vertical
scrollbar is needed.
The table divides the scroll range into pages internally, but the user can still scroll through the
entire list. pageSize is the number of rows that are at least partially visible in the viewport.
When scrolling crosses a page boundary, offset is updated and a (page) event is emitted.
Page size with continuous pagination
limit does not override the calculated page size while scrollbarV and virtualization are
enabled.
To use a fixed page size and restrict scrolling to the current page, set limit, disable virtualization,
and enable externalPaging while supplying only the current page's rows.
Show source
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { DataTableColumnDirective, DatatableComponent } from '@siemens/ngx-datatable';
import { DataService } from '../data.service';
@Component({
selector: 'horz-vert-scrolling-demo',
imports: [DatatableComponent, DataTableColumnDirective, AsyncPipe],
template: `
<ngx-datatable
class="material"
columnMode="force"
[rows]="rows | async"
[headerHeight]="50"
[footerHeight]="0"
[rowHeight]="50"
[scrollbarV]="true"
[scrollbarH]="true"
>
<ngx-datatable-column name="Name" [width]="300" />
<ngx-datatable-column name="Gender" />
<ngx-datatable-column name="Age" />
<ngx-datatable-column name="City" prop="address.city" [width]="300" />
<ngx-datatable-column name="State" prop="address.state" [width]="300" />
</ngx-datatable>
`,
host: { class: 'datatable-example' }
})
export class HorzVertScrollingComponent {
static readonly exampleTitle = 'Horz/Vert Scrolling';
protected readonly rows = inject(DataService).load('100k.json');
}
By default, no pager is visible. Set footerHeight to a value greater than zero to display it (see
Pager component). In continuous mode, selecting a page scrolls the body to the
first row of that page.
Show source
import { Component, inject, signal } from '@angular/core';
import { DatatableComponent, PageEvent } from '@siemens/ngx-datatable';
import { Employee } from '../data.model';
import { MockServerResultsService } from './mock-server-results-service';
import { Page } from './model/page';
@Component({
selector: 'virtual-server-side-demo',
imports: [DatatableComponent],
template: `
@let isLoading = this.isLoading();
@let totalElements = this.totalElements();
@let rows = this.rows();
<ngx-datatable
class="material"
columnMode="force"
[rows]="rows"
[columns]="[
{ name: 'Name', sortable: false },
{ name: 'Gender', sortable: false },
{ name: 'Company', sortable: false }
]"
[headerHeight]="50"
[loadingIndicator]="isLoading > 0"
[ghostLoadingIndicator]="isLoading > 0"
[scrollbarV]="true"
[footerHeight]="50"
[rowHeight]="50"
[externalPaging]="true"
[externalSorting]="true"
[count]="totalElements"
[offset]="pageNumber"
(page)="setPage($event)"
>
<div loading-indicator class="custom-loading-indicator">loading...</div>
</ngx-datatable>
`,
styleUrl: './virtual-server-side.component.scss',
providers: [MockServerResultsService],
host: { class: 'datatable-example' }
})
export class VirtualServerSideComponent {
static readonly exampleTitle = 'Virtual server-side';
private serverResultsService = inject(MockServerResultsService);
readonly totalElements = signal(0);
pageNumber = 0;
readonly rows = signal<Employee[] | undefined>(undefined);
cache: Record<string, boolean> = {};
cachePageSize = 0;
readonly isLoading = signal(0);
setPage(pageInfo: PageEvent) {
// Current page number is determined by last call to setPage
// This is the page the UI is currently displaying
// The current page is based on the UI pagesize and scroll position
// Pagesize can change depending on browser size
this.pageNumber = pageInfo.offset;
// Calculate row offset in the UI using pageInfo
// This is the scroll position in rows
const rowOffset = pageInfo.offset * pageInfo.pageSize;
const page: Page = {
pageNumber: Math.floor(rowOffset / pageInfo.pageSize),
size: pageInfo.pageSize,
totalElements: 0,
totalPages: 0
};
// We keep a index of server loaded pages so we don't load same data twice
// This is based on the server page not the UI
if (this.cachePageSize !== page.size) {
this.cachePageSize = page.size;
this.cache = {};
}
if (this.cache[page.pageNumber]) {
return;
}
this.cache[page.pageNumber] = true;
// Counter of pending API calls
this.isLoading.update(v => v + 1);
this.serverResultsService.getResults(page).subscribe(pagedData => {
// Update total count
this.totalElements.set(pagedData.page.totalElements);
// Create array to store data if missing
// The array should have the correct number of with "holes" for missing data
const currentRows = this.rows() ?? new Array<Employee>(this.totalElements() || 0);
// Calc starting row offset
// This is the position to insert the new data
const start = pagedData.page.pageNumber * pagedData.page.size;
// Copy existing data
const rows = [...currentRows];
// Insert new rows into correct position
rows.splice(start, pagedData.page.size, ...pagedData.data);
// Set rows to our new rows for display
this.rows.set(rows);
// Decrement the counter of pending API calls
this.isLoading.update(v => v - 1);
});
}
}
Discrete pagination
Discrete scrolling requires external paging
A vertically scrollable discrete page only works with externalPaging="true". The application must
supply only the rows for the current page and load another page when the pager emits (page).
Set limit to the desired page size to enable discrete pagination. Without vertical scrolling, the
table renders only that number of rows and selecting another page replaces the visible row range.
Show source
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { DatatableComponent } from '@siemens/ngx-datatable';
import { DataService } from '../data.service';
@Component({
selector: 'client-side-paging-demo',
imports: [DatatableComponent, AsyncPipe],
template: `
<ngx-datatable
class="material"
rowHeight="auto"
columnMode="force"
[rows]="rows | async"
[columns]="[{ name: 'Name' }, { name: 'Gender' }, { name: 'Company' }]"
[headerHeight]="50"
[footerHeight]="50"
[limit]="10"
/>
`,
host: { class: 'datatable-example' }
})
export class ClientSidePagingComponent {
static readonly exampleTitle = 'Client-side Paging';
protected readonly rows = inject(DataService).load('company.json');
}
Vertical scrolling can also be enabled for a discrete page by setting scrollbarV="true" and
virtualization="false". All rows supplied for the current page are rendered, and the user can
scroll within that page but not into another one. The pager or application logic must change the
page.
Disable virtualization for discrete scrolling
virtualization is enabled by default. If scrollbarV is enabled without disabling virtualization,
the table uses continuous pagination and limit is ignored.
Show source
import { Component, inject, OnInit, signal } from '@angular/core';
import { DatatableComponent } from '@siemens/ngx-datatable';
import { Employee } from '../data.model';
import { MockServerResultsService } from './mock-server-results-service';
import { Page } from './model/page';
@Component({
selector: 'scrolling-no-virtual-demo',
imports: [DatatableComponent],
template: `
@let rows = this.rows();
@let page = this.page();
@let isLoading = this.isLoading();
<ngx-datatable
class="material"
rowHeight="auto"
columnMode="force"
[rows]="rows"
[columns]="[{ name: 'Name' }, { name: 'Gender' }, { name: 'Company' }]"
[headerHeight]="50"
[footerHeight]="50"
[scrollbarV]="true"
[virtualization]="false"
[externalPaging]="true"
[count]="page.totalElements"
[offset]="page.pageNumber"
[limit]="page.size"
[ghostLoadingIndicator]="isLoading > 0"
(page)="setPage($event.offset)"
/>
`,
providers: [MockServerResultsService],
host: { class: 'datatable-example' }
})
export class ScrollingNoVirtualComponent implements OnInit {
static readonly exampleTitle = 'Scrolling no virtual';
private serverResultsService = inject(MockServerResultsService);
readonly page = signal<Page>({
pageNumber: 0,
size: 20,
totalElements: 0,
totalPages: 0
});
readonly rows = signal<Employee[]>([]);
readonly isLoading = signal(0);
ngOnInit() {
this.setPage(0);
}
/**
* Populate the table with new data based on the page number
* @param page The page to select
*/
setPage(page: number) {
this.page.update(currentPage => ({ ...currentPage, pageNumber: page }));
this.isLoading.update(v => v + 1);
this.serverResultsService.getResults(this.page()).subscribe(pagedData => {
this.isLoading.update(v => v - 1);
this.page.set(pagedData.page);
this.rows.set(pagedData.data);
});
}
}
Pager component
Set footerHeight to a value greater than zero to display the table footer. The default footer
shows the total row count and adds the built-in pager when more than one page is available.
It works with both continuous and discrete pagination.
When using a custom footer, place the ngx-datatable-pager component inside the template
if the standard navigation controls should remain available.
Show source
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import {
DatatableComponent,
DatatableFooterDirective,
DataTableFooterTemplateDirective,
TableColumn,
DatatablePagerComponent
} from '@siemens/ngx-datatable';
import { map } from 'rxjs';
import { DataService } from '../data.service';
@Component({
selector: 'footer-template-demo',
imports: [
DatatableComponent,
DatatableFooterDirective,
DataTableFooterTemplateDirective,
DatatablePagerComponent,
AsyncPipe
],
template: `
<ngx-datatable
class="material"
rowHeight="auto"
columnMode="force"
[rows]="rows | async"
[columns]="columns"
[footerHeight]="100"
[headerHeight]="50"
>
<ngx-datatable-footer>
<ng-template
let-rowCount="rowCount"
let-pageSize="pageSize"
let-selectedCount="selectedCount"
let-curPage="curPage"
let-offset="offset"
ngx-datatable-footer-template
>
<div style="padding: 5px 10px">
<div><strong>Summary</strong>: Gender: Female</div>
<hr style="width:100%" />
<div
>Rows: {{ rowCount }} | Size: {{ pageSize }} | Current: {{ curPage }} | Offset:
{{ offset }}</div
>
</div>
<ngx-datatable-pager />
</ng-template>
</ngx-datatable-footer>
</ngx-datatable>
`,
host: { class: 'datatable-example' }
})
export class FooterTemplateComponent {
static readonly exampleTitle = 'Footer Template';
protected readonly rows = inject(DataService)
.load('company.json')
.pipe(map(data => data.slice(0, 5)));
columns: TableColumn[] = [{ prop: 'name' }, { name: 'Gender' }, { name: 'Company' }];
}
Virtualization
Row virtualization is enabled by default. It limits DOM rendering to the rows in the visible viewport while the scrollbar still represents the complete data set. This is what allows continuous pagination to move across page boundaries without rendering every row at once.
With virtualization enabled, rowHeight must be a number or a function that returns each row's
height. The table uses those heights to calculate the scroll range and visible rows. With a fixed
numeric row height, the available body height also determines the page size.
Set virtualization="false" to render every supplied row. rowHeight="auto" can then be used for
rows that grow with their content. When externalPaging is also enabled, body scrolling stays
within the supplied page and does not emit requests for adjacent pages.