Home/Cells & Formatting/Custom Cell Templates
Cells & Formatting

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.

#
Person
Department
Salary
Status
1
NNadia Haddadnadia.haddad@example.com
Engineering
$68,000
On leave
2
BBen Okaforben.okafor@example.com
Design
$105,000
Active
3
YYara Nasseryara.nasser@example.com
Data
$142,000
Active
4
KKarim Faroukkarim.farouk@example.com
Support
$83,000
Active
5
EElena Novakelena.novak@example.com
Security
$120,000
On leave
6
WWalid Bashawalid.basha@example.com
Finance
$157,000
Active
7
AAlice Smithalice.smith@example.com
People
$98,000
Active
8
PPetra Langepetra.lange@example.com
Product
$135,000
Active
1–8 of 16

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. $implicit is the row, so this is the one binding that needs no name. The cell shows the name and the email, which the column has no prop for: a template is not limited to its own column's value.
  • Status — let-value="value". The resolved value, after valueGetter or the prop path. Use it when one value is all you need.
  • # — let-rowIndex="rowIndex", on a column with no prop at 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.

Name
Department8
SalaryUSD
Status
$68,000 – $157,000
Nadia
Engineering
$68,000
On leave
Ben
Design
$105,000
Active
Yara
Data
$142,000
Active
Karim
Support
$83,000
Active
Elena
Security
$120,000
On leave
Walid
Finance
$157,000
Active
Alice
People
$98,000
Active
Petra
Product
$135,000
Active
1–8 of 16

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 is let-column="column", never let-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 — $implicit is the row, so let-row works. value and rowIndex are named.
  • headerTemplate — no implicit value. let-column binds undefined and 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

PropertyTypeDefaultDescription
column.cellTemplateTemplateRef<CellContext<T>>—Replaces the cell content on every data row. let-row (implicit), let-value="value", let-rowIndex="rowIndex".
column.headerTemplateTemplateRef<HeaderContext<T>>—Replaces the header label. Context is { column, sortDir } with no implicit value — bind both by name.
column.subHeaderTemplateTemplateRef<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.footerTemplateTemplateRef<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.headerTooltipTemplateTemplateRef<{ column, sortDir }>—Rich content for a header tooltip, with the same context shape as headerTemplate. See Tooltips.
column.tooltipTemplateTemplateRef<CellContext<T>>—Rich content for a cell tooltip; the row is $implicit here as well. See Tooltips.