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

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.13.0/lattice-grid.min.css">
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.13.0/lattice-grid.min.js"></script>

<div id="grid" style="height: 540px"></div>

<script>
  // 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 grid = LatticeGrid.createGrid(document.getElementById('grid'), {
    rowKey: 'id',
    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',
        } } } },
    ],
    rows,  // rendering demo rows
  });
</script>

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.