Composable tables are app-level table factories built with createTableHook. Instead of repeating the same features, row models, default options, and table/cell/header components in every Angular table, you define that shared infrastructure once and consume it from each table component.
Use this pattern when multiple tables in an Angular app share behavior or rendering conventions. For a single isolated table, injectTable is usually enough.
The composable tables example keeps the shared setup in src/app/table.ts. That file creates one app-specific table factory and exports the helpers used by the rest of the example.
import {
columnFilteringFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
createTableHook,
filterFns,
rowPaginationFeature,
rowSortingFeature,
sortFns,
tableFeatures,
} from '@tanstack/angular-table'
import {
PaginationControls,
RowCount,
TableToolbar,
} from './components/table-components'
import {
CategoryCell,
NumberCell,
PriceCell,
ProgressCell,
RowActionsCell,
StatusCell,
TextCell,
} from './components/cell-components'
import {
ColumnFilter,
FooterColumnId,
FooterSum,
SortIndicator,
} from './components/header-components'
const features = tableFeatures({
columnFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortFns,
filterFns,
})
export const {
createAppColumnHelper,
injectAppTable,
injectTableContext,
injectTableCellContext,
injectTableHeaderContext,
} = createTableHook({
features,
getRowId: (row) => row.id,
tableComponents: {
PaginationControls,
RowCount,
TableToolbar,
},
cellComponents: {
TextCell,
NumberCell,
ProgressCell,
StatusCell,
CategoryCell,
PriceCell,
RowActionsCell,
},
headerComponents: {
SortIndicator,
ColumnFilter,
FooterColumnId,
FooterSum,
},
})import {
columnFilteringFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
createTableHook,
filterFns,
rowPaginationFeature,
rowSortingFeature,
sortFns,
tableFeatures,
} from '@tanstack/angular-table'
import {
PaginationControls,
RowCount,
TableToolbar,
} from './components/table-components'
import {
CategoryCell,
NumberCell,
PriceCell,
ProgressCell,
RowActionsCell,
StatusCell,
TextCell,
} from './components/cell-components'
import {
ColumnFilter,
FooterColumnId,
FooterSum,
SortIndicator,
} from './components/header-components'
const features = tableFeatures({
columnFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortFns,
filterFns,
})
export const {
createAppColumnHelper,
injectAppTable,
injectTableContext,
injectTableCellContext,
injectTableHeaderContext,
} = createTableHook({
features,
getRowId: (row) => row.id,
tableComponents: {
PaginationControls,
RowCount,
TableToolbar,
},
cellComponents: {
TextCell,
NumberCell,
ProgressCell,
StatusCell,
CategoryCell,
PriceCell,
RowActionsCell,
},
headerComponents: {
SortIndicator,
ColumnFilter,
FooterColumnId,
FooterSum,
},
})This file is the source of truth for the feature set, row model pipeline, row IDs, and registered components used by both tables in the example.
| Helper | Purpose |
|---|---|
| injectAppTable | Creates a table with the app's shared features (including row model factories), defaults, and registered components already attached. |
| createAppColumnHelper | Creates column helpers where cell, header, and footer contexts know about the registered components. |
| injectTableContext | Reads the current table inside registered table components like PaginationControls. |
| injectTableCellContext | Reads the current cell inside registered cell components like TextCell. |
| injectTableHeaderContext | Reads the current header/footer inside registered header components like SortIndicator. |
Use createAppColumnHelper<TData>() instead of the base column helper when column definitions should render registered components.
import { flexRenderComponent } from '@tanstack/angular-table'
import { createAppColumnHelper } from '../../table'
import type { Person } from '../../makeData'
const personColumnHelper = createAppColumnHelper<Person>()
readonly columns = personColumnHelper.columns([
personColumnHelper.accessor('firstName', {
header: 'First Name',
footer: ({ header }) => flexRenderComponent(header.FooterColumnId),
cell: ({ cell }) => flexRenderComponent(cell.TextCell),
}),
personColumnHelper.accessor('age', {
header: 'Age',
footer: ({ header }) => flexRenderComponent(header.FooterSum),
cell: ({ cell }) => flexRenderComponent(cell.NumberCell),
}),
])import { flexRenderComponent } from '@tanstack/angular-table'
import { createAppColumnHelper } from '../../table'
import type { Person } from '../../makeData'
const personColumnHelper = createAppColumnHelper<Person>()
readonly columns = personColumnHelper.columns([
personColumnHelper.accessor('firstName', {
header: 'First Name',
footer: ({ header }) => flexRenderComponent(header.FooterColumnId),
cell: ({ cell }) => flexRenderComponent(cell.TextCell),
}),
personColumnHelper.accessor('age', {
header: 'Age',
footer: ({ header }) => flexRenderComponent(header.FooterSum),
cell: ({ cell }) => flexRenderComponent(cell.NumberCell),
}),
])The registered components are available through the enhanced cell and header objects because the column helper is bound to the createTableHook configuration.
Create each table with injectAppTable. Per-table options provide the data and columns; shared features and row models come from src/app/table.ts.
table = injectAppTable(() => ({
key: 'users-table',
columns: this.columns,
data: this.data(),
debugTable: true,
}))table = injectAppTable(() => ({
key: 'users-table',
columns: this.columns,
data: this.data(),
debugTable: true,
}))The Angular table instance is augmented with:
Registered table components can access the table through Angular DI:
export class PaginationControls {
readonly table = injectTableContext()
}export class PaginationControls {
readonly table = injectTableContext()
}In templates, use the Angular rendering helpers with the app wrappers:
@for (_header of headerGroup.headers; track _header.id) {
@let header = table.appHeader(_header);
<th (click)="header.column.getToggleSortingHandler()?.($event)">
<ng-container *flexRenderHeader="header; let value">
{{ value }}
</ng-container>
<ng-container
*flexRender="header.SortIndicator; props: header.getContext(); let value"
>
{{ value }}
</ng-container>
</th>
}@for (_header of headerGroup.headers; track _header.id) {
@let header = table.appHeader(_header);
<th (click)="header.column.getToggleSortingHandler()?.($event)">
<ng-container *flexRenderHeader="header; let value">
{{ value }}
</ng-container>
<ng-container
*flexRender="header.SortIndicator; props: header.getContext(); let value"
>
{{ value }}
</ng-container>
</th>
}The example has separate Users and Products table components. Both import createAppColumnHelper and injectAppTable from src/app/table.ts, so they share sorting, filtering, pagination, row IDs, toolbar controls, cell renderers, and header/footer renderers while keeping their own data and columns.
If different product areas need incompatible defaults, create another createTableHook setup file and export a second set of app helpers from there.