demo D138
HTML in cells, safely
Templates escape by default; the opt-out and what it opens up
allowUnsafeTemplates: true
The configuration
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.27.0/lattice-grid.min.css">
<div id="grid" style="height: 540px"></div>
<script type="module">
import { createGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.27.0/modules/htmx.esm.js';
const grid = createGrid(document.getElementById('grid'), {
rowKey: 'id',
autoHeight: true,
// The opt-out, and the whole subject of this page: an unsafe interpolation
// is written as markup rather than escaped. Set it only when the data is
// trusted, because the value is placed with no filtering of its own.
allowUnsafeTemplates: true,
columns: [
{ field: 'service', title: 'Service', layout: { width: 150, pin: 'start' } },
// No template: the plain text path, which never parses markup.
{ id: 'raw', field: 'label', title: 'The stored value', layout: { width: 260 },
cell: { wrap: true } },
// Double braces escape, even with the opt-out set: the choice is per
// interpolation, not per grid.
{ id: 'escaped', field: 'label', title: '{{value}}', layout: { width: 260 },
cell: { template: '{{value}}', wrap: true } },
// Triple braces render as elements, because the opt-out above is set.
{ id: 'unescaped', field: 'label', title: '{{{value}}}', layout: { width: 260 },
cell: { template: '{{{value}}}', wrap: true } },
// A render function returning a string goes through the same gate as triple
// braces: markup without the opt-out is written as text.
{ id: 'fromString', field: 'label', title: 'render to string', layout: { width: 260 },
cell: { render: (p) => String(p.value ?? ''), wrap: true } },
// The route that never needs the opt-out: built with createElement and
// textContent, so a value that contains markup is text by construction.
{ id: 'fromElement', title: 'render to element', layout: { flex: 1, min: 220 },
cell: {
render: (p) => {
const span = document.createElement('span');
const tier = document.createElement('b');
tier.textContent = String(p.data?.tier ?? '');
span.append(tier, document.createTextNode(` and ${String(p.data?.label ?? '')}`));
return span;
},
wrap: true,
} },
],
rows, // e.g. [{ id: 1, service: 'edge-router', tier: 'gold', label: '<b>10G</b> uplift' }, ...]
});
</script>
Rendering trusted markup with allowUnsafeTemplates
By default, Lattice Grid escapes every value it puts into a cell template, so a string containing <span> or & lands on screen as literal text rather than markup. That default is what keeps a JavaScript data grid safe against XSS when a template pulls in user-submitted or third-party data: a name field, a comment, an imported spreadsheet cell. Setting allowUnsafeTemplates: true on the grid config lifts that escaping for every template column at once, so a developer reaches for it when the data is already trusted, or already sanitised upstream, and the cell genuinely needs to render markup rather than describe it: inline badges built from a template string, a highlighted substring, a small icon plus label composed in one interpolation rather than a full custom renderer. The option is grid-wide rather than per-column, so mixing untrusted and markup-bearing columns in the same grid means sanitising the untrusted values yourself before they reach the template. Turning the option on does not change how the grid virtualises rows: templates still compile once at initialisation and run at the same per-cell cost during scroll, whether their output is escaped or not. Screen readers read whatever text content the rendered markup exposes, so a badge or icon inserted this way needs its own accessible label if the visual marker is not decorative.
How do I render HTML inside a Lattice Grid cell?
Set allowUnsafeTemplates: true in the grid configuration, then include markup directly in a column’s cell.template string. The grid stops escaping template output for every column once this is set, so only enable it when the underlying data is trusted or has already been sanitised, since it removes the default protection against injected markup.