Group expansion and data-row selection
Inspect explicit group order, aggregates, and retained data-row selection in collapsed groups.
Actions and expected results
Section titled “Actions and expected results”| Action | Expected result |
|---|---|
| Initial view | East: 2 rows/sum 200; Empty: 0 rows; West: 1 row/sum 240. |
| Select first data row | Selects Alpha, the first leaf, skipping synthetic group rows. |
| Fold East | Hides Alpha/Beta while retaining alpha in selection and sum 200. |
| getSelectedRows | Displays selected business IDs in the last-read retrieved field. |
| Expand East | Shows Alpha/Beta without recreating the selection. |
| Reverse groups | Changes order to West → Empty → East; row IDs and membership remain intact. |
| Reset | Restores initial group order and expansion, and clears selection. |
You can also use group disclosure buttons and click business rows according to the existing Row Selection rules.
State ownership
Section titled “State ownership”The application owns groups and expandedGroupIds. Connect onChangeExpandedGroupIds to accept UI and ref expansion requests. The example uses setGroups to reorder the explicit group array without duplicating order fields inside group objects.
setSelectedRow(0) addresses a visible data leaf, excluding group headings. The index depends on sorting and expansion, so do not persist it as identity. Use the ID provided by getRowId for stable identity.
Selection in a collapsed group remains dormant. The example separately displays row IDs received by onChangeSelection and business data queried through getSelectedRows(). Table owns this selection state; the observed IDs are not injected back as a controlled selection prop.
Aggregates and empty groups
Section titled “Aggregates and empty groups”The example uses aggregations: { amount: "sum" }. Collapsing a group does not remove its members, so aggregate inputs remain. Empty stays visible because it exists in groups; the example displays — when no numeric sum exists. renderGroupContent renders only the inner label, count, and aggregate; the package owns the outer row and disclosure.
Scope and related documentation
Section titled “Scope and related documentation”This is single-depth CSR grouping. It does not include Group selection, Tree, pagination, or remote loading. Group/Row Drag and filtering/Summary combinations remain separate scenarios.
Row grouping guide · Expansion methods · Read selection
Try it live
Section titled “Try it live”import { useRef, useState } from 'react';import { CominsTable, type CominsTableRef, type CominsTableColumn, type CominsRowId } from 'comins-table';import 'comins-table/styles.css';type Row = { id: string; name: string; groupId: string; amount: number };type Group = { id: string; label: string };const initialGroups: Group[] = [{ id: 'east', label: 'East' }, { id: 'empty', label: 'Empty' }, { id: 'west', label: 'West' }];const rows: Row[] = [{ id: 'alpha', name: 'Alpha', groupId: 'east', amount: 120 }, { id: 'beta', name: 'Beta', groupId: 'east', amount: 80 }, { id: 'gamma', name: 'Gamma', groupId: 'west', amount: 240 }];const columns: CominsTableColumn<Row>[] = [{ id: 'name', field: 'name', label: 'Name', width: 230, sort: true }, { id: 'amount', field: 'amount', label: 'Amount', width: 170, sort: true }];export default function TableGroupingDemo({ lang = 'ko' }: { lang?: string }) { const ko = lang === 'ko'; const table = useRef<CominsTableRef<Row>>(null); const [groups, setGroups] = useState(initialGroups); const [expanded, setExpanded] = useState<CominsRowId[]>(['east', 'empty', 'west']); const [selection, setSelection] = useState<CominsRowId[]>([]); const [retrieved, setRetrieved] = useState<string[]>([]); return <div className="demo" data-demo="table-grouping"> <div className="demo-toolbar"> <button onClick={() => table.current?.setSelectedRow(0)}>{ko ? '첫 업무 행 선택' : 'Select first data row'}</button> <button onClick={() => table.current?.foldGroups(['east'])}>{ko ? 'East 접기' : 'Fold East'}</button> <button onClick={() => table.current?.expandGroups(['east'])}>{ko ? 'East 펼치기' : 'Expand East'}</button> <button onClick={() => setRetrieved(table.current?.getSelectedRows().map(row => row.id) ?? [])}>getSelectedRows</button> <button onClick={() => setGroups(current => [...current].reverse())}>{ko ? '그룹 순서 반전' : 'Reverse groups'}</button> <button onClick={() => { setGroups(initialGroups); setExpanded(['east','empty','west']); table.current?.setSelectedRows([]); setRetrieved([]); }}>{ko ? '초기화' : 'Reset'}</button> </div> <div className="table-viewport" style={{ height: 300 }}> <CominsTable<Row, Group> ref={table} data={rows} columns={columns} getRowId={row => row.id} rowSelectionOnClick onChangeSelection={next => setSelection([...next.rowIds])} rowGrouping={{ groups, getGroupId: group => group.id, getGroupLabel: group => group.label, getRowGroupId: row => row.groupId, expandedGroupIds: expanded, onChangeExpandedGroupIds: setExpanded, aggregations: { amount: 'sum' }, renderGroupContent: ({ group, rowCount, aggregateValues }) => <span data-group-label={group.id}>{group.label} · {rowCount} · {aggregateValues.amount ?? '—'}</span>, }} /> </div> <pre data-group-state>{JSON.stringify({ groups: groups.map(group => group.id), expanded, selection, retrieved }, null, 2)}</pre> </div>;}