demo D44
Templates and your own renderer in React
The template compiler, then a custom renderer when it is not enough
cell.template · config.components
This grid formats cells with a compiled template string for the common cases, then a custom renderer for the moments a template cannot cover. It gives you a light option for most columns and full control for the few that need it, without a function call per cell everywhere.
This is the React version. The grid mounts through the createLatticeGrid adapter, which takes its configuration as ordinary props and hands back the live grid through a ref. The grid below is the same one every other tab runs.
The configuration
<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">
<div id="grid"></div>
<script type="module">
import React from 'https://esm.sh/react@18';
import { createRoot } from 'https://esm.sh/react-dom@18/client';
import { createGrid } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.esm.min.js';
import createLatticeGrid from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/react.esm.min.js';
const LatticeGrid = createLatticeGrid({ React, 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 */];
function App() {
return (
<LatticeGrid
rowKey="id"
columns={columns}
rows={rows}
style={{ height: '540px' }}
/>
);
}
createRoot(document.getElementById('grid')).render(<App />);
</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.