A sortable, paginated data table with custom cell/header templates, size variants, sticky headers, loading and empty states, and built-in row numbers. Since 21.4.0 it also reports what the user did (gogSortChange, gogPageChange, gogRowClick), hands sorting and paging to a server (lazy), and selects rows — so it is no longer a display-only grid.
Overview
Import TableComponent and GogColumn, then declare one <gog-column> per field as content — gog-table reads them via contentChildren, so the columns live in your template, not a config array.
A column can swap its default text rendering for a gogColumnBody or gogColumnHeader template declared inside that column — here the status column renders a gog-tag instead of raw text, and decorates its header too. The body context carries the row, its index, and the already-resolved cell value.
Component
Status
Owner
Buttons
Ready
Design
Checkbox
Ready
Forms
Table
In review
Data
Accordion
Planned
Navigation
Spinner
Ready
Feedback
Toast
Ready
Feedback
<gog-table [value]="rows" [showRowNumbers]="false"><gog-columnfield="component"header="Component" [sortable]="true"></gog-column><gog-columnfield="status"header="Status"><ng-templategogColumnHeaderlet-header><spanclass="status-header">{{ header }}</span></ng-template><ng-templategogColumnBodylet-rowlet-value="value"><gog-tag [variant]="statusVariant(row.status)"size="sm">{{ value }}</gog-tag></ng-template></gog-column><gog-columnfield="owner"header="Owner"></gog-column></gog-table>
This replaces the string-keyed <ng-template template="field" type="body|header">, deprecated in 21.3.0 and removed in 21.5.0. The old form matched columns by a string the compiler cannot check, so a typo silently fell back to the default cell.
html
<!-- 21.2.x — the template matched its column by a string the compiler cannot check, --><!-- so a typo silently fell back to the default cell. --><gog-table [value]="rows"><columnfield="status"header="Status"></column><ng-templatetemplate="status"type="body"let-row>…</ng-template></gog-table><!-- 21.3.0 — the template lives inside the column it belongs to. --><gog-table [value]="rows"><gog-columnfield="status"header="Status"><ng-templategogColumnBodylet-rowlet-value="value">…</ng-template></gog-column></gog-table>
Pagination & total count
pageSize turns on pagination (0 disables it), rendered with the same gog-paginator documented separately. showTotal adds a row-count label; totalPosition places it left, right, or opposite the paginator (the default).
stickyHeader puts position: sticky on the header cells so the header row holds at the top of the scrolling region while the rows move under it. The region here is a gog-scroll capped at 260px — a native overflow: auto would bring back the one piece of chrome no --gog-* token can reach. Known defect in 21.4.4: the header scrolls away instead of holding. A sticky element resolves against its nearest scrolling ancestor. The table wraps its own markup in a horizontal gog-scroll, and once that inner scroller activates — which is exactly what putting a wide table in a narrow region does — its viewport becomes the nearest one and wins over the region you placed the table in. It never scrolls vertically itself, so the header simply rides up out of view. Capping the height of the gog-table host, or of the inner scroller, does not help; the fix has to happen inside the component.
A cell whose field is null or undefined falls back to emptyPlaceholder — "-" by default, overridable per table. Both tables below hold the same rows; only the placeholder differs.
Full width of its container by default (see every table above). [fullWidth]="false" shrinks the table to fit its columns instead — useful for a narrow, two-column table that shouldn't stretch to fill a wide page. Give those columns a width when you do: the table is table-layout: fixed, so it divides whatever width it has evenly between columns rather than measuring the text, and a column left to chance can end up narrower than its own header.
The table had no outputs at all before 21.4.0, which is what made it display-only. It now reports the three things a consumer needs to know about: a new sort, a new page, and a click on a row.
#
Component
Status
1
Buttons
Ready
2
Checkbox
Ready
3
Table
In review
Sort a column, page through, click a row — newest event first:
gogPageChange is deliberately quiet twice. It does not fire on the first render, and it does not fire for the reset to page 1 that a new sort causes — that reset belongs to the sort. Without both rules a consumer refetching from each event would issue two requests for one user action.
gogRowClick alone is a mouse-only affordance: a <tr> is not focusable. interactiveRows makes rows focusable and styles them as clickable, so Enter and Space activate the focused one. If the action really is "open this one thing", a link or button inside a cell beats a whole-row target.
Selection 21.4.0
selectionMode turns it on; [(selection)] is always a T[], including in 'single' mode where it holds zero or one row. One shape to read beats a T | T[] | null union to narrow on every access.
Two rules worth stating outright, because both are silent when you get them wrong:
Set dataKey. Without it rows are matched by object identity, so any refetch that produces new objects drops the selection with nothing to show for it. It is also the @for track key, which is what lets the rendered DOM survive a refetch instead of being rebuilt.
Select-all covers the current page, not the whole data set. In lazy mode the table has never seen the other pages, and a control that meant different things in the two modes would be worse than either behaviour on its own.
The checkbox column appears on its own — showSelectionColumn turns it off, for a table that selects by row click instead (pair that with interactiveRows). The header select-all renders only in 'multiple' mode.
Rows per page 21.4.0
pageSize is a model, not a plain input: [pageSize]="20" works exactly as before, and [(pageSize)]="size" became possible. That is the whole reason the rows-per-page select needs no wiring — the table binds its own model straight to the paginator's, and the select writes back through it with no intermediate signal to keep in sync in either direction.
The select is off by default, per table or app-wide through GOG_CONFIG.paginator — see Global Configuration. Changing the size returns to page 1 and does not emit gogPageChange: you already know from pageSizeChange, and firing both would make a lazy table fetch twice. The footer stays visible at a single page whenever the select is on, so the user is never stranded on a size with no control left to change it.
Server-driven tables — lazy21.4.0
By default the table owns the whole data set: it sorts value and slices the page itself. With [lazy]="true" it does neither — valueis the current page, already sorted, rendered exactly as handed over. Give it totalRecords so it knows how many pages exist (without it, pagination stays hidden and it warns in dev mode), then refetch from the outputs.
137 rows that only ever leave the "server" one page at a time, with a 350 ms delay so the loading state is visible. Sorting and slicing happen in the fake endpoint — which is the point: the table must not re-order or re-slice what it is given.
Row numbers still count from the current page ((page - 1) * pageSize + i + 1), and showTotal reports totalRecords rather than value.length. Do not sort or slice value yourself as well — that is exactly what the flag turns off.
In lazy mode pageSizeChange is the refetch signal for a new page size, so bind [pageSize] + (pageSizeChange) rather than the banana-box when you need to act on it.
API Reference
gog-table — Inputs
Name
Type
Default
Description
value
T[]
[]
The row data array. In lazy mode this is the current page, already sorted.
fullWidth
boolean
true
Fills its container by default. Set false to shrink to fit its columns instead.
pageSize
model<number>
0
Rows per page. 0 disables pagination. A model since 21.4.0, so [(pageSize)] binds two-way — which is what lets the rows-per-page select write back with no wiring in between.
showPageSizeSelect21.4.0
boolean | undefined
false
Shows the paginator's rows-per-page select. Also settable app-wide via GOG_CONFIG.paginator.
pageSizeOptions21.4.0
number[] | undefined
[10, 20, 30, 40, 50]
The choices that select offers. Also settable app-wide via GOG_CONFIG.paginator.
lazy21.4.0
boolean
false
Hands sorting and paging to you: value is rendered exactly as given and treated as the current page. Needs totalRecords.
totalRecords21.4.0
number | null
null
How many rows exist in total, for lazy mode. Without it pagination stays hidden and the table warns in dev. showTotal reports this rather than value.length.
selectionMode21.4.0
'none' | 'single' | 'multiple'
'none'
Turns row selection on, and whether more than one row can be held at a time.
selection21.4.0
model<T[]>
[]
Two-way bindable selected rows — always an array, including in 'single' mode where it holds zero or one row.
dataKey21.4.0
string
''
The field (or dot-path) identifying a row. Selection matches on it instead of object identity, and it becomes the @for track key. Set it whenever the data can be refetched.
showSelectionColumn21.4.0
boolean
true
The checkbox column that appears once selection is on. Turn it off for a table that selects by row click.
interactiveRows21.4.0
boolean
false
Makes rows focusable and styled as clickable, so Enter/Space activate the focused row. Without it gogRowClick is a mouse-only affordance.
showRowNumbers
boolean
true
Shows a leading row-number column.
showTotal
boolean
false
Shows a row-count label.
emptyPlaceholder
string
'-'
Fallback text for a cell whose field is null or undefined.
paginatorPosition
'left' | 'center' | 'right'
'center'
Alignment of the pagination controls.
totalPosition
'left' | 'right' | 'opposite'
'opposite'
Alignment of the total-count label (only with showTotal). 'opposite' picks whichever side paginatorPosition isn't on.
loading
boolean
false
Shows a spinner in place of rows.
showColumnBorders
boolean
false
Vertical borders between columns.
stickyHeader
boolean
false
Sticks the header row to the top of the nearest scrolling ancestor.
size
'xsm' | 'sm' | 'md' | 'lg' | 'slg'
'lg'
Row density — cell padding and font size scale with it.
gog-table — Outputs
Name
Payload
Description
gogSortChange21.4.0
GogTableSortEvent
{ field, direction } — including the third click that clears the sort, which arrives as { field: '', direction: null }.
gogPageChange21.4.0
number
The new 1-based page. Deliberately silent in two cases: the first render, and the reset to page 1 that a new sort causes.
gogRowClick21.4.0
GogTableRowClickEvent<T>
{ row, index, originalEvent }. index is the position within the rendered page, not the whole data set.
pageSizeChange21.4.0
number
The model's own change event. In lazy mode this is the refetch signal for a new page size — it does not also emit gogPageChange.
selectionChange21.4.0
T[]
The selection model's change event, for when you don't want the banana-box.
<gog-column> — Inputs
One per field, declared as content inside <gog-table>.
Name
Type
Default
Description
field
string
required
Field name, or a dot-path into a nested property (e.g. "address.city").
header
string
''
Header text.
sortable
boolean
false
Enables click-to-sort on the header: asc → desc → unsorted.
width
string
''
Fixed width, e.g. "120px" or "20%".
minWidth
string
''
Minimum width, e.g. "80px".
maxWidth
string
''
Maximum width, e.g. "300px".
comparator
((a: unknown, b: unknown) => number) | null
null
Custom sort comparator for this column. Defaults to a locale-aware string compare, </> otherwise.
<gog-column> — Content slots
Declared inside the column they belong to — see "Custom cell & header markup" above.
Directive
Context
Description
gogColumnBody
$implicit / row (the row object), index, value
Custom cell markup for this column. value is the already-resolved cell value for the column's field, so a custom cell can decorate it rather than re-derive it. index is the position within the rendered page, not the whole data set.
gogColumnHeader
$implicit (the column's own header text), field
Custom header markup for this column. The header text is handed in so a custom header can decorate it rather than restate it.
Deprecated in 21.3.0
Both of these keep working until they are removed in 21.5.0, so a codebase can migrate one table at a time.
<column> → <gog-column>
<column> was the library's last unprefixed element name. The directive now matches gog-column, column, and the Column export is aliased to GogColumn — so the rename is a find-and-replace with no behaviour change.
<ng-template template="field"> — Inputs
Replaced by the gogColumnBody / gogColumnHeader slots above.
Name
Type
Default
Description
template
string
required
The column field this template rendered for — a string the compiler cannot check, so a typo silently fell back to the default cell. That is why it was replaced.
type
'body' | 'header'
'body'
'body' got let-row (the row object) and let-index (its position). 'header' got no context.
Styling Tokens
Every CSS custom property the table paints with. Override any of them — on a single instance, a subtree, or a theme — to restyle it. See the Theming guide for the full token-layering model, or the Theme Generator to tweak these live.