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.