ShowcaseDocs and examples
Docs

State

Controlled and uncontrolled slices

NatTable can own all table state, or your app can control only the slices it cares about. The most important rule is simple: a state slice is controlled only when its property is present in [state]. Omitted slices stay internal.

State Slices

NatTableUserState contains the serializable view state exposed by the table and companion surface.

Slice Use it for
sorting Active sort entries
globalFilter Current global search query
columnFilters Column-specific filters keyed by column id
pagination pageIndex and pageSize
columnVisibility Hideable column visibility map
columnOrder Leaf-column order
columnPinning Left and right pinned column ids
columnSizing Resized column widths in pixels
rowSelection Selected row ids keyed by getRowId, row.id, or the namespaced positional fallback

The pagination slice always exists in emitted state. Pagination affects rendered rows only when pagination is enabled by a pagination companion control or a table setup that registers pagination.

Start Uncontrolled

Most tables should start uncontrolled. Pass [initialState] for defaults and let the table manage updates internally.

readonly initialState: Partial<NatTableUserState> = {
  pagination: { pageIndex: 0, pageSize: 25 },
  sorting: [{ id: 'symbol', desc: false }],
  columnPinning: { left: ['symbol'], right: [] },
};
<nat-table-surface [initialState]="initialState">
  <nat-table-pagination [pageSizeOptions]="[25, 50, 100]" />

  <nat-table [data]="rows()" [columns]="columns" accessibleName="Open positions" />
</nat-table-surface>

initialState is read as a seed. It is not a live binding for subsequent resets. If a slice also appears in [state], the controlled value wins.

Own One Slice

When your application needs to persist, synchronize, or inspect one state slice, control only that slice and listen to the matching granular output.

import { Component, computed, signal } from '@angular/core';
import type { NatTableUserState, PaginationState, SortingState, VisibilityState } from 'ng-advanced-table';

export class PositionsTable {
  readonly sorting = signal<SortingState>([]);

  readonly tableState = computed<Partial<NatTableUserState>>(() => ({
    sorting: this.sorting()
  }));

  protected onSortingChange(sorting: SortingState): void {
    this.sorting.set(sorting);
  }
}
<nat-table-surface [state]="tableState()" (sortingChange)="onSortingChange($event)">
  <nat-table [data]="rows()" [columns]="columns" accessibleName="Open positions" />
</nat-table-surface>

This keeps sorting controlled and leaves filters, pagination, pinning, sizing, order, visibility, and selection internal.

Own Multiple Slices

Use one signal when several slices belong to the same workflow, such as URL persistence or a saved table view.

readonly viewState = signal<Partial<NatTableUserState>>({
  sorting: [{ id: 'symbol', desc: false }],
  columnVisibility: { desk: true, exchange: false },
  pagination: { pageIndex: 0, pageSize: 25 },
});

protected onPaginationChange(pagination: PaginationState): void {
  this.viewState.update((state) => ({ ...state, pagination }));
}

protected onColumnVisibilityChange(columnVisibility: VisibilityState): void {
  this.viewState.update((state) => ({ ...state, columnVisibility }));
}
<nat-table-surface
  [state]="viewState()"
  (paginationChange)="onPaginationChange($event)"
  (columnVisibilityChange)="onColumnVisibilityChange($event)">
  <nat-table-pagination [pageSizeOptions]="[25, 50, 100]" />
  <nat-table-column-visibility />
  <nat-table [data]="rows()" [columns]="columns" accessibleName="Open positions" />
</nat-table-surface>

Prefer granular outputs when the app controls known slices. Use (stateChange) when you intentionally need a broad table state snapshot.

Full State Snapshots

(stateChange) is typed as Partial<NatTableUserState> because it pairs with the partial [state] input. The emitted value is useful for telemetry, local storage, or "save current view" commands.

readonly latestState = signal<Partial<NatTableUserState> | null>(null);

protected rememberState(state: Partial<NatTableUserState>): void {
  this.latestState.set(state);
}
<nat-table-surface (stateChange)="rememberState($event)">
  <nat-table [data]="rows()" [columns]="columns" accessibleName="Open positions" />
</nat-table-surface>

Be careful with two-way full-state binding:

<nat-table-surface [(state)]="tableState">
  <nat-table [data]="rows()" [columns]="columns" accessibleName="Open positions" />
</nat-table-surface>

After the first emitted state is assigned back into [state], every slice is present and therefore every slice becomes controlled. Use this only when that is what you want.

URL Persistence

For URL-backed views, store only the slices that should survive reloads. Keep volatile slices such as columnSizing or rowSelection out unless the product specifically needs them.

readonly stateFromUrl = signal<Partial<NatTableUserState>>({
  sorting: [{ id: 'price', desc: true }],
  pagination: { pageIndex: 0, pageSize: 50 },
});

protected onSortingChange(sorting: SortingState): void {
  this.stateFromUrl.update((state) => ({ ...state, sorting, pagination: firstPage(state) }));
  this.writeUrl();
}

protected onPaginationChange(pagination: PaginationState): void {
  this.stateFromUrl.update((state) => ({ ...state, pagination }));
  this.writeUrl();
}

private writeUrl(): void {
  const state = this.stateFromUrl();
  // Serialize only the slices that are part of the route contract.
}

function firstPage(state: Partial<NatTableUserState>): PaginationState {
  const pagination = state.pagination ?? { pageIndex: 0, pageSize: 25 };

  return { ...pagination, pageIndex: 0 };
}

Global filter and column-filter updates reset pagination.pageIndex to 0 inside the table. If your app owns filter slices and pagination together, mirror that behavior when updating your signal.

Manual Data Handling

Use manual mode when your app owns sorting, filtering, or pagination outside the table. The table still emits state changes; your container prepares rows and passes back the current page or current row set.

<nat-table-surface
  [mode]="{ pagination: 'manual', sorting: 'manual', filtering: 'manual' }"
  [manualPageCount]="pageCount()"
  [state]="tableState()"
  (stateChange)="loadPage($event)">
  <nat-table-pagination [pageSizeOptions]="[25, 50, 100]" />
  <nat-table [data]="rows()" [columns]="columns" [dataStatus]="status()" [error]="error()" accessibleName="Manual positions" />
</nat-table-surface>
protected loadPage(state: Partial<NatTableUserState>): void {
  this.tableState.set(state);
  this.status.set(NAT_TABLE_DATA_STATUS.loading);
  const pagination = state.pagination ?? { pageIndex: 0, pageSize: 25 };

  this.positionsApi
    .list({
      pageIndex: pagination.pageIndex,
      pageSize: pagination.pageSize,
      sorting: state.sorting ?? [],
      globalFilter: state.globalFilter ?? '',
      columnFilters: state.columnFilters ?? [],
    })
    .subscribe({
      next: (result) => {
        this.rows.set(result.rows);
        this.pageCount.set(result.pageCount);
        this.status.set(NAT_TABLE_DATA_STATUS.success);
      },
      error: (error: unknown) => {
        this.error.set(error);
        this.status.set(NAT_TABLE_DATA_STATUS.error);
      },
    });
}

You can also mix modes. For example, { pagination: 'manual', sorting: 'auto' } prepares pages externally while sorting the rows currently held by the client.

Stable Row IDs

Rows with a string or number id property get stable ids automatically.

interface PositionRow {
  id: string;
  symbol: string;
}

Provide getRowId when the stable id lives under another property, is composite, or needs parent-aware nested row keys.

interface PositionRow {
  accountId: string;
  positionId: string;
  symbol: string;
}

readonly getRowId = (row: PositionRow) => `${row.accountId}:${row.positionId}`;

Without stable IDs, the table falls back to namespaced row positions. Selection, row activation, and per-row instrumentation can then follow indexes instead of the underlying data after sorting, filtering, paging, or refreshes.