Row Grouping Plugin
The Row Grouping plugin organizes your rows into collapsible hierarchical groups. Perfect for organizing data by category, department, status, or any other dimension—or even multiple dimensions for nested grouping.
Installation
Section titled “Installation”import '@toolbox-web/grid/features/grouping-rows';Basic Usage
Section titled “Basic Usage”The groupOn callback receives each row and should return an array representing the group path. For single-level grouping, return a one-element array. For multi-level grouping, return multiple elements (e.g., ['Region', 'Department']).
import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' } ], features: { groupingRows: { groupOn: (row) => [row.department], showRowCount: true, defaultExpanded: false, }, },};
grid.rows = employees;import '@toolbox-web/grid-react/features/grouping-rows';import { DataGrid } from '@toolbox-web/grid-react';import type { GridConfig } from '@toolbox-web/grid-react';
const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' } ], features: { groupingRows: { groupOn: (row) => row.department, showRowCount: true }, },};
function EmployeeGrid({ employees }) { return <DataGrid rows={employees} gridConfig={gridConfig} style={{ height: '400px' }} />;}<script setup>import '@toolbox-web/grid-vue/features/grouping-rows';import { TbwGrid } from '@toolbox-web/grid-vue';import type { GridConfig } from '@toolbox-web/grid-vue';
const employees = [ { name: 'Alice', department: 'Engineering', salary: 95000 }, { name: 'Bob', department: 'Marketing', salary: 75000 }, { name: 'Carol', department: 'Engineering', salary: 90000 },];
const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, ], features: { groupingRows: { groupOn: (row) => row.department, showRowCount: true }, },};</script>
<template> <TbwGrid :rows="employees" :grid-config="gridConfig" style="height: 400px" /></template>import '@toolbox-web/grid-angular/features/grouping-rows';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { GridConfig } from '@toolbox-web/grid-angular';
@Component({ selector: 'app-employee-grid', imports: [Grid], template: ` <tbw-grid [rows]="employees" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class EmployeeGridComponent { employees = [/* employee data */];
gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' } ], features: { groupingRows: { groupOn: (row: any) => [row.department], showRowCount: true, }, }, };}Default Grouping
Section titled “Default Grouping”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/grouping-rows';
const sampleData = [ { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 }, { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 }, { id: 3, name: 'Carol', department: 'Engineering', salary: 105000 }, { id: 4, name: 'Dan', department: 'Sales', salary: 85000 }, { id: 5, name: 'Eve', department: 'Marketing', salary: 72000 }, { id: 6, name: 'Frank', department: 'Engineering', salary: 98000 }, { id: 7, name: 'Grace', department: 'Sales', salary: 88000 },];const columns = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'number' },];
const grid = queryGrid('tbw-grid');
function rebuild(opts: Record<string, unknown>) { const accordion = opts.accordion ?? false; const expandRaw = opts.defaultExpanded; const defaultExpanded = accordion && expandRaw === 'all' ? false : expandRaw === 'all' ? true : expandRaw === 'none' ? false : expandRaw;
grid.gridConfig = { columns, features: { groupingRows: { animation: (opts.animation) ?? 'slide', groupOn: (row: { department: string }) => row.department, defaultExpanded, showRowCount: opts.showRowCount ?? true, indentWidth: (opts.indentWidth) ?? 20, fullWidth: true, accordion, }, }, }; grid.rows = sampleData;}
rebuild({ animation: 'slide', defaultExpanded: 'none', showRowCount: true, accordion: false, indentWidth: 20 });Custom Group Row Renderer
Section titled “Custom Group Row Renderer”Use groupRowRenderer to take full control of the group row’s contents. The renderer receives a GroupRowRenderParams object (key, value, depth, rows, expanded, toggleExpand) and can return an HTMLElement, an HTML string, or void to keep the default rendering. The framework adapters accept their native return types (ReactNode, VNode, or a component Type) and bridge them automatically.
grid.gridConfig = { features: { groupingRows: { groupOn: (row) => [row.department], groupRowRenderer: (params) => { const arrow = params.expanded ? '▼' : '▶'; // The `group-toggle` class makes the element a click target for the // plugin's delegated `closest('.group-toggle')` toggle handler. return `<button type="button" class="group-toggle">${arrow} <strong>${params.value}</strong> (${params.rows.length})</button>`; }, }, },};import '@toolbox-web/grid-react/features/grouping-rows';import { DataGrid } from '@toolbox-web/grid-react';import type { GridConfig } from '@toolbox-web/grid-react';
const gridConfig: GridConfig = { columns, features: { groupingRows: { groupOn: (row) => row.department, groupRowRenderer: (params) => ( <button type="button" onClick={params.toggleExpand}> {params.expanded ? '▼' : '▶'} <strong>{params.value}</strong> ({params.rows.length}) </button> ), }, },};
<DataGrid rows={employees} gridConfig={gridConfig} /><script setup>import { h } from 'vue';import '@toolbox-web/grid-vue/features/grouping-rows';import { TbwGrid } from '@toolbox-web/grid-vue';import type { GridConfig } from '@toolbox-web/grid-vue';
const gridConfig: GridConfig = { columns, features: { groupingRows: { groupOn: (row) => row.department, groupRowRenderer: (params) => h('button', { type: 'button', onClick: params.toggleExpand }, [ params.expanded ? '▼ ' : '▶ ', h('strong', params.value), ` (${params.rows.length})`, ]), }, },};</script>
<template> <TbwGrid :rows="employees" :grid-config="gridConfig" /></template>Pass a component class — the adapter bridges it and binds the params as inputs.
import '@toolbox-web/grid-angular/features/grouping-rows';import { Component, input } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { GridConfig } from '@toolbox-web/grid-angular';
@Component({ selector: 'app-group-row', template: ` <button type="button" (click)="toggleExpand()"> {{ expanded() ? '▼' : '▶' }} <strong>{{ value() }}</strong> ({{ rows().length }}) </button> `,})export class GroupRowComponent { key = input.required<string>(); value = input.required<unknown>(); depth = input.required<number>(); rows = input.required<unknown[]>(); expanded = input.required<boolean>(); toggleExpand = input.required<() => void>();}
@Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="employees" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class MyGridComponent { gridConfig: GridConfig = { columns: [...], features: { groupingRows: { groupOn: (row: any) => [row.department], groupRowRenderer: GroupRowComponent, }, }, };}Configuration Options
Section titled “Configuration Options”See GroupingRowsConfig for the full list of options and their defaults.
Default Expanded Options
Section titled “Default Expanded Options”The defaultExpanded option controls which groups are expanded on initial render:
// Expand all groupsfeatures: { groupingRows: { defaultExpanded: true, groupOn: ... } }
// Collapse all groups (default)features: { groupingRows: { defaultExpanded: false, groupOn: ... } }
// Expand group at index 0features: { groupingRows: { defaultExpanded: 0, groupOn: ... } }
// Expand group with specific keyfeatures: { groupingRows: { defaultExpanded: 'Engineering', groupOn: ... } }
// Expand multiple groups by keyfeatures: { groupingRows: { defaultExpanded: ['Engineering', 'Sales'], groupOn: ... } }Animation Options
Section titled “Animation Options”The animation option controls how grouped rows appear/disappear:
// Slide animation (default)features: { groupingRows: { animation: 'slide', ... } }
// Fade animationfeatures: { groupingRows: { animation: 'fade', ... } }
// No animationfeatures: { groupingRows: { animation: false, ... } }Accordion Mode
Section titled “Accordion Mode”In accordion mode, only one group can be expanded at a time. Expanding a group automatically collapses all sibling groups at the same depth level.
features: { groupingRows: { groupOn: (row) => row.department, accordion: true, // Only one group open at a time },},Pass a function to apply accordion mode only at some depths. It receives the group’s depth (0 = top level):
features: { groupingRows: { groupOn: (row) => [row.region, row.department], accordion: (depth) => depth === 0, // One region open at a time; departments open freely },},Aggregators
Section titled “Aggregators”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/grouping-rows';
// Sample data for grouping demosconst sampleData = [ { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 }, { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 }, { id: 3, name: 'Carol', department: 'Engineering', salary: 105000 }, { id: 4, name: 'Dan', department: 'Sales', salary: 85000 }, { id: 5, name: 'Eve', department: 'Marketing', salary: 72000 }, { id: 6, name: 'Frank', department: 'Engineering', salary: 98000 }, { id: 7, name: 'Grace', department: 'Sales', salary: 88000 },];const columns = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'number' },];
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns, features: { groupingRows: { groupOn: (row: { department: string }) => row.department, defaultExpanded: false, showRowCount: true, indentWidth: 20, aggregators: { salary: 'sum', }, }, }, }; grid.rows = sampleData;Display aggregate values (sum, avg, count, min, max, first, last) in group headers. Works in both full-width and per-column rendering modes.
features: { groupingRows: { groupOn: (row) => row.department, aggregators: { salary: 'sum', // Sum of salaries in each group bonus: 'avg', // Average bonus id: 'count', // Count of rows }, },},Built-in aggregators:
sum- Sum of numeric valuesavg- Average of numeric valuescount- Number of rowsmin- Minimum valuemax- Maximum valuefirst- First row’s valuelast- Last row’s value
You can also provide a custom aggregator function:
aggregators: { salary: (rows, field) => { const total = rows.reduce((sum, r) => sum + r[field], 0); return `$${total.toLocaleString()}`; },}Multi-Level Grouping
Section titled “Multi-Level Grouping”Return multiple values from groupOn to create nested group hierarchies. Row counts display correctly at every depth level.
<tbw-grid style="height: 450px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/grouping-rows';
const sampleData = [ { id: 1, name: 'Alice', country: 'USA', department: 'Engineering', salary: 95000 }, { id: 2, name: 'Bob', country: 'USA', department: 'Marketing', salary: 75000 }, { id: 3, name: 'Carol', country: 'USA', department: 'Engineering', salary: 105000 }, { id: 4, name: 'Dan', country: 'UK', department: 'Sales', salary: 85000 }, { id: 5, name: 'Eve', country: 'UK', department: 'Marketing', salary: 72000 }, { id: 6, name: 'Frank', country: 'USA', department: 'Sales', salary: 98000 }, { id: 7, name: 'Grace', country: 'UK', department: 'Sales', salary: 88000 }, { id: 8, name: 'Henry', country: 'Germany', department: 'Engineering', salary: 92000 }, { id: 9, name: 'Ivy', country: 'Germany', department: 'Engineering', salary: 110000 }, { id: 10, name: 'Jack', country: 'UK', department: 'Engineering', salary: 78000 },];
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number', width: 60 }, { field: 'name', header: 'Name' }, { field: 'country', header: 'Country' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'number', align: 'right' }, ], features: { groupingRows: { groupOn: (row: { country: string; department: string }) => [row.country, row.department], defaultExpanded: true, showRowCount: true, }, },};grid.rows = sampleData;features: { groupingRows: { groupOn: (row) => [row.region, row.department, row.team], },},Grouping Tree Children
Section titled “Grouping Tree Children”Row Grouping combines with the Tree plugin. Grouping then applies to every list of sibling rows: the root rows and the children of each expanded tree node. groupOn receives a second argument describing the list being grouped:
| Property | Description |
|---|---|
parent | The tree row whose children are being grouped, or null for the root rows |
depth | Nesting depth of the rows being grouped (0 = root rows) |
Return null for every row of a list to leave that level ungrouped:
features: { tree: { childrenField: 'children' }, groupingRows: { // Keep the root rows ungrouped; group each node's children by kind groupOn: (row, { depth }) => (depth > 0 ? row.kind : null), },},Groups under a tree node get keys prefixed with that node’s tree key ('plan-1::Cargo'), so the same group value under different parents expands independently. defaultExpanded: true also opens the top-level groups of each child list the first time it appears. setGroupOn(fn, true) opens the top-level groups of every list, the root rows and each child list.
<tbw-grid style="height: 450px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/grouping-rows';import '@toolbox-web/grid/features/tree';
type PlanRow = { name: string; region?: string; kind?: string };
const deliveryPlans = [ { id: 'plan-1', name: 'Rotterdam Q3', region: 'Europe', children: [ { id: 'p1-a', name: 'Brent cargo 0712', kind: 'Cargo', counterparty: 'Northsea Oil', volume: 600000 }, { id: 'p1-b', name: 'Fixed-price swap', kind: 'Deal', counterparty: 'Fjord Trading', volume: 250000 }, { id: 'p1-c', name: 'Forties cargo 0719', kind: 'Cargo', counterparty: 'Baltic Energy', volume: 450000 }, ], }, { id: 'plan-2', name: 'Houston Q3', region: 'Americas', children: [ { id: 'p2-a', name: 'WTI forward', kind: 'Deal', counterparty: 'Gulf Partners', volume: 300000 }, { id: 'p2-b', name: 'LNG cargo 0802', kind: 'Cargo', counterparty: 'Coastal Gas', volume: 150000 }, ], }, { id: 'plan-3', name: 'Hamburg Q3', region: 'Europe', children: [ { id: 'p3-a', name: 'Diesel option', kind: 'Deal', counterparty: 'Elbe Fuels', volume: 80000 }, { id: 'p3-b', name: 'Diesel cargo 0725', kind: 'Cargo', counterparty: 'Elbe Fuels', volume: 120000 }, ], },];
const groupOnFor = (levels: string) => { if (levels === 'every level') { return (row: PlanRow, { depth }: { depth: number }) => (depth === 0 ? row.region : row.kind); } if (levels === 'children') return (row: PlanRow, { depth }: { depth: number }) => (depth > 0 ? row.kind : null); return () => null;};
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'kind', header: 'Kind', width: 90 }, { field: 'counterparty', header: 'Counterparty' }, { field: 'volume', header: 'Volume (bbl)', type: 'number', align: 'right' }, ], features: { tree: { childrenField: 'children', defaultExpanded: true }, groupingRows: { groupOn: groupOnFor('children'), defaultExpanded: true, showRowCount: true }, },};grid.rows = deliveryPlans;With Group set to every level, groupOn returns row.region at depth 0 and row.kind below it, so the plans are grouped too.
The two plugins don’t know about each other. The Hierarchy plugin, which both depend on and which the grid attaches automatically, combines their output.
Programmatic API
Section titled “Programmatic API”See GroupingRowsPlugin for the full list of methods.
const plugin = grid.getPluginByName('groupingRows');
plugin.expand('Engineering');plugin.collapse('Engineering');plugin.toggle('Engineering');plugin.expandAll();plugin.collapseAll();plugin.isExpanded('Engineering'); // booleanplugin.getExpandedGroups(); // string[]plugin.setGroupOn((row) => [row.region, row.department]);// Optionally pass an initial expansion that resolves against the *new* groups// (issue #335). Avoids the stale-snapshot race when chaining setGroupOn → expandAll.plugin.setGroupOn((row) => row.counterparty, true);plugin.setGroupOn((row) => row.region, ['EMEA', 'AMER']);plugin.refreshGroups();Styling
Section titled “Styling”The row grouping plugin supports CSS custom properties for theming. Override these on tbw-grid or a parent container:
CSS Custom Properties
Section titled “CSS Custom Properties”| Property | Default | Description |
|---|---|---|
--tbw-group-indent-width | 1.25em (~20px) | Indentation per group level |
--tbw-grouping-rows-bg | var(--tbw-color-panel-bg) | Group row background |
--tbw-grouping-rows-bg-hover | var(--tbw-color-row-hover) | Group row hover |
--tbw-grouping-rows-toggle-hover | var(--tbw-color-row-hover) | Toggle button hover |
--tbw-grouping-rows-count-color | var(--tbw-color-fg-muted) | Count badge color |
--tbw-grouping-rows-aggregate-color | var(--tbw-color-fg-muted) | Aggregate value color |
--tbw-toggle-size | 1.25em | Toggle button size |
--tbw-font-size-xs | 0.7857em | Count text size |
--tbw-animation-duration | 200ms | Expand/collapse animation |
--tbw-animation-easing | ease-out | Animation curve |
Example
Section titled “Example”tbw-grid { /* Custom row grouping styling */ --tbw-group-indent-width: 1.5em; /* Wider indentation */ --tbw-grouping-rows-bg: #e8f5e9; --tbw-grouping-rows-bg-hover: #c8e6c9; --tbw-grouping-rows-count-color: #388e3c;}CSS Classes
Section titled “CSS Classes”The row grouping plugin uses these class names:
| Class | Element |
|---|---|
.group-row | Group header row |
.group-toggle | Expand/collapse button |
.group-label | Group name and value |
.group-count | Row count badge |
.group-aggregates | Container for aggregate values |
.group-aggregate | Individual aggregate value |
.tbw-group-slide-in | Child row slide animation |
.tbw-group-fade-in | Child row fade animation |
[data-group-depth="N"] | Group nesting level (0-4) |
.tbw-row-expanded | Applied to a .group-row while it is expanded. Use this for theming expanded group rows instead of [aria-expanded="true"]. |
Events
Section titled “Events”Click group headers to expand/collapse groups.
| Event | Detail | Description |
|---|---|---|
group-toggle | { key, expanded, value, depth } | Fired when a group is expanded/collapsed |
group-expand | { groupKey, groupPath } | Fired when a pre-defined group is expanded |
group-collapse | { groupKey, groupPath } | Fired when a pre-defined group is collapsed |
group-load-error | { groupKey, groupPath, error } | Fired when the ServerSide getChildRows() request for a group’s rows fails. See Server-Side Data. |
Server-Side Data
Section titled “Server-Side Data”Requires:
ServerSidePlugin— see Server-Side Plugin
For server-side scenarios where group structure is fetched from the server and rows are loaded on demand, use ServerSidePlugin alongside Row Grouping. ServerSide returns group definitions as root data; whenever a group is expanded (by the user, toggle(), expand(), expandAll(), or defaultExpanded), the plugin fetches its rows via getChildRows().
import '@toolbox-web/grid/features/grouping-rows';import '@toolbox-web/grid/features/server-side';import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, ], features: { serverSide: { pageSize: 50, dataSource: { async getRows(params) { // Return group definitions as root-level nodes const res = await fetch(`/api/groups?start=${params.startNode}&end=${params.endNode}`); const data = await res.json(); return { rows: data.groups, totalNodeCount: data.total }; }, async getChildRows(params) { // Return rows for an expanded group const { groupKey } = params.context; const res = await fetch(`/api/groups/${groupKey}/rows`); return { rows: await res.json() }; }, }, }, groupingRows: true, },};import '@toolbox-web/grid-react/features/grouping-rows';import '@toolbox-web/grid-react/features/server-side';import { DataGrid } from '@toolbox-web/grid-react';import type { GridConfig } from '@toolbox-web/grid-react';
const dataSource = { async getRows(params) { const res = await fetch(`/api/groups?start=${params.startNode}&end=${params.endNode}`); return res.json(); }, async getChildRows(params) { const { groupKey } = params.context; const res = await fetch(`/api/groups/${groupKey}/rows`); return { rows: await res.json() }; },};
const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, ], features: { serverSide: { pageSize: 50, dataSource }, groupingRows: true, },};
function ServerGroupedGrid() { return <DataGrid gridConfig={gridConfig} />;}<script setup>import '@toolbox-web/grid-vue/features/grouping-rows';import '@toolbox-web/grid-vue/features/server-side';import { TbwGrid } from '@toolbox-web/grid-vue';import type { GridConfig } from '@toolbox-web/grid-vue';
const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, ], features: { serverSide: { pageSize: 50, dataSource: { async getRows(params) { const res = await fetch(`/api/groups?start=${params.startNode}&end=${params.endNode}`); return res.json(); }, async getChildRows(params) { const { groupKey } = params.context; const res = await fetch(`/api/groups/${groupKey}/rows`); return { rows: await res.json() }; }, }, }, groupingRows: true, },};</script>
<template> <TbwGrid :grid-config="gridConfig" /></template>import '@toolbox-web/grid-angular/features/grouping-rows';import '@toolbox-web/grid-angular/features/server-side';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { GridConfig } from '@toolbox-web/grid-angular';
@Component({ selector: 'app-server-grouped-grid', imports: [Grid], template: ` <tbw-grid [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class ServerGroupedGridComponent { gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, ], features: { serverSide: { pageSize: 50, dataSource: { async getRows(params: any) { const res = await fetch(`/api/groups?start=${params.startNode}&end=${params.endNode}`); return res.json(); }, async getChildRows(params: any) { const { groupKey } = params.context; const res = await fetch(`/api/groups/${groupKey}/rows`); return { rows: await res.json() }; }, }, }, groupingRows: true, }, };}Data flow:
- ServerSide fetches block → broadcasts
datasource:data - GroupingRowsPlugin claims data as pre-defined groups, merging each block at its node offset (
startNode) - On group expand (any path, including
defaultExpandedandexpandAll()) → firesdatasource:fetch-childrenwith{ source: 'grouping-rows', groupKey } - ServerSide calls
getChildRows()→ broadcastsdatasource:children - GroupingRowsPlugin receives rows and renders them under the group
More groups than pageSize: Top-level groups load in blocks, like flat ServerSide rows. Groups whose block has not loaded yet show as loading rows and take up scroll space. Scrolling to them (or loadThreshold prefetching) requests their block, and each block is added to the groups already loaded. Loaded groups keep their rows and expanded state while more blocks arrive. With defaultExpanded: true, groups that have not been shown before open as their block loads, while groups the user collapsed stay collapsed, even after a refetch. A server sort/filter change, refresh(), or purgeCache() starts over from a fresh set of groups.
Errors and cancellation: If getChildRows() rejects, the group leaves its loading state (it stays expanded, with no rows) and the grid fires group-load-error with { groupKey, groupPath, error }. Collapsing and re-expanding the group retries the fetch. A request that is still in flight is aborted (params.signal) and its result discarded when new group definitions arrive (a server sort/filter change, refresh(), or setGroups()), and when setGroupRows() / clearGroupRows() replaces or clears that group’s rows. After a server refetch, every group that is still expanded and had its rows loaded (or loading) requests them again under the new sort/filter, so it does not end up expanded and empty. A group that is expanded without rows (for example after a failed fetch) gets one automatic attempt per set of group definitions, so a failing group is not re-requested on every render. Collapsing a group does not cancel its request: the rows are cached for the next expand.
grid.on('group-load-error', ({ groupKey, error }) => { console.error(`Rows for group ${groupKey} failed to load`, error);});Standalone Mode (Without ServerSide)
Section titled “Standalone Mode (Without ServerSide)”If ServerSidePlugin is not loaded, use the imperative API to provide groups and rows directly:
import '@toolbox-web/grid/features/grouping-rows';import { queryGrid } from '@toolbox-web/grid';
const grid = await queryGrid('tbw-grid', true);grid.gridConfig = { columns: [ { field: 'name', header: 'Employee' }, { field: 'department', header: 'Department' }, { field: 'salary', header: 'Salary', type: 'currency' }, ], features: { groupingRows: true, },};
const grouping = grid.getPluginByName('groupingRows');
// Lazy-load rows when a group is expandedgrid.on('group-expand', async ({ groupKey }) => { grouping.setGroupLoading(groupKey, true); const rows = await fetchGroupRows(groupKey); grouping.setGroupRows(groupKey, rows); // also clears loading state});
// Load groups asynchronouslyconst groups = await fetch('/api/groups').then(r => r.json());grouping.setGroups(groups);| Method | Description |
|---|---|
setGroups(groups) | Replace groups with an external structure |
getGroups() | Get the current group definitions |
setGroupRows(key, rows) | Populate rows for an expanded group |
setGroupLoading(key, loading) | Toggle loading indicator for a group |
clearGroupRows(key?) | Clear cached rows (specific group or all) |
Nested Pre-Defined Groups
Section titled “Nested Pre-Defined Groups”Groups can be nested using the children property:
const groups: GroupDefinition[] = [ { key: 'engineering', value: 'Engineering', rowCount: 150, children: [ { key: 'frontend', value: 'Frontend', rowCount: 60 }, { key: 'backend', value: 'Backend', rowCount: 90 }, ], },];Plugin Compatibility
Section titled “Plugin Compatibility”A development-mode warning is shown if incompatible plugins are loaded together. Row Grouping does combine with Tree — see Grouping Tree Children.
See Also
Section titled “See Also”- Column Groups — Group column headers
- Tree — Hierarchical tree data
- Pivot — Pivot table with grouping
- Recipes — Task-oriented how-to guides
- Plugins Overview — Plugin compatibility and combinations