Skip to content

Pagination and Scrolling

Pagination and vertical scrolling are currently strongly coupled concepts in ngx-datatable. For the user, pagination can occur in two ways:

  1. Continuous pagination — Pages form one continuous scroll range. Scrolling across a page boundary updates offset and emits (page). The pager and scrollbar control the same position.
  2. 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.