demo D44
Templates and your own renderer
The template compiler, then a custom renderer when it is not enough
cell.template · config.components
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.