GUILD OF GLEKS UIv21.4.4

gog-table

Table

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.

typescript
import { GogColumn, TableComponent } from '@guildofgleks/ui';

@Component({
  // ...
  imports: [TableComponent, GogColumn],
})

Basic usage — sortable columns cycle asc → desc → unsorted on click:

# Component Status Owner Updated
1 Buttons Ready Design Today
2 Checkbox Ready Forms Yesterday
3 Table In review Data 2 days ago
4 Accordion Planned Navigation This week
5 Spinner Ready Feedback This month
6 Toast Ready Feedback This month
<gog-table [value]="rows">
  <gog-column field="component" header="Component" [sortable]="true"></gog-column>
  <gog-column field="status" header="Status" [sortable]="true"></gog-column>
  <gog-column field="owner" header="Owner"></gog-column>
  <gog-column field="updated" header="Updated"></gog-column>
</gog-table>

Examples

Custom cell & header markup

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-column field="component" header="Component" [sortable]="true"></gog-column>

  <gog-column field="status" header="Status">
    <ng-template gogColumnHeader let-header>
      <span class="status-header">{{ header }}</span>
    </ng-template>
    <ng-template gogColumnBody let-row let-value="value">
      <gog-tag [variant]="statusVariant(row.status)" size="sm">{{ value }}</gog-tag>
    </ng-template>
  </gog-column>

  <gog-column field="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">
  <column field="status" header="Status"></column>
  <ng-template template="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-column field="status" header="Status">
    <ng-template gogColumnBody let-row let-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).

# Component Status Owner
1 Buttons Ready Design
2 Checkbox Ready Forms
3 Table In review Data
<gog-table [value]="rows" [pageSize]="3" [showTotal]="true" totalPosition="left">
  <gog-column field="component" header="Component" [sortable]="true"></gog-column>
  <gog-column field="status" header="Status" [sortable]="true"></gog-column>
  <gog-column field="owner" header="Owner"></gog-column>
</gog-table>

Sticky header

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.

# Component Status Owner
1 Buttons Ready Design
2 Checkbox Ready Forms
3 Table In review Data
4 Accordion Planned Navigation
5 Spinner Ready Feedback
6 Toast Ready Feedback
<gog-scroll style="height: 260px;" ariaLabel="Table rows">
  <gog-table [value]="rows" [stickyHeader]="true">
    <gog-column field="component" header="Component"></gog-column>
    <gog-column field="status" header="Status"></gog-column>
    <gog-column field="owner" header="Owner"></gog-column>
  </gog-table>
</gog-scroll>

Missing values

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.

Default placeholder

Component Owner
Buttons Design
Checkbox -
Table -

emptyPlaceholder="N/A"

Component Owner
Buttons Design
Checkbox N/A
Table N/A
<gog-table [value]="sparseRows" [showRowNumbers]="false" size="sm">
  <gog-column field="component" header="Component"></gog-column>
  <gog-column field="owner" header="Owner"></gog-column>
</gog-table>

<gog-table [value]="sparseRows" [showRowNumbers]="false" emptyPlaceholder="N/A" size="sm">
  <gog-column field="component" header="Component"></gog-column>
  <gog-column field="owner" header="Owner"></gog-column>
</gog-table>

Full width

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.

Component Status
Buttons Ready
Checkbox Ready
Table In review
Accordion Planned
Spinner Ready
Toast Ready
<gog-table [value]="rows" [showRowNumbers]="false" [fullWidth]="false" size="sm">
  <gog-column field="component" header="Component" width="115px"></gog-column>
  <gog-column field="status" header="Status" width="80px"></gog-column>
</gog-table>

Loading state

Shows a spinner in place of rows while loading is true.

# Component Status Owner
1 Buttons Ready Design
2 Checkbox Ready Forms
3 Table In review Data
<gog-table [value]="rows" [loading]="loading()">...</gog-table>

Empty state

An empty value array renders a built-in placeholder row.

# Component Owner
1 Buttons Design
2 Checkbox Forms
3 Table Data
4 Accordion Navigation
5 Spinner Feedback
6 Toast Feedback
<gog-table [value]="showEmpty() ? [] : rows">...</gog-table>

Reacting to the user 21.4.0

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:

  • No events yet.
<gog-table
  [value]="rows"
  [pageSize]="3"
  [interactiveRows]="true"
  (gogSortChange)="onSortChange($event)"
  (gogPageChange)="onPageChange($event)"
  (gogRowClick)="onRowClick($event)"
>
  <gog-column field="component" header="Component" [sortable]="true"></gog-column>
  <gog-column field="status" header="Status" [sortable]="true"></gog-column>
</gog-table>

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.

# Component Status Owner
1 Buttons Ready Design
2 Checkbox Ready Forms
3 Table In review Data
4 Accordion Planned Navigation
5 Spinner Ready Feedback
6 Toast Ready Feedback
<gog-table
  [value]="rows"
  selectionMode="multiple"
  [(selection)]="selection"
  dataKey="component"
>
  <gog-column field="component" header="Component"></gog-column>
  <gog-column field="status" header="Status"></gog-column>
  <gog-column field="owner" header="Owner"></gog-column>
</gog-table>

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.

# Component Owner
1 Buttons Design
2 Checkbox Forms
<gog-table
  [value]="rows"
  [(pageSize)]="rowsPerPage"
  [showPageSizeSelect]="true"
  [pageSizeOptions]="[2, 3, 6]"
>
  <gog-column field="component" header="Component"></gog-column>
  <gog-column field="owner" header="Owner"></gog-column>
</gog-table>

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.

# Name Team Score
<gog-table
  [value]="serverRows()"
  [lazy]="true"
  [totalRecords]="serverTotal()"
  [(pageSize)]="serverPageSize"
  [loading]="serverLoading()"
  [showTotal]="true"
  [showPageSizeSelect]="true"
  [pageSizeOptions]="[10, 20, 50]"
  dataKey="id"
  (gogSortChange)="onServerSort($event)"
  (gogPageChange)="onServerPage($event)"
  (pageSizeChange)="onServerPageSize($event)"
>
  <gog-column field="name" header="Name" [sortable]="true"></gog-column>
  <gog-column field="team" header="Team" [sortable]="true"></gog-column>
  <gog-column field="score" header="Score" [sortable]="true"></gog-column>
</gog-table>

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

NameTypeDefaultDescription
valueT[][]The row data array. In lazy mode this is the current page, already sorted.
fullWidthbooleantrueFills its container by default. Set false to shrink to fit its columns instead.
pageSizemodel<number>0Rows 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.0boolean | undefinedfalseShows the paginator's rows-per-page select. Also settable app-wide via GOG_CONFIG.paginator.
pageSizeOptions21.4.0number[] | undefined[10, 20, 30, 40, 50]The choices that select offers. Also settable app-wide via GOG_CONFIG.paginator.
lazy21.4.0booleanfalseHands sorting and paging to you: value is rendered exactly as given and treated as the current page. Needs totalRecords.
totalRecords21.4.0number | nullnullHow 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.0model<T[]>[]Two-way bindable selected rows — always an array, including in 'single' mode where it holds zero or one row.
dataKey21.4.0string''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.0booleantrueThe checkbox column that appears once selection is on. Turn it off for a table that selects by row click.
interactiveRows21.4.0booleanfalseMakes rows focusable and styled as clickable, so Enter/Space activate the focused row. Without it gogRowClick is a mouse-only affordance.
showRowNumbersbooleantrueShows a leading row-number column.
showTotalbooleanfalseShows a row-count label.
emptyPlaceholderstring'-'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.
loadingbooleanfalseShows a spinner in place of rows.
showColumnBordersbooleanfalseVertical borders between columns.
stickyHeaderbooleanfalseSticks 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

NamePayloadDescription
gogSortChange21.4.0GogTableSortEvent{ field, direction } — including the third click that clears the sort, which arrives as { field: '', direction: null }.
gogPageChange21.4.0numberThe 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.0GogTableRowClickEvent<T>{ row, index, originalEvent }. index is the position within the rendered page, not the whole data set.
pageSizeChange21.4.0numberThe 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.0T[]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>.

NameTypeDefaultDescription
fieldstringrequiredField name, or a dot-path into a nested property (e.g. "address.city").
headerstring''Header text.
sortablebooleanfalseEnables click-to-sort on the header: asc → desc → unsorted.
widthstring''Fixed width, e.g. "120px" or "20%".
minWidthstring''Minimum width, e.g. "80px".
maxWidthstring''Maximum width, e.g. "300px".
comparator((a: unknown, b: unknown) => number) | nullnullCustom 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.

DirectiveContextDescription
gogColumnBody$implicit / row (the row object), index, valueCustom 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), fieldCustom 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.

NameTypeDefaultDescription
templatestringrequiredThe 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.

TokenDescription
--gog-table-border-color / -border-widthOuter border.
--gog-table-surface / -text-color / -accent-colorSurface, body text and header accent.
--gog-table-hover-bgRow hover background.
--gog-table-muted-colorSecondary text (e.g. empty state).
--gog-table-header-letter-spacing / -text-transformHeader cell typography.
--gog-table-{size}-padding-v / -th-font-size / -td-font-sizeRow density, per size step (xsm/sm/md/lg/slg).