|
| 1 | +--- |
| 2 | +title: Composable Tables Guide |
| 3 | +--- |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +Use this pattern when multiple tables in an Angular app share behavior or rendering conventions. For a single isolated table, `injectTable` is usually enough. |
| 8 | + |
| 9 | +## Examples |
| 10 | + |
| 11 | +- [Composable Tables](../examples/composable-tables) - Two tables sharing one app table setup from `src/app/table.ts`. |
| 12 | +- [Basic App Table](../examples/basic-app-table) - Minimal `createTableHook` usage without the larger component registry. |
| 13 | + |
| 14 | +## Setup |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +```ts |
| 19 | +import { |
| 20 | + columnFilteringFeature, |
| 21 | + createFilteredRowModel, |
| 22 | + createPaginatedRowModel, |
| 23 | + createSortedRowModel, |
| 24 | + createTableHook, |
| 25 | + filterFns, |
| 26 | + rowPaginationFeature, |
| 27 | + rowSortingFeature, |
| 28 | + sortFns, |
| 29 | + tableFeatures, |
| 30 | +} from '@tanstack/angular-table' |
| 31 | + |
| 32 | +import { |
| 33 | + PaginationControls, |
| 34 | + RowCount, |
| 35 | + TableToolbar, |
| 36 | +} from './components/table-components' |
| 37 | +import { |
| 38 | + CategoryCell, |
| 39 | + NumberCell, |
| 40 | + PriceCell, |
| 41 | + ProgressCell, |
| 42 | + RowActionsCell, |
| 43 | + StatusCell, |
| 44 | + TextCell, |
| 45 | +} from './components/cell-components' |
| 46 | +import { |
| 47 | + ColumnFilter, |
| 48 | + FooterColumnId, |
| 49 | + FooterSum, |
| 50 | + SortIndicator, |
| 51 | +} from './components/header-components' |
| 52 | + |
| 53 | +export const { |
| 54 | + createAppColumnHelper, |
| 55 | + injectAppTable, |
| 56 | + injectTableContext, |
| 57 | + injectTableCellContext, |
| 58 | + injectTableHeaderContext, |
| 59 | +} = createTableHook({ |
| 60 | + features: tableFeatures({ |
| 61 | + columnFilteringFeature, |
| 62 | + rowPaginationFeature, |
| 63 | + rowSortingFeature, |
| 64 | + }), |
| 65 | + rowModels: { |
| 66 | + sortedRowModel: createSortedRowModel(sortFns), |
| 67 | + filteredRowModel: createFilteredRowModel(filterFns), |
| 68 | + paginatedRowModel: createPaginatedRowModel(), |
| 69 | + }, |
| 70 | + getRowId: (row) => row.id, |
| 71 | + tableComponents: { |
| 72 | + PaginationControls, |
| 73 | + RowCount, |
| 74 | + TableToolbar, |
| 75 | + }, |
| 76 | + cellComponents: { |
| 77 | + TextCell, |
| 78 | + NumberCell, |
| 79 | + ProgressCell, |
| 80 | + StatusCell, |
| 81 | + CategoryCell, |
| 82 | + PriceCell, |
| 83 | + RowActionsCell, |
| 84 | + }, |
| 85 | + headerComponents: { |
| 86 | + SortIndicator, |
| 87 | + ColumnFilter, |
| 88 | + FooterColumnId, |
| 89 | + FooterSum, |
| 90 | + }, |
| 91 | +}) |
| 92 | +``` |
| 93 | + |
| 94 | +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. |
| 95 | + |
| 96 | +## Returned Helpers |
| 97 | + |
| 98 | +| Helper | Purpose | |
| 99 | +|---|---| |
| 100 | +| `injectAppTable` | Creates a table with the app's shared `features`, `rowModels`, defaults, and registered components already attached. | |
| 101 | +| `createAppColumnHelper` | Creates column helpers where `cell`, `header`, and `footer` contexts know about the registered components. | |
| 102 | +| `injectTableContext` | Reads the current table inside registered table components like `PaginationControls`. | |
| 103 | +| `injectTableCellContext` | Reads the current cell inside registered cell components like `TextCell`. | |
| 104 | +| `injectTableHeaderContext` | Reads the current header/footer inside registered header components like `SortIndicator`. | |
| 105 | + |
| 106 | +## Columns |
| 107 | + |
| 108 | +Use `createAppColumnHelper<TData>()` instead of the base column helper when column definitions should render registered components. |
| 109 | + |
| 110 | +```ts |
| 111 | +import { flexRenderComponent } from '@tanstack/angular-table' |
| 112 | +import { createAppColumnHelper } from '../../table' |
| 113 | +import type { Person } from '../../makeData' |
| 114 | + |
| 115 | +const personColumnHelper = createAppColumnHelper<Person>() |
| 116 | + |
| 117 | +readonly columns = personColumnHelper.columns([ |
| 118 | + personColumnHelper.accessor('firstName', { |
| 119 | + header: 'First Name', |
| 120 | + footer: ({ header }) => flexRenderComponent(header.FooterColumnId), |
| 121 | + cell: ({ cell }) => flexRenderComponent(cell.TextCell), |
| 122 | + }), |
| 123 | + personColumnHelper.accessor('age', { |
| 124 | + header: 'Age', |
| 125 | + footer: ({ header }) => flexRenderComponent(header.FooterSum), |
| 126 | + cell: ({ cell }) => flexRenderComponent(cell.NumberCell), |
| 127 | + }), |
| 128 | +]) |
| 129 | +``` |
| 130 | + |
| 131 | +The registered components are available through the enhanced `cell` and `header` objects because the column helper is bound to the `createTableHook` configuration. |
| 132 | + |
| 133 | +## Table Rendering |
| 134 | + |
| 135 | +Create each table with `injectAppTable`. Per-table options provide the data and columns; shared features and row models come from `src/app/table.ts`. |
| 136 | + |
| 137 | +```ts |
| 138 | +table = injectAppTable(() => ({ |
| 139 | + key: 'users-table', |
| 140 | + columns: this.columns, |
| 141 | + data: this.data(), |
| 142 | + debugTable: true, |
| 143 | +})) |
| 144 | +``` |
| 145 | + |
| 146 | +The Angular table instance is augmented with: |
| 147 | + |
| 148 | +- `table.PaginationControls`, `table.RowCount`, and `table.TableToolbar` |
| 149 | +- `table.appCell(cell)` for enhanced cell component types in templates |
| 150 | +- `table.appHeader(header)` for enhanced header component types in templates |
| 151 | +- `table.appFooter(footer)` for enhanced footer component types in templates |
| 152 | + |
| 153 | +Registered table components can access the table through Angular DI: |
| 154 | + |
| 155 | +```ts |
| 156 | +export class PaginationControls { |
| 157 | + readonly table = injectTableContext() |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +In templates, use the Angular rendering helpers with the app wrappers: |
| 162 | + |
| 163 | +```html |
| 164 | +@for (_header of headerGroup.headers; track _header.id) { |
| 165 | + @let header = table.appHeader(_header); |
| 166 | + |
| 167 | + <th (click)="header.column.getToggleSortingHandler()?.($event)"> |
| 168 | + <ng-container *flexRenderHeader="header; let value"> |
| 169 | + {{ value }} |
| 170 | + </ng-container> |
| 171 | + <ng-container |
| 172 | + *flexRender="header.SortIndicator; props: header.getContext(); let value" |
| 173 | + > |
| 174 | + {{ value }} |
| 175 | + </ng-container> |
| 176 | + </th> |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +## Reusing The Hook |
| 181 | + |
| 182 | +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. |
| 183 | + |
| 184 | +If different product areas need incompatible defaults, create another `createTableHook` setup file and export a second set of app helpers from there. |
0 commit comments