Custom Cell Templates
Four template slots per column — the cell, the header, a band under the header and a band under the last row. Each one is an ng-template of yours, handed a typed context, and the grid puts it where the default content would have gone.
cellTemplate — the whole row, not just the value
Three columns rendered by templates, each taking something different from the context: the row for the avatar, the value for the badge, and the row's position for the rank.
A cellTemplate replaces a cell's content and is handed a
CellContext<T>. Three columns here take three different things out of it:
- Person —
let-row.$implicitis the row, so this is the one binding that needs no name. The cell shows the name and the email, which the column has nopropfor: a template is not limited to its own column's value. - Status —
let-value="value". The resolved value, aftervalueGetteror theproppath. Use it when one value is all you need. - # —
let-rowIndex="rowIndex", on a column with nopropat all. There is nothing to resolve, so the template is the entire content of the column.
Bind by name. Only the row is $implicit. let-value
without the ="value" binds the row, and the template renders the object rather than
the value — with no error to tell you so.
Salary has no template and is formatted anyway, through
valueFormatter. Reach for a template when you need markup; reach for a formatter
when you need text. A formatter also keeps sorting and export on the raw number, which a
template does not — see Value Getters & Formatters.
headerTemplate, subHeaderTemplate and footerTemplate — the three around the cells
The same Salary column carries all three: a header that adds a unit, a band under the header holding the range, and a band under the last row holding the average. Both bands stay put while the rows scroll.
Three more slots sit around the cells, and one column can carry all of them:
headerTemplate— replaces the header label. Its context is{ column, sortDir }and has no implicit value at all, so it islet-column="column", neverlet-column. Written the short way the header renders blank and nothing throws.subHeaderTemplate— a band between the header and the first row. The range lives there here.footerTemplate— a band under the last row. The average lives there.
One column turns the band on for the whole grid. Declaring a
subHeaderTemplate or a footerTemplate anywhere adds that row to the
grid; every other column simply renders an empty cell in it. Department carries a footer too, so
the bottom band holds two entries and the reader can see they are per column rather than one
spanning strip.
Both bands stay put while the rows scroll, in the same way the header does — which is what makes them worth using for a total rather than a row at the end of the data.
The numbers are computed over the whole data set, not over the page on screen — the average under the grid is $111,500, while the eight rows in front of you average $113,500. That is the point of these two slots: a summary the grid cannot work out for you, in a place the reader does not have to scroll to. Where the number comes from is your decision, and a server-side total belongs here just as naturally as a client-side one.
The four slots
Every one is a TemplateRef you declare in your own component and hand to a column.
The grid renders it where the default content would have gone, with a typed context:
cellTemplate— the cell, on every data row.CellContext<T>.headerTemplate— the header label.{ column, sortDir }.subHeaderTemplate— a band between the header and the first row.SummaryContext<T>.footerTemplate— a band under the last row. Same context.
Read the context, never guess the implicit
$implicit is a different thing in every one of them, and getting it wrong throws
nothing:
cellTemplate—$implicitis the row, solet-rowworks.valueandrowIndexare named.headerTemplate— no implicit value.let-columnbindsundefinedand the header renders empty.subHeaderTemplate/footerTemplate—column, named.
The rule is one line: bind everything but a cell's row by name.
Getting the TemplateRef into the column
A template ref does not exist until the view has been created, so a column array built in the
constructor gets undefined. Use a viewChild() signal read inside a
computed():
private readonly nameTpl = viewChild<TemplateRef<CellContext<Employee>>>('nameTpl');
readonly columns = computed((): GridColumn<Employee>[] => [
{ key: 'firstName', name: 'Person', prop: 'firstName', cellTemplate: this.nameTpl() },
…
]);
Do not build the array in ngAfterViewInit. It works in the
browser and breaks under server-side rendering: the hook never runs on the server, so the
server sends No columns to show and the real grid appears only after the page
hydrates. With a viewChild() signal the query is simply undefined on
the server, so the columns render normally there and only the templated part waits for the
client.
A template or a formatter?
A cellTemplate replaces the cell with your markup — and with it goes the plain text
the grid would have had. Sorting and filtering still run on the underlying value, but
export never runs a template: a column whose content exists only in markup
writes its raw value to the file, and a column with no prop is not in the file at
all.
So use valueFormatter when what you want is text, and a template when what you want
is markup. Salary in the first example is formatted rather than templated for exactly that
reason. See Value Getters & Formatters.
Template slots
| Property | Type | Default | Description |
|---|---|---|---|
| column.cellTemplate | TemplateRef<CellContext<T>> | — | Replaces the cell content on every data row. let-row (implicit), let-value="value", let-rowIndex="rowIndex". |
| column.headerTemplate | TemplateRef<HeaderContext<T>> | — | Replaces the header label. Context is { column, sortDir } with no implicit value — bind both by name. |
| column.subHeaderTemplate | TemplateRef<SummaryContext<T>> | — | A band between the header and the first row. Declaring it on any one column adds the band to the whole grid. |
| column.footerTemplate | TemplateRef<SummaryContext<T>> | — | A band under the last row — totals and averages. Same context, same whole-grid rule. |
| column.valueFormatter | (value, row) => string | — | Text rather than markup, and the option to reach for first: it keeps sorting and export on the raw value. See Value Getters & Formatters. |
| column.valueGetter | (row) => unknown | — | Resolves the value a cell template receives — and the value the grid sorts, filters and exports. |
| column.headerTooltipTemplate | TemplateRef<{ column, sortDir }> | — | Rich content for a header tooltip, with the same context shape as headerTemplate. See Tooltips. |
| column.tooltipTemplate | TemplateRef<CellContext<T>> | — | Rich content for a cell tooltip; the row is $implicit here as well. See Tooltips. |