Lattice Grid Buy a licence

developer guide

Custom Group Row Renderer for a Data Grid

A group renderer hands you the group key, its totals and its leaf count, and lets you draw the heading row your product needs. grid.rows.leavesOf() reaches the rows behind it, your own control can expand and collapse, and groupDefaultExpanded decides what is already open when the grid appears.

Developer guideColumns and cell rendering › Custom Group Row Renderer for a Data Grid

Group rows you draw yourself

The grid's own group row is an expander, a label and a count. When the heading has to carry more than that - a sprint section with a chevron, the section name, the points summed across it, a done/total count and a progress bar - groupRenderer hands you the whole row. It is drawn as one band across every column, over the pinned regions, and no ordinary cells are mounted underneath it.

A section header with a rollup the grid was never told to compute

createGrid(element, {
  columns,
  rows,
  groupRenderer: ({ value, leafCount, expanded, leaves }) => {
    // `leaves()` is the group's own rows. Roll up whatever you like.
    const rows = leaves();
    const points = rows.reduce((sum, r) => sum + r.data.points, 0);
    const done = rows.filter((r) => r.data.done).length;
    const pct = Math.round((done / leafCount) * 100);
    return `<span data-lat-group-toggle>${expanded ? '▾' : '▸'}</span>
      <b>${value}</b> ${points} pts · ${done}/${leafCount}
      <progress value="${pct}" max="100"></progress>`;
  },
});

What the renderer is given

FieldWhat it is
keyThe group's key - the same string rows.expand() and rows.collapse() take, and the one group:toggled carries.
columnThe id of the column this level groups on. It is stamped per rebuild from the grouping, not derived from the row's depth, so it stays correct when an outer grouping is removed and this level becomes the outermost.
valueThe value this group stands for.
levelDepth of the group. Zero is the outermost.
expandedWhether the group is open. Draw your chevron from this; the row is re-rendered when it changes.
leafCountHow many records sit beneath the heading, at any depth. Read this when the size is all you need.
totalsThe group's own reductions by column id - whatever total asked for. Unaffected by drawing the row.
leaves()The rows beneath the heading, computed when you call it. The members the filters left, in display order, so a rollup agrees with the rows drawn below. Also available on its own as grid.rows.leavesOf(key).
toggle()Expand the group if it is closed, collapse it if it is open.
grid, row, elementThe grid, the group row model, and the element to fill if you would rather write into it than return anything.

A string here is markup, and in fullWidth.render it is text. That difference is deliberate, and it is the difference between the two features. A full-width row renders one of your data rows, and keeps host markup behind allowUnsafeTemplates everywhere a data value reaches the page. A group heading has no data row at all - the grid synthesised it from the grouping - so its string can only be a template you wrote in your own source. That is the same footing the board's cardRenderer already stands on, and the header this exists for (a chevron, a bar) is unwritable without it. If you interpolate a value that came from outside your application, escape it yourself, or build a node and return that instead.

Your chevron is yours to wire. The grid's own expander binds a click handler to the button it built; a string cannot carry a handler, so that chevron would be dead. Any element in your markup carrying data-lat-group-toggle expands or collapses the group it sits in, and toggle() does the same from a node you built yourself.

leaves() is a function on purpose. A group is unbounded and the renderer runs as rows are painted. Handing every group an array of its members would cost a hundred thousand rows on a heading nobody looked at. Call it when you need the rows; read leafCount when you need the size.

Which groups start open

groupDefaultExpanded decides the state of a group before anyone has touched it: true (the default) opens them all, false closes them all, a number opens the first N levels, and a predicate answers per group. Once the user or your own code expands or collapses a group, that decision stands - the default is not consulted for it again, so a group cannot spring shut under the user on the next rebuild.

The current sprint open, everything else closed, executed

const { createHeadlessGrid } = await import('../packages/core/src/index.js');

// The renderer the grid calls for each group row. Called here directly too,
// with the params the grid builds, so this block shows its actual output.
const groupRenderer = (p) => {
  const rows = p.leaves();
  const done = rows.filter((r) => r.data.done).length;
  const points = rows.reduce((sum, r) => sum + r.data.points, 0);
  return `${p.value} ${done}/${p.leafCount} ${points}pts ${p.expanded ? 'open' : 'closed'}`;
};

const grid = createHeadlessGrid({
  rowKey: 'id',
  columns: [{ field: 'section' }, { field: 'points', type: 'number', total: 'sum' }, { field: 'done' }],
  rows: [
    { id: 1, section: 'Sprint 12', points: 3, done: true },
    { id: 2, section: 'Sprint 12', points: 5, done: false },
    { id: 3, section: 'Backlog', points: 8, done: false },
  ],
  groupRenderer,
  // Per group, not merely per depth: only the current sprint starts open.
  groupDefaultExpanded: (group) => group.value === 'Sprint 12',
});
grid.columns.group(['section']);

const out = [];
for (let i = 0; i < grid.rows.count(); i++) {
  const row = grid.rows.get(i);
  if (!row.group) continue;
  out.push(groupRenderer({
    row, key: row.key, column: row.groupColumn, value: row.groupValue,
    level: row.level, expanded: row.expanded, leafCount: row.leafCount,
    totals: row.totals, leaves: () => grid.rows.leavesOf(row.key),
    toggle: () => {}, grid, element: null,
  }));
}
grid.destroy();
return out.join(' | ');

grid.rows.leavesOf(key) is the same reach on its own, for a breadcrumb, a side panel or a rollup computed away from the row: it returns the leaf rows beneath a group heading, which is what leafCount counts.