Header Groups
A column with children spans them in a header row of its own. From 1.3.0 that group can also fold: down to the one column it is worth reading at a glance, or away entirely behind a drawer with its name down the side.
columnGroupShow on a child makes its group foldable, collapsed gives it a starting state, collapseMode: 'rail' folds it away behind a drawer, and headerGroupCell draws the header yourself.Grouping Columns
children makes a column a group header spanning its leaves. Only id, header and the three folding fields apply to the group itself; everything else belongs to the leaves.
Resizing a group distributes the change across the columns underneath it.
// children makes a column a header group. Only id, header, collapsed,
// collapseMode and headerGroupCell apply to the group itself; everything
// else belongs to the leaves. Nesting is unlimited.
const columns: ColumnDef<Person>[] = [
{
id: 'identity',
header: 'Identity',
children: [
{ id: 'name', header: 'Name', flex: 1, minWidth: 140 },
{ id: 'email', header: 'Email', flex: 1.4, minWidth: 200 }
]
},
{
id: 'employment',
header: 'Employment',
children: [
{ id: 'team', header: 'Team', width: 120 },
{ id: 'role', header: 'Role', width: 120 },
{ id: 'salary', header: 'Salary', width: 130, align: 'right', type: 'currency' }
]
}
];
// A group id shares the columns' namespace, so it needs one of its own: a
// group and a column that answer to the same id cannot both be addressed.
// Resizing a group distributes the change across the columns underneath it.A group id shares the columns' namespace, so it needs one of its own: a group and a column that answer to the same id cannot both be addressed, by the snapshot or by anything else.
Folding to a Summary Column
A group folds when one of its children says what it is for. columnGroupShow: 'open' marks the detail a closed group puts away, 'closed' the summary it folds down to, and a child that says neither is drawn either way. Declaring it
on any child is what gives that group a toggle at the trailing edge of its header cell.
Pay folds down to Total and opens up to Base and Bonus. Ccy declares nothing, so it stays either way. Fold it from the header, from the column menu of any column in it, or from the buttons.
Columns on screen: Name, Base, Bonus, Ccy, Team. The toggle is the chevron at the trailing edge of the
Pay header cell, tinted here through the groupToggle slot so it is
easy to find; by default it is the same ghost button the rest of the header controls use.
// A group folds when one of its children says what it is for.
// 'open' is the detail a closed group puts away, 'closed' the summary it
// folds down to, and a child that says neither is drawn either way.
const columns: ColumnDef<Person>[] = [
{ id: 'name', header: 'Name', flex: 1, pinned: 'left' },
{
id: 'pay',
header: 'Pay',
collapsed: false, // its starting state
children: [
{ id: 'salary', header: 'Total', columnGroupShow: 'closed', type: 'currency' },
{ id: 'base', header: 'Base', columnGroupShow: 'open', type: 'currency' },
{ id: 'bonus', header: 'Bonus', columnGroupShow: 'open', type: 'currency' },
{ id: 'currency', header: 'Ccy', width: 80 } // neither: always drawn
]
}
];
// The toggle sits at the trailing edge of the group's own header cell, and
// the column menu of every column in the group carries the same action.
// A toggle is only offered when the state it would switch to leaves a
// column of the group on screen: a group whose children are all 'open'
// would fold its own header away, and nothing would be left to click.Folding to a Rail
collapseMode: 'rail' folds the other way: the whole group goes, header and cells alike, and a narrow drawer stands
in its place with the group's name down its length. It runs the full height of the grid, header
included, and clicking anywhere down it opens the group again, which is why no toggle sits in the
header over it.
Nothing has to declare columnGroupShow for this, because the drawer is what unfolds the group. Planning below starts folded, Money
starts open, and both fold this way.
// The other way to fold: the whole group goes, header and cells alike,
// and a narrow drawer stands in its place with the group's name down its
// length. Nothing needs columnGroupShow, because the drawer is what
// unfolds the group again.
const columns: ColumnDef<Person>[] = [
{ id: 'name', header: 'Name', flex: 1 },
{
id: 'planning',
header: 'Planning',
collapseMode: 'rail',
collapsed: true, // opens folded
children: [joined, rating]
}
];
// The drawer runs the full height of the grid, header included, and the
// name starts at the top and stays there as the rows scroll. Clicking
// anywhere down it opens the group again, which is why no toggle sits in
// the header over it.
// The rail is a column the grid draws for itself, like the selection
// checkbox: it holds no data, so it is never exported, copied or filtered
// on. A pinned group folds to a pinned drawer, which stays at its edge
// while the rest scrolls.The drawer is a column the grid draws for itself, the way the selection checkbox is: it holds no data, so it is never exported, copied or filtered on, and the group's own header cell is still underneath it, named and marked collapsed, which is how the keyboard reaches it.
A Pinned Group Folds to a Pinned Drawer
A group whose leaves share a pin side keeps that side when it folds, so the drawer stays at the edge while the rest of the table scrolls under it. Fold Who below and scroll the grid sideways: the strip stays where the pinned columns were.
// The other way to fold: the whole group goes, header and cells alike,
// and a narrow drawer stands in its place with the group's name down its
// length. Nothing needs columnGroupShow, because the drawer is what
// unfolds the group again.
const columns: ColumnDef<Person>[] = [
{ id: 'name', header: 'Name', flex: 1 },
{
id: 'planning',
header: 'Planning',
collapseMode: 'rail',
collapsed: true, // opens folded
children: [joined, rating]
}
];
// The drawer runs the full height of the grid, header included, and the
// name starts at the top and stays there as the rows scroll. Clicking
// anywhere down it opens the group again, which is why no toggle sits in
// the header over it.
// The rail is a column the grid draws for itself, like the selection
// checkbox: it holds no data, so it is never exported, copied or filtered
// on. A pinned group folds to a pinned drawer, which stays at its edge
// while the rest scrolls.A Group Inside a Group
columnGroupShow reads against the nearest group above the column, so a nested group's own children
answer to it rather than to the outer one. Q1 folds down to Total, taking the whole By month group
with it; By month folds down to Jan, which declares nothing against that group.
Columns on screen: Name, Jan, Feb, Mar, Team
// columnGroupShow reads against the nearest group above the column, so a
// nested group's own children answer to it rather than to the outer one.
const columns: ColumnDef<Row>[] = [
{ id: 'name', header: 'Name', flex: 1 },
{
id: 'quarter',
header: 'Q1',
children: [
{ id: 'q1Total', header: 'Total', columnGroupShow: 'closed' },
{
id: 'months',
header: 'By month',
columnGroupShow: 'open', // the whole nested group folds with Q1
children: [
{ id: 'jan', header: 'Jan' },
{ id: 'feb', header: 'Feb', columnGroupShow: 'open' },
{ id: 'mar', header: 'Mar', columnGroupShow: 'open' }
]
}
]
}
];
// Folding Q1 puts the nested group away with everything under it. Folding
// By month keeps Jan, which declares nothing against that group, and puts
// Feb and Mar away.Drawing the Header Yourself
headerGroupCell draws a group header the way headerCell draws a leaf one. It is handed the group cell and the same action the built-in control performs,
and the control stays beside whatever the snippet draws. header stays the label everything non-visual reads: the column menu, the announcer and the name of the
fold action.
<script lang="ts">
import type { HeaderGroupContext } from '@sv5ui/datagrid';
</script>
<!-- headerGroupCell draws a group header the way headerCell draws a leaf
one. The grid's own fold control stays beside whatever it draws, and
\`header\` stays the label everything non-visual reads: the column menu,
the announcer, and the name of the fold action. -->
{#snippet payHeader({ cell, toggle }: HeaderGroupContext)}
<button onclick={toggle} class="flex items-center gap-1.5">
<Icon name={cell.collapsed ? 'lucide:chevrons-right' : 'lucide:chevrons-left'} />
{cell.header}
<Badge label={String(cell.span)} size="xs" />
</button>
{/snippet}
<!-- cell carries the group's id, header, span, the leaves under it, and
whether it is collapsible and collapsed. toggle does nothing on a
group that cannot fold. -->
{ id: 'pay', header: 'Pay', headerGroupCell: payHeader, children: [...] }Header Group Context
What a headerGroupCell snippet receives.
| Field | Description |
|---|---|
cell.id | The group's id |
cell.header | Its label, and its accessible name |
cell.start | Index of the first column it spans, which is what names the cell in the DOM |
cell.span | How many columns it covers right now |
cell.isPlaceholder | True for the filler drawn above a column that has no group at this level. A placeholder never folds |
cell.leafIds | The leaves under it |
cell.pinned | The pin side its leaves share, if any |
cell.collapsible | Whether this group is offered a toggle at all |
cell.collapsed | Whether it is folded right now |
toggle | The same action the built-in control performs; a no-op on a group that cannot fold |
Driving It From Code
Every surface that folds a group comes through the same door, so the header toggle, the
column menu, the keyboard and your own call all announce it and all emit columnGroupToggled.
// Both are on grid.api and on getColumnOps(grid). The model settles
// whether a group may take that state at all, so a group with no toggle
// stays as it is rather than folding itself off the screen.
grid.api.toggleGroup?.('pay');
grid.api.setGroupCollapsed?.('pay', true);
// Read it back:
grid.columns.isCollapsed('pay'); // boolean
grid.columns.isRail('planning'); // folds to a drawer rather than a summary
grid.columns.groupDef('pay'); // the group's own definition
grid.columns.foldableGroupOf('salary'); // the group this column folds with, if any
// Every fold announces and emits, the same way a column move does:
grid.events.on('columnGroupToggled', ({ groupId, collapsed }) => save(groupId, collapsed));
// The keyboard reaches the levels above the leaf header row:
// ArrowUp from a leaf header walks up into the groups
// ArrowLeft / ArrowRight step between the groups of that level
// Enter or Space folds the group under the caret
// ArrowDown comes back downThe keyboard reaches the header levels themselves: ArrowUp from a leaf header walks up into the groups, ArrowLeft and ArrowRight step between the groups of a level, Enter or Space folds the one under the caret, and ArrowDown comes back. A column with no group above it has nowhere to go up to. The accessibility page has the rest of the map.
Folding Is Not Hiding
What the column chooser put away stays away when a group opens, and what a group folded comes back when it opens. The model keeps them apart for that reason, and a snapshot keeps both: columns by their own id, groups by theirs.
// Folding is not hiding, and the two are kept apart in the model:
grid.columns.hiddenOverrides; // what the column chooser put away, by column
grid.columns.collapsedGroups; // what a fold put away, by group
// So ticking a column in the chooser never opens a single column in the
// middle of a closed group, and a column put away stays away when its
// group opens.
// Both travel in a snapshot, keyed the way they are held:
{
version: 1,
columns: {
hidden: { country: true },
collapsed: { pay: true }
}
}
// A group that has since disappeared is dropped on the way back in, the
// same as a column id that no longer exists.A group that has since disappeared is dropped on the way back in, the same as a column id that no longer exists, so a saved view survives a schema that moved on. The persistence page covers the rest of the snapshot.
What a Group Declares
The fields that mean something on a group, or on a child of one.
| Field | Default |
|---|---|
children | - |
columnGroupShow | - |
collapsed | false |
collapseMode | 'summary' |
headerGroupCell | - |
Folding From Code
What grid.api, getColumnOps(grid) and the column model offer.
| Member | Description |
|---|---|
toggleGroup(id) | On grid.api and on getColumnOps(grid). Announces and emits columnGroupToggled |
setGroupCollapsed(id, on) | The same door, when you know which state you want |
columns.isCollapsed(id) | The group's own starting state, with the user's answer over it |
columns.isRail(id) | Whether this group folds to a drawer rather than to a summary column |
columns.groupDef(id) | The group node itself, for whatever draws or names it |
columns.foldableGroupOf(id) | The group a column would fold with, nearest first. What the column menu asks before offering the action |
columnGroupToggled | Emitted on every fold, whichever surface asked for it |