Docs
Cloudflare
Railway
CodeRabbit
Clerk
Netlify
OpenRouter
AG Grid
SerpAPI
WorkOS
Unkey
Prisma
Sentry
Electric
Cloudflare
Railway
CodeRabbit
Clerk
Netlify
OpenRouter
AG Grid
SerpAPI
WorkOS
Unkey
Prisma
Sentry
Electric
Table API Reference
Column API Reference
Row API Reference
Cell API Reference
Header API Reference
Features API Reference
Static Functions API Reference
Core Guides

Composable Tables Guide

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.

Examples

  • Composable Tables - Two tables sharing one app table setup from src/app/table.ts.
  • Basic App Table - Minimal createTableHook usage without the larger component registry.

Setup

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.

ts
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.

Returned Helpers

HelperPurpose
injectAppTableCreates a table with the app's shared features (including row model factories), defaults, and registered components already attached.
createAppColumnHelperCreates column helpers where cell, header, and footer contexts know about the registered components.
injectTableContextReads the current table inside registered table components like PaginationControls.
injectTableCellContextReads the current cell inside registered cell components like TextCell.
injectTableHeaderContextReads the current header/footer inside registered header components like SortIndicator.

Columns

Use createAppColumnHelper<TData>() instead of the base column helper when column definitions should render registered components.

ts
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.

Table Rendering

Create each table with injectAppTable. Per-table options provide the data and columns; shared features and row models come from src/app/table.ts.

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:

  • table.PaginationControls, table.RowCount, and table.TableToolbar
  • table.appCell(cell) for enhanced cell component types in templates
  • table.appHeader(header) for enhanced header component types in templates
  • table.appFooter(footer) for enhanced footer component types in templates

Registered table components can access the table through Angular DI:

ts
export class PaginationControls {
  readonly table = injectTableContext()
}
export class PaginationControls {
  readonly table = injectTableContext()
}

In templates, use the Angular rendering helpers with the app wrappers:

html
@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>
}

Reusing The Hook

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.