Documentation
Row Grouping
Organize rows into expandable hierarchies — load children upfront or on demand.
- 1
Mark the expandable column
Setexpandable: trueon the column that shows the hierarchy label (where the expand control appears).TypeScriptCopy{accessor: "organization",label: "Organization",width: 200,expandable: true,}TypeScriptCopy{accessor: "organization",label: "Organization",width: 200,expandable: true,}TypeScriptCopy{accessor: "organization",label: "Organization",width: 200,expandable: true,}TypeScriptCopy{accessor: "organization",label: "Organization",width: 200,expandable: true,}TypeScriptCopy{accessor: "organization",label: "Organization",width: 200,expandable: true,}TypeScriptCopy{accessor: "organization",label: "Organization",width: 200,expandable: true,} - 2
Shape nested data
Nest child arrays under property names you will list inrowGrouping. Child rows share the same columns — for different columns per level, see Nested Tables.TypeScriptCopy{id: "company-1",organization: "TechSolutions Inc.",divisions: [{id: "div-100",organization: "Engineering",departments: [{ id: "dept-1", organization: "Frontend" },{ id: "dept-2", organization: "Backend" },],},],}TypeScriptCopy{id: "company-1",organization: "TechSolutions Inc.",divisions: [{id: "div-100",organization: "Engineering",departments: [{ id: "dept-1", organization: "Frontend" },{ id: "dept-2", organization: "Backend" },],},],}TypeScriptCopy{id: "company-1",organization: "TechSolutions Inc.",divisions: [{id: "div-100",organization: "Engineering",departments: [{ id: "dept-1", organization: "Frontend" },{ id: "dept-2", organization: "Backend" },],},],}TypeScriptCopy{id: "company-1",organization: "TechSolutions Inc.",divisions: [{id: "div-100",organization: "Engineering",departments: [{ id: "dept-1", organization: "Frontend" },{ id: "dept-2", organization: "Backend" },],},],}TypeScriptCopy{id: "company-1",organization: "TechSolutions Inc.",divisions: [{id: "div-100",organization: "Engineering",departments: [{ id: "dept-1", organization: "Frontend" },{ id: "dept-2", organization: "Backend" },],},],}TypeScriptCopy{id: "company-1",organization: "TechSolutions Inc.",divisions: [{id: "div-100",organization: "Engineering",departments: [{ id: "dept-1", organization: "Frontend" },{ id: "dept-2", organization: "Backend" },],},],} - 3
Set rowGrouping
PassrowGroupingin depth order, plusgetRowIdso expansion stays stable across sort and updates.React TSXCopy<SimpleTablecolumns={columns}rows={rows}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}/>AngularCopy<simple-table[columns]="columns"[rows]="rows"[rowGrouping]="['divisions', 'departments']"[getRowId]="getRowId"></simple-table>Vue SFCCopy<SimpleTable:columns="columns":rows="rows":row-grouping="['divisions', 'departments']":get-row-id="(ctx) => String(ctx.row.id)"/>SvelteCopy<SimpleTable{columns}{rows}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}/>Solid TSXCopy<SimpleTablecolumns={columns}rows={rows()}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}/>TypeScriptCopynew SimpleTableVanilla(container, {columns,rows,rowGrouping: ["divisions", "departments"],getRowId: ({ row }) => String(row.id),});
Patterns
Start expanded
Set expandAll so grouped rows open on first load.
React TSX
Copy
<SimpleTablecolumns={columns}rows={rows}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}expandAll/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"[rowGrouping]="['divisions', 'departments']"[getRowId]="getRowId"[expandAll]="true"></simple-table>
Vue SFC
Copy
<SimpleTable:columns="columns":rows="rows":row-grouping="['divisions', 'departments']":get-row-id="(ctx) => String(ctx.row.id)":expand-all="true"/>
Svelte
Copy
<SimpleTable{columns}{rows}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}expandAll={true}/>
Solid TSX
Copy
<SimpleTablecolumns={columns}rows={rows()}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}expandAll/>
TypeScript
Copy
new SimpleTableVanilla(container, {columns,rows,rowGrouping: ["divisions", "departments"],getRowId: ({ row }) => String(row.id),expandAll: true,});
Sticky parent rows
Set enableStickyParents so parents stick while you scroll their children. Beta — defaults to off.
React TSX
Copy
<SimpleTablecolumns={columns}rows={rows}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}enableStickyParents/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"[rowGrouping]="['divisions', 'departments']"[getRowId]="getRowId"[enableStickyParents]="true"></simple-table>
Vue SFC
Copy
<SimpleTable:columns="columns":rows="rows":row-grouping="['divisions', 'departments']":get-row-id="(ctx) => String(ctx.row.id)":enable-sticky-parents="true"/>
Svelte
Copy
<SimpleTable{columns}{rows}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}enableStickyParents={true}/>
Solid TSX
Copy
<SimpleTablecolumns={columns}rows={rows()}rowGrouping={["divisions", "departments"]}getRowId={({ row }) => String(row.id)}enableStickyParents/>
TypeScript
Copy
new SimpleTableVanilla(container, {columns,rows,rowGrouping: ["divisions", "departments"],getRowId: ({ row }) => String(row.id),enableStickyParents: true,});
Lazy-load children
Use onRowGroupExpand to fetch children on expand. Helpers setLoading, setError, and setEmpty drive row states; rowIndexPath locates the nested row to update.
React TSX
Copy
<SimpleTablecolumns={columns}rows={rows}rowGrouping={["stores", "products"]}getRowId={({ row }) => String(row.id)}onRowGroupExpand={async ({ row, isExpanded, groupingKey, setLoading, setError, setEmpty, rowIndexPath }) => {if (!isExpanded) return;setLoading(true);try {const children = await fetchChildren(row.id, groupingKey);setLoading(false);if (children.length === 0) {setEmpty(true, "No data");return;}// update rows using rowIndexPath / groupingKey} catch (error) {setLoading(false);setError(error.message);}}}/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"[rowGrouping]="['stores', 'products']"[getRowId]="getRowId"(rowGroupExpand)="handleRowGroupExpand($event)"></simple-table>
Vue SFC
Copy
<SimpleTable:columns="columns":rows="rows":row-grouping="['stores', 'products']":get-row-id="(ctx) => String(ctx.row.id)":on-row-group-expand="handleRowGroupExpand"/>
Svelte
Copy
<SimpleTable{columns}{rows}rowGrouping={["stores", "products"]}getRowId={({ row }) => String(row.id)}onRowGroupExpand={handleRowGroupExpand}/>
Solid TSX
Copy
<SimpleTablecolumns={columns}rows={rows()}rowGrouping={["stores", "products"]}getRowId={({ row }) => String(row.id)}onRowGroupExpand={async ({ row, isExpanded, groupingKey, setLoading, setError, setEmpty, rowIndexPath }) => {if (!isExpanded) return;setLoading(true);try {const children = await fetchChildren(row.id, groupingKey);setLoading(false);if (children.length === 0) {setEmpty(true, "No data");return;}// update rows using rowIndexPath / groupingKey} catch (error) {setLoading(false);setError(error.message);}}}/>
TypeScript
Copy
new SimpleTableVanilla(container, {columns,rows,rowGrouping: ["stores", "products"],getRowId: ({ row }) => String(row.id),onRowGroupExpand: async ({ row, isExpanded, groupingKey, setLoading, setError, setEmpty, rowIndexPath }) => {if (!isExpanded) return;setLoading(true);try {const children = await fetchChildren(row.id, groupingKey);setLoading(false);if (children.length === 0) {setEmpty(true, "No data");return;}// update rows using rowIndexPath / groupingKey} catch (error) {setLoading(false);setError(error.message);}},});
Programmatic expand / collapse
Control expansion from the table API: expandAll, collapseAll, expandDepth, setExpandedDepths, toggleDepth.
React TSX
Copy
tableRef.current?.expandAll();tableRef.current?.collapseAll();tableRef.current?.expandDepth(0);tableRef.current?.setExpandedDepths(new Set([0, 1]));tableRef.current?.toggleDepth(0);
Angular
Copy
this.tableRef.getAPI()?.expandAll();this.tableRef.getAPI()?.collapseAll();this.tableRef.getAPI()?.expandDepth(0);this.tableRef.getAPI()?.setExpandedDepths(new Set([0, 1]));this.tableRef.getAPI()?.toggleDepth(0);
Vue SFC
Copy
tableRef.value?.expandAll();tableRef.value?.collapseAll();tableRef.value?.expandDepth(0);tableRef.value?.setExpandedDepths(new Set([0, 1]));tableRef.value?.toggleDepth(0);
Svelte
Copy
tableRef.getAPI()?.expandAll();tableRef.getAPI()?.collapseAll();tableRef.getAPI()?.expandDepth(0);tableRef.getAPI()?.setExpandedDepths(new Set([0, 1]));tableRef.getAPI()?.toggleDepth(0);
Solid TSX
Copy
tableRef.expandAll();tableRef.collapseAll();tableRef.expandDepth(0);tableRef.setExpandedDepths(new Set([0, 1]));tableRef.toggleDepth(0);
TypeScript
Copy
table.expandAll();table.collapseAll();table.expandDepth(0);table.setExpandedDepths(new Set([0, 1]));table.toggleDepth(0);
Example
Control Expansion:
React TSX
Copy
1import { useRef } from "react";2import {SimpleTable} from "@simple-table/react";import type { Theme, TableAPI } from "@simple-table/react";3import { rowGroupingConfig, type OrgUnit } from "./row-grouping.demo-data";4import "@simple-table/react/styles.css";56const btnStyle = (color: string) => ({7 padding: "6px 12px",8 background: color,9 color: "white",10 border: "none",11 borderRadius: 4,12 cursor: "pointer",13 fontSize: 12,14 fontWeight: 500 as const15});1617const RowGroupingDemo = ({18 height = "400px",19 theme20}: {21 height?: string | number;22 theme?: Theme;23}) => {24 const tableRef = useRef<TableAPI<OrgUnit>>(null);2526 return (27 <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>28 <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>29 <span style={{ fontSize: 13, fontWeight: 600, marginRight: 8 }}>Control Expansion:</span>30 <button style={btnStyle("#28a745")} onClick={() => tableRef.current?.expandAll()} title="expandAll()">31 Expand All32 </button>33 <button style={btnStyle("#dc3545")} onClick={() => tableRef.current?.collapseAll()} title="collapseAll()">34 Collapse All35 </button>36 <button37 style={btnStyle("#007bff")}38 onClick={() => { tableRef.current?.collapseAll(); tableRef.current?.expandDepth(0); }}39 title="expandDepth(0)"40 >41 Only Divisions42 </button>43 <button44 style={btnStyle("#6c757d")}45 onClick={() => tableRef.current?.setExpandedDepths(new Set([0, 1]))}46 title="setExpandedDepths(new Set([0, 1]))"47 >48 Divisions + Departments49 </button>50 <button style={btnStyle("#6f42c1")} onClick={() => tableRef.current?.toggleDepth(0)} title="toggleDepth(0)">51 Toggle Divisions52 </button>53 </div>54 <SimpleTable55 ref={tableRef}56 columns={rowGroupingConfig.headers}57 rows={rowGroupingConfig.rows}58 rowGrouping={rowGroupingConfig.tableProps.rowGrouping}59 enableStickyParents={rowGroupingConfig.tableProps.enableStickyParents}60 getRowId={({ row }) => row.id}61 columnResizing62 height={height}63 theme={theme}64 />65 </div>66 );67};6869export default RowGroupingDemo;
Angularrow-grouping-demo.component.ts
Copy
1import { Component, Input, ViewChild } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, GetRowIdParams, Theme } from "@simple-table/angular";3import { rowGroupingConfig } from "./row-grouping.demo-data";4import "@simple-table/angular/styles.css";5import type { OrgUnit } from "./row-grouping.demo-data";67@Component({8 selector: "row-grouping-demo",9 standalone: true,10 imports: [SimpleTableComponent],11 template: `12 <div style="display: flex; flex-direction: column; gap: 12px">13 <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center">14 <span style="font-size: 13px; font-weight: 600; margin-right: 8px">Control Expansion:</span>15 <button [style.padding]="'6px 12px'" [style.background]="'#28a745'" [style.color]="'white'" [style.border]="'none'" [style.borderRadius]="'4px'" [style.cursor]="'pointer'" [style.fontSize]="'12px'" [style.fontWeight]="500" (click)="expandAll()" title="expandAll()">Expand All</button>16 <button [style.padding]="'6px 12px'" [style.background]="'#dc3545'" [style.color]="'white'" [style.border]="'none'" [style.borderRadius]="'4px'" [style.cursor]="'pointer'" [style.fontSize]="'12px'" [style.fontWeight]="500" (click)="collapseAll()" title="collapseAll()">Collapse All</button>17 <button [style.padding]="'6px 12px'" [style.background]="'#007bff'" [style.color]="'white'" [style.border]="'none'" [style.borderRadius]="'4px'" [style.cursor]="'pointer'" [style.fontSize]="'12px'" [style.fontWeight]="500" (click)="onlyDivisions()" title="expandDepth(0)">Only Divisions</button>18 <button [style.padding]="'6px 12px'" [style.background]="'#6c757d'" [style.color]="'white'" [style.border]="'none'" [style.borderRadius]="'4px'" [style.cursor]="'pointer'" [style.fontSize]="'12px'" [style.fontWeight]="500" (click)="divisionsAndDepts()" title="setExpandedDepths(new Set([0, 1]))">Divisions + Departments</button>19 <button [style.padding]="'6px 12px'" [style.background]="'#6f42c1'" [style.color]="'white'" [style.border]="'none'" [style.borderRadius]="'4px'" [style.cursor]="'pointer'" [style.fontSize]="'12px'" [style.fontWeight]="500" (click)="toggleDivisions()" title="toggleDepth(0)">Toggle Divisions</button>20 </div>21 <simple-table22 #simpleTable23 [rows]="rows"24 [columns]="headers"25 [height]="height"26 [theme]="theme"27 [rowGrouping]="grouping"28 [enableStickyParents]="true"29 [getRowId]="getRowId"30 [columnResizing]="true"31 ></simple-table>32 </div>33 `,34})35export class RowGroupingDemoComponent {36 @ViewChild("simpleTable") tableRef!: SimpleTableComponent;37 @Input() height: string | number = "400px";38 @Input() theme?: Theme;3940 readonly rows: OrgUnit[] = rowGroupingConfig.rows;41 readonly headers: AngularColumnDef<OrgUnit>[] = rowGroupingConfig.headers;42 readonly grouping = rowGroupingConfig.tableProps.rowGrouping;43 getRowId = ({ row }: GetRowIdParams<OrgUnit>) => row.id;4445 expandAll() { this.tableRef.getAPI()?.expandAll(); }46 collapseAll() { this.tableRef.getAPI()?.collapseAll(); }47 onlyDivisions() { this.tableRef.getAPI()?.collapseAll(); this.tableRef.getAPI()?.expandDepth(0); }48 divisionsAndDepts() { this.tableRef.getAPI()?.setExpandedDepths(new Set([0, 1])); }49 toggleDivisions() { this.tableRef.getAPI()?.toggleDepth(0); }50}515253// row-grouping.demo-data.ts54// Self-contained demo table setup for this example.55import type { AngularColumnDef } from "@simple-table/angular";5657/** Shared metrics at every grouping depth; nesting uses rowGrouping keys. */58export interface OrgUnit {59 id: string;60 organization: string;61 employees: number;62 budget: string;63 performance: string;64 location: string;65 growthRate: string;66 status: string;67 established: string;68 divisions?: OrgUnit[];69 departments?: OrgUnit[];70}7172export const rowGroupingHeaders: AngularColumnDef<OrgUnit>[] = [73 { accessor: "organization", label: "Organization", width: 200, expandable: true, type: "string" },74 { accessor: "employees", label: "Employees", width: 100, type: "number" },75 { accessor: "budget", label: "Annual Budget", width: 140, type: "string" },76 { accessor: "performance", label: "Performance", width: 120, type: "string" },77 { accessor: "location", label: "Location", width: 130, type: "string" },78 { accessor: "growthRate", label: "Growth", width: 90, type: "string" },79 { accessor: "status", label: "Status", width: 110, type: "string" },80 { accessor: "established", label: "Est. Date", width: 110, type: "date" },81];8283export const rowGroupingData: OrgUnit[] = [84 {85 id: "company-1",86 organization: "TechSolutions Inc.",87 employees: 137,88 budget: "$15.0M",89 performance: "Exceeding",90 location: "San Francisco",91 growthRate: "+9%",92 status: "Expanding",93 established: "2018-01-01",94 divisions: [95 {96 id: "div-100",97 organization: "Engineering Division",98 employees: 97,99 budget: "$10.6M",100 performance: "Exceeding",101 location: "Multiple",102 growthRate: "+11%",103 status: "Expanding",104 established: "2018-01-15",105 departments: [106 { id: "dept-1001", organization: "Frontend", employees: 28, budget: "$2.8M", performance: "Exceeding", location: "San Francisco", growthRate: "+12%", status: "Hiring", established: "2019-05-16" },107 { id: "dept-1002", organization: "Backend", employees: 32, budget: "$3.4M", performance: "Meeting", location: "Seattle", growthRate: "+8%", status: "Stable", established: "2018-03-22" },108 { id: "dept-1003", organization: "DevOps", employees: 15, budget: "$1.9M", performance: "Exceeding", location: "Remote", growthRate: "+15%", status: "Hiring", established: "2020-11-05" },109 { id: "dept-1004", organization: "Mobile", employees: 22, budget: "$2.5M", performance: "Meeting", location: "Austin", growthRate: "+10%", status: "Restructuring", established: "2019-08-12" },110 ],111 },112 {113 id: "div-101",114 organization: "Product Division",115 employees: 40,116 budget: "$4.4M",117 performance: "Meeting",118 location: "Multiple",119 growthRate: "+5%",120 status: "Stable",121 established: "2019-01-10",122 departments: [123 { id: "dept-1101", organization: "Design", employees: 17, budget: "$1.8M", performance: "Meeting", location: "Portland", growthRate: "+6%", status: "Stable", established: "2019-02-28" },124 { id: "dept-1102", organization: "Research", employees: 9, budget: "$1.4M", performance: "Below Target", location: "Boston", growthRate: "+3%", status: "Reviewing", established: "2020-07-15" },125 { id: "dept-1103", organization: "QA Testing", employees: 14, budget: "$1.2M", performance: "Meeting", location: "Chicago", growthRate: "+5%", status: "Stable", established: "2019-11-01" },126 ],127 },128 ],129 },130 {131 id: "company-2",132 organization: "HealthFirst Group",133 employees: 138,134 budget: "$22.4M",135 performance: "Meeting",136 location: "Boston",137 growthRate: "+8%",138 status: "Stable",139 established: "2010-01-01",140 divisions: [141 {142 id: "div-200",143 organization: "Hospital Operations",144 employees: 106,145 budget: "$13.1M",146 performance: "Meeting",147 location: "Multiple",148 growthRate: "+6%",149 status: "Expanding",150 established: "2010-01-05",151 departments: [152 { id: "dept-2001", organization: "Emergency", employees: 48, budget: "$5.2M", performance: "Meeting", location: "New York", growthRate: "+4%", status: "Critical", established: "2010-06-14" },153 { id: "dept-2002", organization: "Cardiology", employees: 32, budget: "$4.8M", performance: "Exceeding", location: "Chicago", growthRate: "+9%", status: "Expanding", established: "2012-03-25" },154 { id: "dept-2003", organization: "Pediatrics", employees: 26, budget: "$3.1M", performance: "Meeting", location: "Boston", growthRate: "+7%", status: "Stable", established: "2014-08-30" },155 ],156 },157 {158 id: "div-201",159 organization: "Research & Development",160 employees: 32,161 budget: "$9.3M",162 performance: "Exceeding",163 location: "Multiple",164 growthRate: "+15%",165 status: "Hiring",166 established: "2017-01-10",167 departments: [168 { id: "dept-2101", organization: "Clinical Trials", employees: 18, budget: "$4.2M", performance: "Exceeding", location: "San Diego", growthRate: "+12%", status: "Expanding", established: "2017-04-18" },169 { id: "dept-2102", organization: "Genomics", employees: 14, budget: "$5.1M", performance: "Exceeding", location: "Cambridge", growthRate: "+18%", status: "Hiring", established: "2019-02-21" },170 ],171 },172 ],173 },174 {175 id: "company-3",176 organization: "Global Finance",177 employees: 121,178 budget: "$15.5M",179 performance: "Meeting",180 location: "New York",181 growthRate: "+3%",182 status: "Restructuring",183 established: "2005-01-01",184 divisions: [185 {186 id: "div-300",187 organization: "Banking Operations",188 employees: 121,189 budget: "$15.5M",190 performance: "Meeting",191 location: "Multiple",192 growthRate: "+3%",193 status: "Stable",194 established: "2005-01-15",195 departments: [196 { id: "dept-3001", organization: "Retail Banking", employees: 56, budget: "$4.8M", performance: "Meeting", location: "New York", growthRate: "+2%", status: "Stable", established: "2005-11-08" },197 { id: "dept-3002", organization: "Investment", employees: 38, budget: "$7.2M", performance: "Exceeding", location: "Chicago", growthRate: "+11%", status: "Hiring", established: "2008-05-12" },198 { id: "dept-3003", organization: "Loans", employees: 27, budget: "$3.5M", performance: "Below Target", location: "Dallas", growthRate: "-3%", status: "Restructuring", established: "2010-03-17" },199 ],200 },201 ],202 },203 {204 id: "company-4",205 organization: "Apex University",206 employees: 115,207 budget: "$13.4M",208 performance: "Meeting",209 location: "Cambridge",210 growthRate: "+6%",211 status: "Stable",212 established: "1992-01-01",213 divisions: [214 {215 id: "div-400",216 organization: "Academic Departments",217 employees: 115,218 budget: "$13.4M",219 performance: "Meeting",220 location: "Multiple",221 growthRate: "+6%",222 status: "Stable",223 established: "1992-01-15",224 departments: [225 { id: "dept-4001", organization: "Computer Science", employees: 35, budget: "$3.8M", performance: "Meeting", location: "Boston", growthRate: "+8%", status: "Expanding", established: "1998-08-24" },226 { id: "dept-4002", organization: "Business", employees: 42, budget: "$4.5M", performance: "Exceeding", location: "Chicago", growthRate: "+6%", status: "Stable", established: "1995-09-15" },227 { id: "dept-4003", organization: "Engineering", employees: 38, budget: "$5.1M", performance: "Meeting", location: "San Francisco", growthRate: "+4%", status: "Stable", established: "1992-02-11" },228 ],229 },230 ],231 },232 {233 id: "company-5",234 organization: "Industrial Systems",235 employees: 152,236 budget: "$12.9M",237 performance: "Meeting",238 location: "Detroit",239 growthRate: "+3%",240 status: "Stable",241 established: "2001-01-01",242 divisions: [243 {244 id: "div-500",245 organization: "Production",246 employees: 152,247 budget: "$12.9M",248 performance: "Meeting",249 location: "Multiple",250 growthRate: "+3%",251 status: "Stable",252 established: "2001-01-10",253 departments: [254 { id: "dept-5001", organization: "Assembly", employees: 78, budget: "$6.2M", performance: "Meeting", location: "Detroit", growthRate: "+2%", status: "Stable", established: "2001-05-18" },255 { id: "dept-5002", organization: "Quality Control", employees: 32, budget: "$2.8M", performance: "Exceeding", location: "Pittsburgh", growthRate: "+5%", status: "Hiring", established: "2003-11-24" },256 { id: "dept-5003", organization: "Logistics", employees: 42, budget: "$3.9M", performance: "Meeting", location: "Indianapolis", growthRate: "+3%", status: "Stable", established: "2005-02-08" },257 ],258 },259 ],260 },261];262263export const rowGroupingConfig = {264 headers: rowGroupingHeaders,265 rows: rowGroupingData,266 tableProps: {267 rowGrouping: ["divisions", "departments"],268 enableStickyParents: true,269 columnResizing: true,270 },271};272
Vue SFC
Copy
1<template>2 <div style="display: flex; flex-direction: column; gap: 12px">3 <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center">4 <span style="font-size: 13px; font-weight: 600; margin-right: 8px">Control Expansion:</span>5 <button :style="btnStyle('#28a745')" @click="expandAll" title="expandAll()">Expand All</button>6 <button :style="btnStyle('#dc3545')" @click="collapseAll" title="collapseAll()">Collapse All</button>7 <button :style="btnStyle('#007bff')" @click="onlyDivisions" title="expandDepth(0)">Only Divisions</button>8 <button :style="btnStyle('#6c757d')" @click="divisionsAndDepts" title="setExpandedDepths(new Set([0, 1]))">Divisions + Departments</button>9 <button :style="btnStyle('#6f42c1')" @click="toggleDivisions" title="toggleDepth(0)">Toggle Divisions</button>10 </div>11 <SimpleTable12 ref="tableRef"13 :columns="rowGroupingConfig.headers"14 :rows="rowGroupingConfig.rows"15 :row-grouping="rowGroupingConfig.tableProps.rowGrouping"16 :enable-sticky-parents="true"17 :get-row-id="getRowId"18 :column-resizing="true"19 :height="height"20 :theme="theme"21 />22 </div>23</template>2425<script setup lang="ts">26import { ref } from "vue";27import { SimpleTable } from "@simple-table/vue";28import type { Theme, GetRowIdParams, SimpleTableExposed } from "@simple-table/vue";29import { rowGroupingConfig } from "./row-grouping.demo-data";30import type { OrgUnit } from "./row-grouping.demo-data";31import "@simple-table/vue/styles.css";3233withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {34 height: "400px",35});3637const tableRef = ref<SimpleTableExposed<OrgUnit> | null>(null);38const getRowId = ({ row }: GetRowIdParams<OrgUnit>) => row.id;3940function btnStyle(color: string) {41 return {42 padding: "6px 12px",43 background: color,44 color: "white",45 border: "none",46 borderRadius: "4px",47 cursor: "pointer",48 fontSize: "12px",49 fontWeight: "500",50 };51}5253function expandAll() { tableRef.value?.getAPI()?.expandAll(); }54function collapseAll() { tableRef.value?.getAPI()?.collapseAll(); }55function onlyDivisions() { tableRef.value?.getAPI()?.collapseAll(); tableRef.value?.getAPI()?.expandDepth(0); }56function divisionsAndDepts() { tableRef.value?.getAPI()?.setExpandedDepths(new Set([0, 1])); }57function toggleDivisions() { tableRef.value?.getAPI()?.toggleDepth(0); }58</script>
Svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, TableAPI, GetRowIdParams } from "@simple-table/svelte";4 import { rowGroupingConfig } from "./row-grouping.demo-data";5 import type { OrgUnit } from "./row-grouping.demo-data";6 import "@simple-table/svelte/styles.css";78 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();910 let tableRef: { getAPI: () => TableAPI | null } | undefined;1112 const getRowId = ({ row }: GetRowIdParams<OrgUnit>) => row.id;1314 function expandAll() { tableRef?.getAPI()?.expandAll(); }15 function collapseAll() { tableRef?.getAPI()?.collapseAll(); }16 function onlyDivisions() { tableRef?.getAPI()?.collapseAll(); tableRef?.getAPI()?.expandDepth(0); }17 function divisionsAndDepts() { tableRef?.getAPI()?.setExpandedDepths(new Set([0, 1])); }18 function toggleDivisions() { tableRef?.getAPI()?.toggleDepth(0); }1920 function btnStyle(color: string) {21 return `padding:6px 12px;background:${color};color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:500`;22 }23</script>2425<div style="display: flex; flex-direction: column; gap: 12px">26 <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center">27 <span style="font-size: 13px; font-weight: 600; margin-right: 8px">Control Expansion:</span>28 <button style={btnStyle("#28a745")} onclick={expandAll} title="expandAll()">Expand All</button>29 <button style={btnStyle("#dc3545")} onclick={collapseAll} title="collapseAll()">Collapse All</button>30 <button style={btnStyle("#007bff")} onclick={onlyDivisions} title="expandDepth(0)">Only Divisions</button>31 <button style={btnStyle("#6c757d")} onclick={divisionsAndDepts} title="setExpandedDepths(new Set([0, 1]))">Divisions + Departments</button>32 <button style={btnStyle("#6f42c1")} onclick={toggleDivisions} title="toggleDepth(0)">Toggle Divisions</button>33 </div>34 <SimpleTable35 bind:this={tableRef}36 columns={rowGroupingConfig.headers}37 rows={rowGroupingConfig.rows}38 rowGrouping={rowGroupingConfig.tableProps.rowGrouping}39 enableStickyParents={true}40 getRowId={getRowId}41 columnResizing={true}42 {height}43 {theme}44 />45</div>
Solid TSX
Copy
1import { SimpleTable } from "@simple-table/solid";2import type { Theme, TableAPI } from "@simple-table/solid";3import { rowGroupingConfig, type OrgUnit } from "./row-grouping.demo-data";4import "@simple-table/solid/styles.css";56const btnStyle = (color: string) => ({7 padding: "6px 12px",8 background: color,9 color: "white",10 border: "none",11 "border-radius": "4px",12 cursor: "pointer",13 "font-size": "12px",14 "font-weight": 500,15});1617export default function RowGroupingDemo(props: { height?: string | number; theme?: Theme }) {18 let tableRef: TableAPI<OrgUnit> | undefined;1920 return (21 <div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>22 <div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap", "align-items": "center" }}>23 <span style={{ "font-size": "13px", "font-weight": 600, "margin-right": "8px" }}>Control Expansion:</span>24 <button style={btnStyle("#28a745")} onClick={() => tableRef?.expandAll()} title="expandAll()">Expand All</button>25 <button style={btnStyle("#dc3545")} onClick={() => tableRef?.collapseAll()} title="collapseAll()">Collapse All</button>26 <button style={btnStyle("#007bff")} onClick={() => { tableRef?.collapseAll(); tableRef?.expandDepth(0); }} title="expandDepth(0)">Only Divisions</button>27 <button style={btnStyle("#6c757d")} onClick={() => tableRef?.setExpandedDepths(new Set([0, 1]))} title="setExpandedDepths(new Set([0, 1]))">Divisions + Departments</button>28 <button style={btnStyle("#6f42c1")} onClick={() => tableRef?.toggleDepth(0)} title="toggleDepth(0)">Toggle Divisions</button>29 </div>30 <SimpleTable31 ref={(api) => (tableRef = api)}32 columns={rowGroupingConfig.headers}33 rows={rowGroupingConfig.rows}34 rowGrouping={rowGroupingConfig.tableProps.rowGrouping}35 enableStickyParents={rowGroupingConfig.tableProps.enableStickyParents}36 getRowId={({ row }) => row.id}37 columnResizing38 height={props.height ?? "400px"}39 theme={props.theme}40 />41 </div>42 );43}
TypeScriptRowGroupingDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { OrgUnit } from "./row-grouping.demo-data";3import type { Theme, GetRowIdParams } from "simple-table-core";4import { rowGroupingConfig } from "./row-grouping.demo-data";5import "simple-table-core/styles.css";67const getRowId = ({ row }: GetRowIdParams<OrgUnit>) => row.id;89export function renderRowGroupingDemo(10 container: HTMLElement,11 options?: { height?: string | number; theme?: Theme }12): SimpleTableVanilla<OrgUnit> {13 const wrapper = document.createElement("div");14 wrapper.style.cssText = "display:flex;flex-direction:column;gap:12px";1516 const controls = document.createElement("div");17 controls.style.cssText = "display:flex;gap:8px;flex-wrap:wrap;align-items:center";1819 const label = document.createElement("span");20 label.textContent = "Control Expansion:";21 label.style.cssText = "font-size:13px;font-weight:600;margin-right:8px";22 controls.appendChild(label);2324 const tableContainer = document.createElement("div");25 wrapper.appendChild(controls);26 wrapper.appendChild(tableContainer);27 container.appendChild(wrapper);2829 const table = new SimpleTableVanilla(tableContainer, {30 columns: rowGroupingConfig.headers,31 rows: rowGroupingConfig.rows,32 height: options?.height ?? "400px",33 theme: options?.theme,34 rowGrouping: rowGroupingConfig.tableProps.rowGrouping,35 enableStickyParents: true,36 getRowId,37 columnResizing: true,38 });3940 const api = table.getAPI();4142 const buttons: Array<{ label: string; color: string; action: () => void }> = [43 { label: "Expand All", color: "#28a745", action: () => api.expandAll() },44 { label: "Collapse All", color: "#dc3545", action: () => api.collapseAll() },45 { label: "Only Divisions", color: "#007bff", action: () => { api.collapseAll(); api.expandDepth(0); } },46 { label: "Divisions + Departments", color: "#6c757d", action: () => api.setExpandedDepths(new Set([0, 1])) },47 { label: "Toggle Divisions", color: "#6f42c1", action: () => api.toggleDepth(0) },48 ];4950 for (const { label: text, color, action } of buttons) {51 const btn = document.createElement("button");52 btn.textContent = text;53 btn.style.cssText = `padding:6px 12px;background:${color};color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:500`;54 btn.addEventListener("click", action);55 controls.appendChild(btn);56 }5758 return table;59}606162// row-grouping.demo-data.ts63// Self-contained demo table setup for this example.64import type { ColumnDef } from "simple-table-core";6566/** Shared metrics at every grouping depth; nesting uses rowGrouping keys. */67export interface OrgUnit {68 id: string;69 organization: string;70 employees: number;71 budget: string;72 performance: string;73 location: string;74 growthRate: string;75 status: string;76 established: string;77 divisions?: OrgUnit[];78 departments?: OrgUnit[];79}8081export const rowGroupingHeaders: ColumnDef<OrgUnit>[] = [82 { accessor: "organization", label: "Organization", width: 200, expandable: true, type: "string" },83 { accessor: "employees", label: "Employees", width: 100, type: "number" },84 { accessor: "budget", label: "Annual Budget", width: 140, type: "string" },85 { accessor: "performance", label: "Performance", width: 120, type: "string" },86 { accessor: "location", label: "Location", width: 130, type: "string" },87 { accessor: "growthRate", label: "Growth", width: 90, type: "string" },88 { accessor: "status", label: "Status", width: 110, type: "string" },89 { accessor: "established", label: "Est. Date", width: 110, type: "date" },90];9192export const rowGroupingData: OrgUnit[] = [93 {94 id: "company-1",95 organization: "TechSolutions Inc.",96 employees: 137,97 budget: "$15.0M",98 performance: "Exceeding",99 location: "San Francisco",100 growthRate: "+9%",101 status: "Expanding",102 established: "2018-01-01",103 divisions: [104 {105 id: "div-100",106 organization: "Engineering Division",107 employees: 97,108 budget: "$10.6M",109 performance: "Exceeding",110 location: "Multiple",111 growthRate: "+11%",112 status: "Expanding",113 established: "2018-01-15",114 departments: [115 { id: "dept-1001", organization: "Frontend", employees: 28, budget: "$2.8M", performance: "Exceeding", location: "San Francisco", growthRate: "+12%", status: "Hiring", established: "2019-05-16" },116 { id: "dept-1002", organization: "Backend", employees: 32, budget: "$3.4M", performance: "Meeting", location: "Seattle", growthRate: "+8%", status: "Stable", established: "2018-03-22" },117 { id: "dept-1003", organization: "DevOps", employees: 15, budget: "$1.9M", performance: "Exceeding", location: "Remote", growthRate: "+15%", status: "Hiring", established: "2020-11-05" },118 { id: "dept-1004", organization: "Mobile", employees: 22, budget: "$2.5M", performance: "Meeting", location: "Austin", growthRate: "+10%", status: "Restructuring", established: "2019-08-12" },119 ],120 },121 {122 id: "div-101",123 organization: "Product Division",124 employees: 40,125 budget: "$4.4M",126 performance: "Meeting",127 location: "Multiple",128 growthRate: "+5%",129 status: "Stable",130 established: "2019-01-10",131 departments: [132 { id: "dept-1101", organization: "Design", employees: 17, budget: "$1.8M", performance: "Meeting", location: "Portland", growthRate: "+6%", status: "Stable", established: "2019-02-28" },133 { id: "dept-1102", organization: "Research", employees: 9, budget: "$1.4M", performance: "Below Target", location: "Boston", growthRate: "+3%", status: "Reviewing", established: "2020-07-15" },134 { id: "dept-1103", organization: "QA Testing", employees: 14, budget: "$1.2M", performance: "Meeting", location: "Chicago", growthRate: "+5%", status: "Stable", established: "2019-11-01" },135 ],136 },137 ],138 },139 {140 id: "company-2",141 organization: "HealthFirst Group",142 employees: 138,143 budget: "$22.4M",144 performance: "Meeting",145 location: "Boston",146 growthRate: "+8%",147 status: "Stable",148 established: "2010-01-01",149 divisions: [150 {151 id: "div-200",152 organization: "Hospital Operations",153 employees: 106,154 budget: "$13.1M",155 performance: "Meeting",156 location: "Multiple",157 growthRate: "+6%",158 status: "Expanding",159 established: "2010-01-05",160 departments: [161 { id: "dept-2001", organization: "Emergency", employees: 48, budget: "$5.2M", performance: "Meeting", location: "New York", growthRate: "+4%", status: "Critical", established: "2010-06-14" },162 { id: "dept-2002", organization: "Cardiology", employees: 32, budget: "$4.8M", performance: "Exceeding", location: "Chicago", growthRate: "+9%", status: "Expanding", established: "2012-03-25" },163 { id: "dept-2003", organization: "Pediatrics", employees: 26, budget: "$3.1M", performance: "Meeting", location: "Boston", growthRate: "+7%", status: "Stable", established: "2014-08-30" },164 ],165 },166 {167 id: "div-201",168 organization: "Research & Development",169 employees: 32,170 budget: "$9.3M",171 performance: "Exceeding",172 location: "Multiple",173 growthRate: "+15%",174 status: "Hiring",175 established: "2017-01-10",176 departments: [177 { id: "dept-2101", organization: "Clinical Trials", employees: 18, budget: "$4.2M", performance: "Exceeding", location: "San Diego", growthRate: "+12%", status: "Expanding", established: "2017-04-18" },178 { id: "dept-2102", organization: "Genomics", employees: 14, budget: "$5.1M", performance: "Exceeding", location: "Cambridge", growthRate: "+18%", status: "Hiring", established: "2019-02-21" },179 ],180 },181 ],182 },183 {184 id: "company-3",185 organization: "Global Finance",186 employees: 121,187 budget: "$15.5M",188 performance: "Meeting",189 location: "New York",190 growthRate: "+3%",191 status: "Restructuring",192 established: "2005-01-01",193 divisions: [194 {195 id: "div-300",196 organization: "Banking Operations",197 employees: 121,198 budget: "$15.5M",199 performance: "Meeting",200 location: "Multiple",201 growthRate: "+3%",202 status: "Stable",203 established: "2005-01-15",204 departments: [205 { id: "dept-3001", organization: "Retail Banking", employees: 56, budget: "$4.8M", performance: "Meeting", location: "New York", growthRate: "+2%", status: "Stable", established: "2005-11-08" },206 { id: "dept-3002", organization: "Investment", employees: 38, budget: "$7.2M", performance: "Exceeding", location: "Chicago", growthRate: "+11%", status: "Hiring", established: "2008-05-12" },207 { id: "dept-3003", organization: "Loans", employees: 27, budget: "$3.5M", performance: "Below Target", location: "Dallas", growthRate: "-3%", status: "Restructuring", established: "2010-03-17" },208 ],209 },210 ],211 },212 {213 id: "company-4",214 organization: "Apex University",215 employees: 115,216 budget: "$13.4M",217 performance: "Meeting",218 location: "Cambridge",219 growthRate: "+6%",220 status: "Stable",221 established: "1992-01-01",222 divisions: [223 {224 id: "div-400",225 organization: "Academic Departments",226 employees: 115,227 budget: "$13.4M",228 performance: "Meeting",229 location: "Multiple",230 growthRate: "+6%",231 status: "Stable",232 established: "1992-01-15",233 departments: [234 { id: "dept-4001", organization: "Computer Science", employees: 35, budget: "$3.8M", performance: "Meeting", location: "Boston", growthRate: "+8%", status: "Expanding", established: "1998-08-24" },235 { id: "dept-4002", organization: "Business", employees: 42, budget: "$4.5M", performance: "Exceeding", location: "Chicago", growthRate: "+6%", status: "Stable", established: "1995-09-15" },236 { id: "dept-4003", organization: "Engineering", employees: 38, budget: "$5.1M", performance: "Meeting", location: "San Francisco", growthRate: "+4%", status: "Stable", established: "1992-02-11" },237 ],238 },239 ],240 },241 {242 id: "company-5",243 organization: "Industrial Systems",244 employees: 152,245 budget: "$12.9M",246 performance: "Meeting",247 location: "Detroit",248 growthRate: "+3%",249 status: "Stable",250 established: "2001-01-01",251 divisions: [252 {253 id: "div-500",254 organization: "Production",255 employees: 152,256 budget: "$12.9M",257 performance: "Meeting",258 location: "Multiple",259 growthRate: "+3%",260 status: "Stable",261 established: "2001-01-10",262 departments: [263 { id: "dept-5001", organization: "Assembly", employees: 78, budget: "$6.2M", performance: "Meeting", location: "Detroit", growthRate: "+2%", status: "Stable", established: "2001-05-18" },264 { id: "dept-5002", organization: "Quality Control", employees: 32, budget: "$2.8M", performance: "Exceeding", location: "Pittsburgh", growthRate: "+5%", status: "Hiring", established: "2003-11-24" },265 { id: "dept-5003", organization: "Logistics", employees: 42, budget: "$3.9M", performance: "Meeting", location: "Indianapolis", growthRate: "+3%", status: "Stable", established: "2005-02-08" },266 ],267 },268 ],269 },270];271272export const rowGroupingConfig = {273 headers: rowGroupingHeaders,274 rows: rowGroupingData,275 tableProps: {276 rowGrouping: ["divisions", "departments"],277 enableStickyParents: true,278 columnResizing: true,279 },280};281
Dynamic loading example
Children load when a parent expands (regions → stores → products).
React TSX
Copy
1import { useState, useCallback } from "react";2import { SimpleTable } from "@simple-table/react";3import type { Theme, OnRowGroupExpandProps } from "@simple-table/react";4import {5 dynamicRowLoadingConfig,6 generateInitialRegions,7 fetchStoresForRegion,8 fetchProductsForStore9} from "./dynamic-row-loading.demo-data";10import type { DynamicRegion, DynamicTreeRow } from "./dynamic-row-loading.demo-data";11import "@simple-table/react/styles.css";1213const DynamicRowLoadingDemo = ({14 height = "400px",15 theme16}: {17 height?: string | number;18 theme?: Theme;19}) => {20 const [rows, setRows] = useState<DynamicRegion[]>(() => generateInitialRegions());2122 const handleRowExpand = useCallback(23 async ({24 row,25 depth,26 groupingKey,27 isExpanded,28 setLoading,29 setError,30 setEmpty,31 rowIndexPath32 }: OnRowGroupExpandProps<DynamicTreeRow>) => {33 if (!isExpanded) return;34 if (groupingKey && row[groupingKey as keyof DynamicTreeRow] != null) {35 const nested = row[groupingKey as keyof DynamicTreeRow];36 if (Array.isArray(nested) && nested.length > 0) return;37 }3839 try {40 if (depth === 0 && groupingKey === "stores" && row.type === "region") {41 setLoading(true);42 const stores = await fetchStoresForRegion(row.id);43 setLoading(false);44 if (stores.length === 0) {45 setEmpty(true, "No stores found for this region");46 return;47 }48 setRows((prevRows) => {49 const newRows = [...prevRows];50 newRows[rowIndexPath[0]].stores = stores;51 return newRows;52 });53 } else if (depth === 1 && groupingKey === "products" && row.type === "store") {54 setLoading(true);55 const products = await fetchProductsForStore(row.id);56 setLoading(false);57 if (products.length === 0) {58 setEmpty(true, "No products found for this store");59 return;60 }61 setRows((prevRows) => {62 const newRows = [...prevRows];63 const region = newRows[rowIndexPath[0]];64 if (region.stores && region.stores[rowIndexPath[1]]) {65 region.stores[rowIndexPath[1]].products = products;66 }67 return newRows;68 });69 }70 } catch (error) {71 setLoading(false);72 setError(error instanceof Error ? error.message : "Failed to load data");73 }74 },75 [],76 );7778 return (79 <SimpleTable80 columnResizing={dynamicRowLoadingConfig.tableProps.columnResizing}81 columns={dynamicRowLoadingConfig.headers}82 enableColumnEditor={dynamicRowLoadingConfig.tableProps.enableColumnEditor}83 expandAll={dynamicRowLoadingConfig.tableProps.expandAll}84 height={height}85 onRowGroupExpand={handleRowExpand}86 rowGrouping={dynamicRowLoadingConfig.tableProps.rowGrouping}87 getRowId={({ row }) => row.id}88 rows={rows}89 selectableCells={dynamicRowLoadingConfig.tableProps.selectableCells}90 theme={theme}91 oddEvenRowBackground={dynamicRowLoadingConfig.tableProps.oddEvenRowBackground}92 />93 );94};9596export default DynamicRowLoadingDemo;
Angulardynamic-row-loading-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, GetRowIdParams, OnRowGroupExpandProps, Theme } from "@simple-table/angular";3import {4 dynamicRowLoadingConfig,5 generateInitialRegions,6 fetchStoresForRegion,7 fetchProductsForStore,8} from "./dynamic-row-loading.demo-data";9import type { DynamicRegion, DynamicTreeRow } from "./dynamic-row-loading.demo-data";10import "@simple-table/angular/styles.css";1112@Component({13 selector: "dynamic-row-loading-demo",14 standalone: true,15 imports: [SimpleTableComponent],16 template: `17 <simple-table18 [columnResizing]="true"19 [columns]="headers"20 [enableColumnEditor]="true"21 [expandAll]="false"22 [height]="height"23 [onRowGroupExpand]="handleRowExpand"24 [rowGrouping]="grouping"25 [getRowId]="getRowId"26 [rows]="rows"27 [selectableCells]="true"28 [theme]="theme"29 [oddEvenRowBackground]="true"30 ></simple-table>31 `,32})33export class DynamicRowLoadingDemoComponent {34 @Input() height: string | number = "400px";35 @Input() theme?: Theme;3637 headers: AngularColumnDef<DynamicTreeRow>[] = dynamicRowLoadingConfig.headers;38 rows: DynamicRegion[] = generateInitialRegions();39 readonly grouping = ["stores", "products"];40 readonly getRowId = ({ row }: GetRowIdParams<DynamicTreeRow>) => row.id;4142 handleRowExpand = async ({43 row,44 depth,45 groupingKey,46 isExpanded,47 setLoading,48 setError,49 setEmpty,50 rowIndexPath,51 }: OnRowGroupExpandProps<DynamicTreeRow>) => {52 if (!isExpanded) return;53 if (groupingKey === "stores" && row.type === "region" && row.stores && row.stores.length > 0) {54 return;55 }56 if (groupingKey === "products" && row.type === "store" && row.products && row.products.length > 0) {57 return;58 }5960 try {61 if (depth === 0 && groupingKey === "stores" && row.type === "region") {62 setLoading(true);63 const stores = await fetchStoresForRegion(row.id);64 setLoading(false);65 if (stores.length === 0) {66 setEmpty(true, "No stores found");67 return;68 }69 const newRows = [...this.rows];70 newRows[rowIndexPath[0]].stores = stores;71 this.rows = newRows;72 } else if (depth === 1 && groupingKey === "products" && row.type === "store") {73 setLoading(true);74 const products = await fetchProductsForStore(row.id);75 setLoading(false);76 if (products.length === 0) {77 setEmpty(true, "No products found");78 return;79 }80 const newRows = [...this.rows];81 const region = newRows[rowIndexPath[0]];82 if (region.stores && region.stores[rowIndexPath[1]]) {83 region.stores[rowIndexPath[1]].products = products;84 }85 this.rows = newRows;86 }87 } catch (error) {88 setLoading(false);89 setError(error instanceof Error ? error.message : "Failed to load data");90 }91 };92}939495// dynamic-row-loading.demo-data.ts96// Self-contained demo table setup for this example.97import type { AngularColumnDef, ValueFormatterProps } from "@simple-table/angular";9899export interface DynamicRegion {100 id: string;101 name: string;102 type: "region";103 totalSales: number;104 totalRevenue: number;105 activeStores: number;106 avgRating: string;107 lastUpdate: string;108 stores?: DynamicStore[];109}110111export interface DynamicStore {112 id: string;113 name: string;114 type: "store";115 totalSales: number;116 totalRevenue: number;117 activeStores?: number;118 avgRating: string;119 lastUpdate: string;120 products?: DynamicProduct[];121}122123export interface DynamicProduct {124 id: string;125 name: string;126 type: "product";127 totalSales: number;128 totalRevenue: number;129 activeStores?: number;130 avgRating: string;131 lastUpdate: string;132}133134/** Row union across grouping depths (region → store → product). */135export type DynamicTreeRow = DynamicRegion | DynamicStore | DynamicProduct;136137export const dynamicRowLoadingHeaders: AngularColumnDef<DynamicTreeRow>[] = [138 { accessor: "name", label: "Name", width: 280, expandable: true, type: "string", pinned: "left" },139 { accessor: "type", label: "Type", width: 100, type: "string" },140 {141 accessor: "totalSales", label: "Total Sales", width: 120, type: "number", align: "right",142 aggregation: { type: "sum" },143 valueFormatter: ({ value }: ValueFormatterProps<DynamicTreeRow>) => typeof value !== "number" ? "—" : value.toLocaleString(),144 },145 {146 accessor: "totalRevenue", label: "Revenue", width: 140, type: "number", align: "right",147 aggregation: { type: "sum" },148 valueFormatter: ({ value }: ValueFormatterProps<DynamicTreeRow>) => typeof value !== "number" ? "—" : `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,149 },150 {151 accessor: "activeStores", label: "Stores", width: 100, type: "number", align: "right",152 valueFormatter: ({ value }: ValueFormatterProps<DynamicTreeRow>) => typeof value !== "number" ? "—" : value.toLocaleString(),153 },154 { accessor: "avgRating", label: "Avg Rating", width: 120, type: "string", align: "center" },155 { accessor: "lastUpdate", label: "Last Updated", width: 130, type: "date" },156];157158const REGION_NAMES = [159 "North America - East", "North America - West", "Europe - North", "Europe - South",160 "Asia Pacific - East", "Asia Pacific - Southeast", "Middle East",161 "Latin America - North", "Latin America - South", "Africa - North", "Africa - South", "Oceania",162];163164const STORE_NAMES = [165 "Manhattan Flagship", "Brooklyn Heights", "Boston Downtown", "Miami Beach",166 "Los Angeles Beverly Hills", "San Francisco Union Square", "Seattle Downtown", "Portland Pearl District",167 "London Oxford Street", "Stockholm Gamla Stan", "Copenhagen Strøget", "Amsterdam Central",168 "Paris Champs-Élysées", "Madrid Gran Vía", "Rome Via del Corso", "Barcelona La Rambla",169 "Tokyo Shibuya", "Shanghai Nanjing Road", "Hong Kong Central", "Seoul Gangnam",170 "Singapore Orchard", "Bangkok Siam", "Kuala Lumpur Bukit Bintang", "Jakarta Grand Indonesia",171 "Dubai Mall", "Abu Dhabi Marina", "Riyadh Kingdom Centre",172 "Mexico City Reforma", "Monterrey Valle", "Guadalajara Centro",173 "São Paulo Paulista", "Buenos Aires Palermo", "Santiago Providencia",174 "Cairo City Stars", "Casablanca Morocco Mall", "Tunis Centre Urbain",175 "Johannesburg Sandton", "Cape Town V&A Waterfront",176 "Sydney Pitt Street", "Melbourne Bourke Street", "Auckland Queen Street",177];178179const PRODUCT_NAMES = [180 "Wireless Headphones Pro", "Smart Watch Elite", "USB-C Hub Deluxe", "Mechanical Keyboard RGB",181 "Ergonomic Mouse", "Webcam 4K", "Portable SSD 2TB", "Wireless Charger Pad",182 "Phone Stand Aluminum", "Bluetooth Speaker Mini", "Laptop Stand Pro", "Cable Organizer Set",183 "Gaming Mouse Elite", "Noise Cancelling Headset", "RGB Desk Mat XL", "Wireless Presenter",184 "Document Camera", "Smart Pen Digital", "Monitor Arm Dual", "Docking Station Pro",185 "Microphone USB Studio", "Tablet Stand Adjustable", "HDMI Switch 4K", "Laptop Cooling Pad",186 "Blue Light Blocking Glasses", "Anti-Glare Screen Protector", "Laptop Privacy Filter",187 "Wireless Charging Pad Trio", "MagSafe Car Mount", "Charging Cable Braided 10ft",188 "Ergonomic Vertical Mouse", "Trackball Mouse Wireless", "Gaming Mouse Pad XXL",189 "Keyboard Wrist Rest", "Monitor Privacy Filter", "Laptop Sleeve Premium",190 "Desktop Mic Arm", "Cable Management Box", "USB Hub 7-Port", "Ergonomic Chair Cushion",191 "Footrest Adjustable", "Desk Lamp LED Smart", "Portable Monitor 15.6", "Screen Cleaning Kit",192 "Desk Organizer Bamboo", "Wireless Trackpad", "Numeric Keypad Wireless",193 "Presentation Clicker", "Gaming Controller Pro", "Racing Wheel Set",194];195196const seededRandom = (seed: string) => {197 let hash = 0;198 for (let i = 0; i < seed.length; i++) {199 hash = (hash << 5) - hash + seed.charCodeAt(i);200 hash = hash & hash;201 }202 const x = Math.sin(hash) * 10000;203 return x - Math.floor(x);204};205206const getRandomInt = (seed: string, min: number, max: number) =>207 Math.floor(seededRandom(seed) * (max - min + 1)) + min;208209const getRandomRating = (seed: string) => (4.0 + seededRandom(seed + "rating") * 1.0).toFixed(1);210211const getRandomDate = (seed: string) => {212 const daysAgo = getRandomInt(seed + "date", 0, 5);213 const date = new Date();214 date.setDate(date.getDate() - daysAgo);215 return date.toISOString().split("T")[0];216};217218const generateStoresForRegion = (regionId: string): DynamicStore[] => {219 const regionIndex = parseInt(regionId.split("-")[1]);220 const numStores = getRandomInt(regionId, 3, 4);221 const startIndex = (regionIndex - 1) * 3;222 return Array.from({ length: numStores }, (_, i) => {223 const storeId = `STORE-${regionIndex}${String(i + 1).padStart(2, "0")}`;224 const storeIndex = startIndex + i;225 const totalSales = getRandomInt(storeId, 10000, 25000);226 const avgPrice = getRandomInt(storeId + "price", 25, 35);227 return {228 id: storeId,229 name: STORE_NAMES[storeIndex % STORE_NAMES.length],230 type: "store",231 totalSales,232 totalRevenue: totalSales * avgPrice,233 avgRating: getRandomRating(storeId),234 lastUpdate: getRandomDate(storeId),235 };236 });237};238239const generateProductsForStore = (storeId: string): DynamicProduct[] => {240 const numProducts = getRandomInt(storeId, 3, 5);241 const storeNumber = parseInt(storeId.split("-")[1]);242 const startIndex = storeNumber * 3;243 return Array.from({ length: numProducts }, (_, i) => {244 const productId = `PROD-${storeId.split("-")[1]}-${i + 1}`;245 const totalSales = getRandomInt(productId, 2000, 8000);246 const avgPrice = getRandomInt(productId + "price", 20, 40);247 return {248 id: productId,249 name: PRODUCT_NAMES[(startIndex + i) % PRODUCT_NAMES.length],250 type: "product",251 totalSales,252 totalRevenue: totalSales * avgPrice,253 avgRating: getRandomRating(productId),254 lastUpdate: getRandomDate(productId),255 };256 });257};258259const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));260261export const fetchStoresForRegion = async (regionId: string): Promise<DynamicStore[]> => {262 await delay(1500);263 return generateStoresForRegion(regionId);264};265266export const fetchProductsForStore = async (storeId: string): Promise<DynamicProduct[]> => {267 await delay(1000);268 return generateProductsForStore(storeId);269};270271export const generateInitialRegions = (): DynamicRegion[] => {272 return REGION_NAMES.map((name, index) => {273 const regionId = `REG-${index + 1}`;274 const stores = generateStoresForRegion(regionId);275 const totalSales = stores.reduce((sum, s) => sum + s.totalSales, 0);276 const totalRevenue = stores.reduce((sum, s) => sum + s.totalRevenue, 0);277 const avgRating = (stores.reduce((sum, s) => sum + parseFloat(s.avgRating), 0) / stores.length).toFixed(1);278 return {279 id: regionId,280 name,281 type: "region",282 totalSales,283 totalRevenue,284 activeStores: getRandomInt(regionId, 3, 4),285 avgRating,286 lastUpdate: getRandomDate(regionId),287 };288 });289};290291export const dynamicRowLoadingConfig = {292 headers: dynamicRowLoadingHeaders,293 tableProps: {294 rowGrouping: ["stores", "products"],295 expandAll: false,296 columnResizing: true,297 selectableCells: true,298 oddEvenRowBackground: true,299 enableColumnEditor: true,300 },301};302
Vue SFC
Copy
1<template>2 <SimpleTable3 :column-resizing="dynamicRowLoadingConfig.tableProps.columnResizing"4 :columns="dynamicRowLoadingConfig.headers"5 :enable-column-editor="dynamicRowLoadingConfig.tableProps.enableColumnEditor"6 :expand-all="dynamicRowLoadingConfig.tableProps.expandAll"7 :height="height"8 :on-row-group-expand="handleRowExpand"9 :row-grouping="dynamicRowLoadingConfig.tableProps.rowGrouping"10 :get-row-id="getRowId"11 :rows="rows"12 :selectable-cells="dynamicRowLoadingConfig.tableProps.selectableCells"13 :theme="theme"14 :odd-even-row-background="dynamicRowLoadingConfig.tableProps.oddEvenRowBackground"15 />16</template>1718<script setup lang="ts">19import { ref } from "vue";20import { SimpleTable } from "@simple-table/vue";21import type { Theme, OnRowGroupExpandProps, GetRowIdParams } from "@simple-table/vue";22import {23 dynamicRowLoadingConfig,24 generateInitialRegions,25 fetchStoresForRegion,26 fetchProductsForStore,27} from "./dynamic-row-loading.demo-data";28import type { DynamicRegion, DynamicTreeRow } from "./dynamic-row-loading.demo-data";29import "@simple-table/vue/styles.css";3031withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), { height: "400px" });3233const rows = ref<DynamicRegion[]>(generateInitialRegions());3435const getRowId = ({ row }: GetRowIdParams<DynamicTreeRow>) => row.id;3637async function handleRowExpand({38 row,39 depth,40 groupingKey,41 isExpanded,42 setLoading,43 setError,44 setEmpty,45 rowIndexPath,46}: OnRowGroupExpandProps<DynamicTreeRow>) {47 if (!isExpanded) return;48 if (groupingKey && row[groupingKey as keyof DynamicTreeRow] != null) {49 const nested = row[groupingKey as keyof DynamicTreeRow];50 if (Array.isArray(nested) && nested.length > 0) return;51 }5253 try {54 if (depth === 0 && groupingKey === "stores" && row.type === "region") {55 setLoading(true);56 const stores = await fetchStoresForRegion(row.id);57 setLoading(false);58 if (stores.length === 0) {59 setEmpty(true, "No stores found for this region");60 return;61 }62 const newRows = [...rows.value];63 newRows[rowIndexPath[0]].stores = stores;64 rows.value = newRows;65 } else if (depth === 1 && groupingKey === "products" && row.type === "store") {66 setLoading(true);67 const products = await fetchProductsForStore(row.id);68 setLoading(false);69 if (products.length === 0) {70 setEmpty(true, "No products found for this store");71 return;72 }73 const newRows = [...rows.value];74 const region = newRows[rowIndexPath[0]];75 if (region.stores && region.stores[rowIndexPath[1]]) {76 region.stores[rowIndexPath[1]].products = products;77 }78 rows.value = newRows;79 }80 } catch (error) {81 setLoading(false);82 setError(error instanceof Error ? error.message : "Failed to load data");83 }84}85</script>
Svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, OnRowGroupExpandProps, GetRowIdParams } from "@simple-table/svelte";4 import {5 dynamicRowLoadingConfig,6 generateInitialRegions,7 fetchStoresForRegion,8 fetchProductsForStore,9 } from "./dynamic-row-loading.demo-data";10 import type { DynamicRegion, DynamicTreeRow } from "./dynamic-row-loading.demo-data";11 import "@simple-table/svelte/styles.css";1213 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();1415 let rows = $state<DynamicRegion[]>(generateInitialRegions());16 const getRowId = ({ row }: GetRowIdParams<DynamicTreeRow>) => row.id;1718 async function handleRowExpand({19 row,20 depth,21 groupingKey,22 isExpanded,23 setLoading,24 setError,25 setEmpty,26 rowIndexPath,27 }: OnRowGroupExpandProps<DynamicTreeRow>) {28 if (!isExpanded) return;29 if (groupingKey === "stores" && row.type === "region" && row.stores && row.stores.length > 0) {30 return;31 }32 if (groupingKey === "products" && row.type === "store" && row.products && row.products.length > 0) {33 return;34 }3536 try {37 if (depth === 0 && groupingKey === "stores" && row.type === "region") {38 setLoading(true);39 const stores = await fetchStoresForRegion(row.id);40 setLoading(false);41 if (stores.length === 0) {42 setEmpty(true, "No stores found");43 return;44 }45 const newRows = [...rows];46 newRows[rowIndexPath[0]] = { ...newRows[rowIndexPath[0]], stores };47 rows = newRows;48 } else if (depth === 1 && groupingKey === "products" && row.type === "store") {49 setLoading(true);50 const products = await fetchProductsForStore(row.id);51 setLoading(false);52 if (products.length === 0) {53 setEmpty(true, "No products found");54 return;55 }56 const newRows = [...rows];57 const region = newRows[rowIndexPath[0]];58 if (region.stores && region.stores[rowIndexPath[1]]) {59 region.stores[rowIndexPath[1]] = { ...region.stores[rowIndexPath[1]], products };60 }61 rows = newRows;62 }63 } catch (error) {64 setLoading(false);65 setError(error instanceof Error ? error.message : "Failed to load data");66 }67 }68</script>6970<SimpleTable71 columnResizing={dynamicRowLoadingConfig.tableProps.columnResizing}72 columns={dynamicRowLoadingConfig.headers}73 enableColumnEditor={dynamicRowLoadingConfig.tableProps.enableColumnEditor}74 expandAll={dynamicRowLoadingConfig.tableProps.expandAll}75 {getRowId}76 {height}77 onRowGroupExpand={handleRowExpand}78 rowGrouping={dynamicRowLoadingConfig.tableProps.rowGrouping}79 rows={rows}80 selectableCells={dynamicRowLoadingConfig.tableProps.selectableCells}81 {theme}82 oddEvenRowBackground={dynamicRowLoadingConfig.tableProps.oddEvenRowBackground}83/>
Solid TSX
Copy
1import { createSignal } from "solid-js";2import { SimpleTable } from "@simple-table/solid";3import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid";4import {5 dynamicRowLoadingConfig,6 generateInitialRegions,7 fetchStoresForRegion,8 fetchProductsForStore,9} from "./dynamic-row-loading.demo-data";10import type { DynamicRegion, DynamicTreeRow } from "./dynamic-row-loading.demo-data";11import "@simple-table/solid/styles.css";1213export default function DynamicRowLoadingDemo(props: { height?: string | number; theme?: Theme }) {14 const [rows, setRows] = createSignal<DynamicRegion[]>(generateInitialRegions());1516 const handleRowExpand = async ({17 row,18 depth,19 groupingKey,20 isExpanded,21 setLoading,22 setError,23 setEmpty,24 rowIndexPath,25 }: OnRowGroupExpandProps<DynamicTreeRow>) => {26 if (!isExpanded) return;27 if (groupingKey && row[groupingKey as keyof DynamicTreeRow] != null) {28 const nested = row[groupingKey as keyof DynamicTreeRow];29 if (Array.isArray(nested) && nested.length > 0) return;30 }3132 try {33 if (depth === 0 && groupingKey === "stores" && row.type === "region") {34 setLoading(true);35 const stores = await fetchStoresForRegion(row.id);36 setLoading(false);37 if (stores.length === 0) {38 setEmpty(true, "No stores found for this region");39 return;40 }41 setRows((prev) => {42 const newRows = [...prev];43 newRows[rowIndexPath[0]].stores = stores;44 return newRows;45 });46 } else if (depth === 1 && groupingKey === "products" && row.type === "store") {47 setLoading(true);48 const products = await fetchProductsForStore(row.id);49 setLoading(false);50 if (products.length === 0) {51 setEmpty(true, "No products found for this store");52 return;53 }54 setRows((prev) => {55 const newRows = [...prev];56 const region = newRows[rowIndexPath[0]];57 if (region.stores && region.stores[rowIndexPath[1]]) {58 region.stores[rowIndexPath[1]].products = products;59 }60 return newRows;61 });62 }63 } catch (error) {64 setLoading(false);65 setError(error instanceof Error ? error.message : "Failed to load data");66 }67 };6869 return (70 <SimpleTable71 columnResizing={dynamicRowLoadingConfig.tableProps.columnResizing}72 columns={dynamicRowLoadingConfig.headers}73 enableColumnEditor={dynamicRowLoadingConfig.tableProps.enableColumnEditor}74 expandAll={dynamicRowLoadingConfig.tableProps.expandAll}75 height={props.height ?? "400px"}76 onRowGroupExpand={handleRowExpand}77 rowGrouping={dynamicRowLoadingConfig.tableProps.rowGrouping}78 getRowId={({ row }) => row.id}79 rows={rows()}80 selectableCells={dynamicRowLoadingConfig.tableProps.selectableCells}81 theme={props.theme}82 oddEvenRowBackground={dynamicRowLoadingConfig.tableProps.oddEvenRowBackground}83 />84 );85}
TypeScriptDynamicRowLoadingDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme, OnRowGroupExpandProps, GetRowIdParams } from "simple-table-core";3import {4 dynamicRowLoadingConfig,5 generateInitialRegions,6 fetchStoresForRegion,7 fetchProductsForStore,8} from "./dynamic-row-loading.demo-data";9import type { DynamicRegion, DynamicTreeRow } from "./dynamic-row-loading.demo-data";10import "simple-table-core/styles.css";1112const getRowId = ({ row }: GetRowIdParams<DynamicTreeRow>) => row.id;1314export function renderDynamicRowLoadingDemo(15 container: HTMLElement,16 options?: { height?: string | number; theme?: Theme },17): SimpleTableVanilla<DynamicTreeRow> {18 let rows: DynamicRegion[] = generateInitialRegions();1920 const handleRowExpand = async ({21 row,22 depth,23 groupingKey,24 isExpanded,25 setLoading,26 setError,27 setEmpty,28 rowIndexPath,29 }: OnRowGroupExpandProps<DynamicTreeRow>) => {30 if (!isExpanded) return;31 if (groupingKey === "stores" && row.type === "region" && row.stores && row.stores.length > 0) {32 return;33 }34 if (groupingKey === "products" && row.type === "store" && row.products && row.products.length > 0) {35 return;36 }3738 try {39 if (depth === 0 && groupingKey === "stores" && row.type === "region") {40 setLoading(true);41 const stores = await fetchStoresForRegion(row.id);42 setLoading(false);43 if (stores.length === 0) {44 setEmpty(true, "No stores found for this region");45 return;46 }47 rows[rowIndexPath[0]].stores = stores;48 table.update({ rows: [...rows] });49 } else if (depth === 1 && groupingKey === "products" && row.type === "store") {50 setLoading(true);51 const products = await fetchProductsForStore(row.id);52 setLoading(false);53 if (products.length === 0) {54 setEmpty(true, "No products found for this store");55 return;56 }57 const region = rows[rowIndexPath[0]];58 if (region.stores && region.stores[rowIndexPath[1]]) {59 region.stores[rowIndexPath[1]].products = products;60 }61 table.update({ rows: [...rows] });62 }63 } catch (error) {64 setLoading(false);65 setError(error instanceof Error ? error.message : "Failed to load data");66 }67 };6869 const table = new SimpleTableVanilla<DynamicTreeRow>(container, {70 columnResizing: dynamicRowLoadingConfig.tableProps.columnResizing,71 columns: dynamicRowLoadingConfig.headers,72 enableColumnEditor: dynamicRowLoadingConfig.tableProps.enableColumnEditor,73 expandAll: dynamicRowLoadingConfig.tableProps.expandAll,74 height: options?.height ?? "400px",75 onRowGroupExpand: handleRowExpand,76 rowGrouping: dynamicRowLoadingConfig.tableProps.rowGrouping,77 getRowId,78 rows,79 selectableCells: dynamicRowLoadingConfig.tableProps.selectableCells,80 theme: options?.theme,81 oddEvenRowBackground: dynamicRowLoadingConfig.tableProps.oddEvenRowBackground,82 });8384 return table;85}868788// dynamic-row-loading.demo-data.ts89// Self-contained demo table setup for this example.90import type { ColumnDef } from "simple-table-core";9192export interface DynamicRegion {93 id: string;94 name: string;95 type: "region";96 totalSales: number;97 totalRevenue: number;98 activeStores: number;99 avgRating: string;100 lastUpdate: string;101 stores?: DynamicStore[];102}103104export interface DynamicStore {105 id: string;106 name: string;107 type: "store";108 totalSales: number;109 totalRevenue: number;110 activeStores?: number;111 avgRating: string;112 lastUpdate: string;113 products?: DynamicProduct[];114}115116export interface DynamicProduct {117 id: string;118 name: string;119 type: "product";120 totalSales: number;121 totalRevenue: number;122 activeStores?: number;123 avgRating: string;124 lastUpdate: string;125}126127/** Row union across grouping depths (region → store → product). */128export type DynamicTreeRow = DynamicRegion | DynamicStore | DynamicProduct;129130export const dynamicRowLoadingHeaders: ColumnDef<DynamicTreeRow>[] = [131 { accessor: "name", label: "Name", width: 280, expandable: true, type: "string", pinned: "left" },132 { accessor: "type", label: "Type", width: 100, type: "string" },133 {134 accessor: "totalSales", label: "Total Sales", width: 120, type: "number", align: "right",135 aggregation: { type: "sum" },136 valueFormatter: ({ value }) => typeof value !== "number" ? "—" : value.toLocaleString(),137 },138 {139 accessor: "totalRevenue", label: "Revenue", width: 140, type: "number", align: "right",140 aggregation: { type: "sum" },141 valueFormatter: ({ value }) => typeof value !== "number" ? "—" : `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,142 },143 {144 accessor: "activeStores", label: "Stores", width: 100, type: "number", align: "right",145 valueFormatter: ({ value }) => typeof value !== "number" ? "—" : value.toLocaleString(),146 },147 { accessor: "avgRating", label: "Avg Rating", width: 120, type: "string", align: "center" },148 { accessor: "lastUpdate", label: "Last Updated", width: 130, type: "date" },149];150151const REGION_NAMES = [152 "North America - East", "North America - West", "Europe - North", "Europe - South",153 "Asia Pacific - East", "Asia Pacific - Southeast", "Middle East",154 "Latin America - North", "Latin America - South", "Africa - North", "Africa - South", "Oceania",155];156157const STORE_NAMES = [158 "Manhattan Flagship", "Brooklyn Heights", "Boston Downtown", "Miami Beach",159 "Los Angeles Beverly Hills", "San Francisco Union Square", "Seattle Downtown", "Portland Pearl District",160 "London Oxford Street", "Stockholm Gamla Stan", "Copenhagen Strøget", "Amsterdam Central",161 "Paris Champs-Élysées", "Madrid Gran Vía", "Rome Via del Corso", "Barcelona La Rambla",162 "Tokyo Shibuya", "Shanghai Nanjing Road", "Hong Kong Central", "Seoul Gangnam",163 "Singapore Orchard", "Bangkok Siam", "Kuala Lumpur Bukit Bintang", "Jakarta Grand Indonesia",164 "Dubai Mall", "Abu Dhabi Marina", "Riyadh Kingdom Centre",165 "Mexico City Reforma", "Monterrey Valle", "Guadalajara Centro",166 "São Paulo Paulista", "Buenos Aires Palermo", "Santiago Providencia",167 "Cairo City Stars", "Casablanca Morocco Mall", "Tunis Centre Urbain",168 "Johannesburg Sandton", "Cape Town V&A Waterfront",169 "Sydney Pitt Street", "Melbourne Bourke Street", "Auckland Queen Street",170];171172const PRODUCT_NAMES = [173 "Wireless Headphones Pro", "Smart Watch Elite", "USB-C Hub Deluxe", "Mechanical Keyboard RGB",174 "Ergonomic Mouse", "Webcam 4K", "Portable SSD 2TB", "Wireless Charger Pad",175 "Phone Stand Aluminum", "Bluetooth Speaker Mini", "Laptop Stand Pro", "Cable Organizer Set",176 "Gaming Mouse Elite", "Noise Cancelling Headset", "RGB Desk Mat XL", "Wireless Presenter",177 "Document Camera", "Smart Pen Digital", "Monitor Arm Dual", "Docking Station Pro",178 "Microphone USB Studio", "Tablet Stand Adjustable", "HDMI Switch 4K", "Laptop Cooling Pad",179 "Blue Light Blocking Glasses", "Anti-Glare Screen Protector", "Laptop Privacy Filter",180 "Wireless Charging Pad Trio", "MagSafe Car Mount", "Charging Cable Braided 10ft",181 "Ergonomic Vertical Mouse", "Trackball Mouse Wireless", "Gaming Mouse Pad XXL",182 "Keyboard Wrist Rest", "Monitor Privacy Filter", "Laptop Sleeve Premium",183 "Desktop Mic Arm", "Cable Management Box", "USB Hub 7-Port", "Ergonomic Chair Cushion",184 "Footrest Adjustable", "Desk Lamp LED Smart", "Portable Monitor 15.6", "Screen Cleaning Kit",185 "Desk Organizer Bamboo", "Wireless Trackpad", "Numeric Keypad Wireless",186 "Presentation Clicker", "Gaming Controller Pro", "Racing Wheel Set",187];188189const seededRandom = (seed: string) => {190 let hash = 0;191 for (let i = 0; i < seed.length; i++) {192 hash = (hash << 5) - hash + seed.charCodeAt(i);193 hash = hash & hash;194 }195 const x = Math.sin(hash) * 10000;196 return x - Math.floor(x);197};198199const getRandomInt = (seed: string, min: number, max: number) =>200 Math.floor(seededRandom(seed) * (max - min + 1)) + min;201202const getRandomRating = (seed: string) => (4.0 + seededRandom(seed + "rating") * 1.0).toFixed(1);203204const getRandomDate = (seed: string) => {205 const daysAgo = getRandomInt(seed + "date", 0, 5);206 const date = new Date();207 date.setDate(date.getDate() - daysAgo);208 return date.toISOString().split("T")[0];209};210211const generateStoresForRegion = (regionId: string): DynamicStore[] => {212 const regionIndex = parseInt(regionId.split("-")[1]);213 const numStores = getRandomInt(regionId, 3, 4);214 const startIndex = (regionIndex - 1) * 3;215 return Array.from({ length: numStores }, (_, i) => {216 const storeId = `STORE-${regionIndex}${String(i + 1).padStart(2, "0")}`;217 const storeIndex = startIndex + i;218 const totalSales = getRandomInt(storeId, 10000, 25000);219 const avgPrice = getRandomInt(storeId + "price", 25, 35);220 return {221 id: storeId,222 name: STORE_NAMES[storeIndex % STORE_NAMES.length],223 type: "store",224 totalSales,225 totalRevenue: totalSales * avgPrice,226 avgRating: getRandomRating(storeId),227 lastUpdate: getRandomDate(storeId),228 };229 });230};231232const generateProductsForStore = (storeId: string): DynamicProduct[] => {233 const numProducts = getRandomInt(storeId, 3, 5);234 const storeNumber = parseInt(storeId.split("-")[1]);235 const startIndex = storeNumber * 3;236 return Array.from({ length: numProducts }, (_, i) => {237 const productId = `PROD-${storeId.split("-")[1]}-${i + 1}`;238 const totalSales = getRandomInt(productId, 2000, 8000);239 const avgPrice = getRandomInt(productId + "price", 20, 40);240 return {241 id: productId,242 name: PRODUCT_NAMES[(startIndex + i) % PRODUCT_NAMES.length],243 type: "product",244 totalSales,245 totalRevenue: totalSales * avgPrice,246 avgRating: getRandomRating(productId),247 lastUpdate: getRandomDate(productId),248 };249 });250};251252const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));253254export const fetchStoresForRegion = async (regionId: string): Promise<DynamicStore[]> => {255 await delay(1500);256 return generateStoresForRegion(regionId);257};258259export const fetchProductsForStore = async (storeId: string): Promise<DynamicProduct[]> => {260 await delay(1000);261 return generateProductsForStore(storeId);262};263264export const generateInitialRegions = (): DynamicRegion[] => {265 return REGION_NAMES.map((name, index) => {266 const regionId = `REG-${index + 1}`;267 const stores = generateStoresForRegion(regionId);268 const totalSales = stores.reduce((sum, s) => sum + s.totalSales, 0);269 const totalRevenue = stores.reduce((sum, s) => sum + s.totalRevenue, 0);270 const avgRating = (stores.reduce((sum, s) => sum + parseFloat(s.avgRating), 0) / stores.length).toFixed(1);271 return {272 id: regionId,273 name,274 type: "region",275 totalSales,276 totalRevenue,277 activeStores: getRandomInt(regionId, 3, 4),278 avgRating,279 lastUpdate: getRandomDate(regionId),280 };281 });282};283284export const dynamicRowLoadingConfig = {285 headers: dynamicRowLoadingHeaders,286 tableProps: {287 rowGrouping: ["stores", "products"],288 expandAll: false,289 columnResizing: true,290 selectableCells: true,291 oddEvenRowBackground: true,292 enableColumnEditor: true,293 },294};295
Props
Row Grouping Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
ColumnDef.expandableboolean | Optional | Shows the expand/collapse control on this column for hierarchical rows. | |
rowGroupingstring[] | Optional | Nested array property names that define hierarchy levels, in depth order. | |
expandAllboolean | Optional | When true, grouped rows start expanded. | |
getRowId(props: { row: Row }) => string | number | null | undefined | Optional | Stable row id so expansion survives sort and data updates. | |
onRowGroupExpand | Optional | Fires on expand/collapse. Includes setLoading, setError, setEmpty, and rowIndexPath for lazy loading. | |
canExpandRowGroup(row: Row) => boolean | Optional | Return false to disable expand for a specific row. | |
enableStickyParentsboolean | Optional | Beta: sticky parent rows while scrolling through children. Default false. | |
loadingStateRendererstring | ReactNode | Optional | Custom content for loading rows (from setLoading). Default skeleton if omitted. | |
errorStateRendererstring | ReactNode | Optional | Custom content for error rows (from setError). | |
emptyStateRendererstring | ReactNode | Optional | Custom content for empty child rows (from setEmpty). |