Lattice Grid Buy a licence

demo D44

Templates and your own renderer

The template compiler, then a custom renderer when it is not enough

cell.template · config.components

Building…
Loading a live grid…

The configuration

import * as ng from '@angular/core';
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import createLatticeGrid from '@toclocoinc/lattice-grid/modules/angular';

const { LatticeGridComponent } = createLatticeGrid({ ng, createGrid });

// A render function of your own: handed the cell params, it returns the element
// to show and is re-run on every repaint. Here a ten-segment ladder, which no
// built-in draws, reading a percentage as a count of steps.
function ladderRenderer(p) {
  const value = Number(p?.value);
  const lit = Number.isFinite(value) ? Math.round(Math.max(0, Math.min(100, value)) / 10) : 0;

  const doc = p?.grid?.element?.ownerDocument ?? document;
  const root = doc.createElement('span');
  root.setAttribute('role', 'img');
  root.setAttribute('aria-label', `${lit} of 10`);
  root.style.cssText = 'display:inline-flex;align-items:center;gap:3px;line-height:1';

  for (let i = 0; i < 10; i++) {
    const seg = doc.createElement('span');
    // currentColor is what makes it work in all four themes: each step takes
    // the cell's own text colour instead of naming one.
    seg.style.cssText = 'width:5px;height:13px;border-radius:1.5px;background:currentColor';
    seg.style.opacity = i < lit ? '1' : '0.16';
    root.appendChild(seg);
  }

  const label = doc.createElement('span');
  label.style.cssText = 'margin-inline-start:8px;font-variant-numeric:tabular-nums';
  label.textContent = p?.text != null ? String(p.text) : '';
  root.appendChild(label);

  return root;
}

const columns = [
  { field: 'service', title: 'Service', layout: { pin: 'start', width: 170 } },
  // Bindings and pipes. data.* reaches the row, value is the cell. Styled
  // inline rather than by class, so the template is the whole of the example
  // and nothing about it lives in a stylesheet.
  { field: 'owner', title: 'Owner', layout: { width: 260 },
    cell: { template: '<strong>{{value}}</strong> <span style="opacity:.6">{{data.email}}</span>' } },
  { field: 'region', title: 'Region', layout: { width: 190 },
    cell: { template: '{{value|upper}} <span style="opacity:.6">{{data.team|lower}}</span>' } },
  { field: 'cost', title: 'Monthly cost', type: 'number', layout: { width: 190 },
    cell: { template: '{{value|currency:USD:0}} <span style="opacity:.6">/mo</span>' } },
  // The rung above: ten quantised steps, which no built-in draws.
  { field: 'utilisation', title: 'Capacity band', type: 'number', layout: { width: 220 },
    format: { suffix: '%' }, cell: { render: ladderRenderer } },
  { field: 'status', title: 'Status', layout: { width: 130 },
    cell: { render: 'pill', props: { variant: {
      map: { healthy: 'success', degraded: 'warning', failing: 'danger' },
      default: 'neutral',
    } } } },
];

const rows = [/* rendering demo rows */];



@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent],
  template: '<lattice-grid [config]="grid" style="display:block;height:540px"></lattice-grid>',
})
export class AppComponent {
  grid = {
    rowKey: 'id',
    columns: columns,
    rows: rows,
  };
}

bootstrapApplication(AppComponent);

Compiling a cell template before you need a custom renderer

Most columns never need a hand-written renderer: a template string covers formatting, conditional text and small layout changes without a function call per cell. Lattice Grid compiles the string set on cell.template into a function once, at grid initialisation, rather than re-parsing it on every render pass, so a template column pays the same per-cell cost as a plain value lookup during scroll. A developer reaches for a template when a cell needs to combine two fields, apply a unit suffix, or switch a class name on a condition, and reaches past it to config.components only when the cell needs its own DOM lifecycle: an embedded chart, a form control, or anything that must attach and remove event listeners as rows recycle. Registering a component hands that column’s cells to a mount and unmount pair the grid calls as rows scroll into and out of the recycled pool, so a custom renderer follows the same virtual scrolling discipline as the built-in ones instead of leaking listeners on a long session. Screen readers still get a stable accessible name from the underlying cell value regardless of which path renders it. The two mechanisms sit on the same JavaScript data grid rendering pipeline, so a column can move from template to component later without touching sibling columns.

When should I use a template instead of writing a custom cell renderer?

Use cell.template for anything expressible as string interpolation: concatenated fields, formatted numbers, conditional labels. Move to config.components when a cell needs interactive elements, its own state, or third-party markup that a string cannot represent cleanly. Templates compile once and stay cheap on scroll; components carry more weight per cell, so reserve them for columns that genuinely need a DOM lifecycle.