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)}/>Vue SFCCopy<SimpleTable:columns="columns":rows="rows":row-grouping="['divisions', 'departments']":get-row-id="(ctx) => String(ctx.row.id)"/>AngularCopy<simple-table[columns]="columns"[rows]="rows"[rowGrouping]="['divisions', 'departments']"[getRowId]="getRowId"></simple-table>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/>
Vue SFC
Copy
<SimpleTable:columns="columns":rows="rows":row-grouping="['divisions', 'departments']":get-row-id="(ctx) => String(ctx.row.id)":expand-all="true"/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"[rowGrouping]="['divisions', 'departments']"[getRowId]="getRowId"[expandAll]="true"></simple-table>
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/>
Vue SFC
Copy
<SimpleTable:columns="columns":rows="rows":row-grouping="['divisions', 'departments']":get-row-id="(ctx) => String(ctx.row.id)":enable-sticky-parents="true"/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"[rowGrouping]="['divisions', 'departments']"[getRowId]="getRowId"[enableStickyParents]="true"></simple-table>
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);}}}/>
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"/>
Angular
Copy
<simple-table[columns]="columns"[rows]="rows"[rowGrouping]="['stores', 'products']"[getRowId]="getRowId"[onRowGroupExpand]="handleRowGroupExpand"></simple-table>
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);
Vue SFC
Copy
tableRef.value?.expandAll();tableRef.value?.collapseAll();tableRef.value?.expandDepth(0);tableRef.value?.setExpandedDepths(new Set([0, 1]));tableRef.value?.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);
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 } 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 const,15});1617const RowGroupingDemo = ({18 height = "400px",19 theme,20}: {21 height?: string | number;22 theme?: Theme;23}) => {24 const tableRef = useRef<TableAPI>(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={rowGroupingConfig.tableProps.getRowId}61 columnResizing62 height={height}63 theme={theme}64 />65 </div>66 );67};6869export default RowGroupingDemo;
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="rowGroupingConfig.tableProps.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";import type { Theme, TableAPI } from "@simple-table/vue";28import { rowGroupingConfig } from "./row-grouping.demo-data";29import "@simple-table/vue/styles.css";3031withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {32 height: "400px",33});3435const tableRef = ref<{ getAPI: () => TableAPI | null } | null>(null);3637function btnStyle(color: string) {38 return {39 padding: "6px 12px",40 background: color,41 color: "white",42 border: "none",43 borderRadius: "4px",44 cursor: "pointer",45 fontSize: "12px",46 fontWeight: "500",47 };48}4950function expandAll() { tableRef.value?.getAPI()?.expandAll(); }51function collapseAll() { tableRef.value?.getAPI()?.collapseAll(); }52function onlyDivisions() { tableRef.value?.getAPI()?.collapseAll(); tableRef.value?.getAPI()?.expandDepth(0); }53function divisionsAndDepts() { tableRef.value?.getAPI()?.setExpandedDepths(new Set([0, 1])); }54function toggleDivisions() { tableRef.value?.getAPI()?.toggleDepth(0); }55</script>
Angularrow-grouping-demo.component.ts
Copy
1import { Component, Input, ViewChild } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, Row, Theme } from "@simple-table/angular";3import { rowGroupingConfig } from "./row-grouping.demo-data";4import "@simple-table/angular/styles.css";56@Component({7 selector: "row-grouping-demo",8 standalone: true,9 imports: [SimpleTableComponent],10 template: `11 <div style="display: flex; flex-direction: column; gap: 12px">12 <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center">13 <span style="font-size: 13px; font-weight: 600; margin-right: 8px">Control Expansion:</span>14 <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>15 <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>16 <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>17 <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>18 <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>19 </div>20 <simple-table21 #simpleTable22 [rows]="rows"23 [columns]="headers"24 [height]="height"25 [theme]="theme"26 [rowGrouping]="grouping"27 [enableStickyParents]="true"28 [getRowId]="getRowId"29 [columnResizing]="true"30 ></simple-table>31 </div>32 `,33})34export class RowGroupingDemoComponent {35 @ViewChild("simpleTable") tableRef!: SimpleTableComponent;36 @Input() height: string | number = "400px";37 @Input() theme?: Theme;3839 readonly rows: Row[] = rowGroupingConfig.rows;40 readonly headers: AngularColumnDef[] = rowGroupingConfig.headers;41 readonly grouping = rowGroupingConfig.tableProps.rowGrouping;42 readonly getRowId = rowGroupingConfig.tableProps.getRowId;4344 expandAll() { this.tableRef.getAPI()?.expandAll(); }45 collapseAll() { this.tableRef.getAPI()?.collapseAll(); }46 onlyDivisions() { this.tableRef.getAPI()?.collapseAll(); this.tableRef.getAPI()?.expandDepth(0); }47 divisionsAndDepts() { this.tableRef.getAPI()?.setExpandedDepths(new Set([0, 1])); }48 toggleDivisions() { this.tableRef.getAPI()?.toggleDepth(0); }49}505152// row-grouping.demo-data.ts53// Self-contained demo table setup for this example.54import type { AngularColumnDef } from "@simple-table/angular";555657export const rowGroupingHeaders: AngularColumnDef[] = [58 { accessor: "organization", label: "Organization", width: 200, expandable: true, type: "string" },59 { accessor: "employees", label: "Employees", width: 100, type: "number" },60 { accessor: "budget", label: "Annual Budget", width: 140, type: "string" },61 { accessor: "performance", label: "Performance", width: 120, type: "string" },62 { accessor: "location", label: "Location", width: 130, type: "string" },63 { accessor: "growthRate", label: "Growth", width: 90, type: "string" },64 { accessor: "status", label: "Status", width: 110, type: "string" },65 { accessor: "established", label: "Est. Date", width: 110, type: "date" },66];6768export const rowGroupingData = [69 {70 id: "company-1",71 organization: "TechSolutions Inc.",72 employees: 137,73 budget: "$15.0M",74 performance: "Exceeding",75 location: "San Francisco",76 growthRate: "+9%",77 status: "Expanding",78 established: "2018-01-01",79 divisions: [80 {81 id: "div-100",82 organization: "Engineering Division",83 employees: 97,84 budget: "$10.6M",85 performance: "Exceeding",86 location: "Multiple",87 growthRate: "+11%",88 status: "Expanding",89 established: "2018-01-15",90 departments: [91 { id: "dept-1001", organization: "Frontend", employees: 28, budget: "$2.8M", performance: "Exceeding", location: "San Francisco", growthRate: "+12%", status: "Hiring", established: "2019-05-16" },92 { id: "dept-1002", organization: "Backend", employees: 32, budget: "$3.4M", performance: "Meeting", location: "Seattle", growthRate: "+8%", status: "Stable", established: "2018-03-22" },93 { id: "dept-1003", organization: "DevOps", employees: 15, budget: "$1.9M", performance: "Exceeding", location: "Remote", growthRate: "+15%", status: "Hiring", established: "2020-11-05" },94 { id: "dept-1004", organization: "Mobile", employees: 22, budget: "$2.5M", performance: "Meeting", location: "Austin", growthRate: "+10%", status: "Restructuring", established: "2019-08-12" },95 ],96 },97 {98 id: "div-101",99 organization: "Product Division",100 employees: 40,101 budget: "$4.4M",102 performance: "Meeting",103 location: "Multiple",104 growthRate: "+5%",105 status: "Stable",106 established: "2019-01-10",107 departments: [108 { id: "dept-1101", organization: "Design", employees: 17, budget: "$1.8M", performance: "Meeting", location: "Portland", growthRate: "+6%", status: "Stable", established: "2019-02-28" },109 { id: "dept-1102", organization: "Research", employees: 9, budget: "$1.4M", performance: "Below Target", location: "Boston", growthRate: "+3%", status: "Reviewing", established: "2020-07-15" },110 { id: "dept-1103", organization: "QA Testing", employees: 14, budget: "$1.2M", performance: "Meeting", location: "Chicago", growthRate: "+5%", status: "Stable", established: "2019-11-01" },111 ],112 },113 ],114 },115 {116 id: "company-2",117 organization: "HealthFirst Group",118 employees: 138,119 budget: "$22.4M",120 performance: "Meeting",121 location: "Boston",122 growthRate: "+8%",123 status: "Stable",124 established: "2010-01-01",125 divisions: [126 {127 id: "div-200",128 organization: "Hospital Operations",129 employees: 106,130 budget: "$13.1M",131 performance: "Meeting",132 location: "Multiple",133 growthRate: "+6%",134 status: "Expanding",135 established: "2010-01-05",136 departments: [137 { id: "dept-2001", organization: "Emergency", employees: 48, budget: "$5.2M", performance: "Meeting", location: "New York", growthRate: "+4%", status: "Critical", established: "2010-06-14" },138 { id: "dept-2002", organization: "Cardiology", employees: 32, budget: "$4.8M", performance: "Exceeding", location: "Chicago", growthRate: "+9%", status: "Expanding", established: "2012-03-25" },139 { id: "dept-2003", organization: "Pediatrics", employees: 26, budget: "$3.1M", performance: "Meeting", location: "Boston", growthRate: "+7%", status: "Stable", established: "2014-08-30" },140 ],141 },142 {143 id: "div-201",144 organization: "Research & Development",145 employees: 32,146 budget: "$9.3M",147 performance: "Exceeding",148 location: "Multiple",149 growthRate: "+15%",150 status: "Hiring",151 established: "2017-01-10",152 departments: [153 { id: "dept-2101", organization: "Clinical Trials", employees: 18, budget: "$4.2M", performance: "Exceeding", location: "San Diego", growthRate: "+12%", status: "Expanding", established: "2017-04-18" },154 { id: "dept-2102", organization: "Genomics", employees: 14, budget: "$5.1M", performance: "Exceeding", location: "Cambridge", growthRate: "+18%", status: "Hiring", established: "2019-02-21" },155 ],156 },157 ],158 },159 {160 id: "company-3",161 organization: "Global Finance",162 employees: 121,163 budget: "$15.5M",164 performance: "Meeting",165 location: "New York",166 growthRate: "+3%",167 status: "Restructuring",168 established: "2005-01-01",169 divisions: [170 {171 id: "div-300",172 organization: "Banking Operations",173 employees: 121,174 budget: "$15.5M",175 performance: "Meeting",176 location: "Multiple",177 growthRate: "+3%",178 status: "Stable",179 established: "2005-01-15",180 departments: [181 { id: "dept-3001", organization: "Retail Banking", employees: 56, budget: "$4.8M", performance: "Meeting", location: "New York", growthRate: "+2%", status: "Stable", established: "2005-11-08" },182 { id: "dept-3002", organization: "Investment", employees: 38, budget: "$7.2M", performance: "Exceeding", location: "Chicago", growthRate: "+11%", status: "Hiring", established: "2008-05-12" },183 { id: "dept-3003", organization: "Loans", employees: 27, budget: "$3.5M", performance: "Below Target", location: "Dallas", growthRate: "-3%", status: "Restructuring", established: "2010-03-17" },184 ],185 },186 ],187 },188 {189 id: "company-4",190 organization: "Apex University",191 employees: 115,192 budget: "$13.4M",193 performance: "Meeting",194 location: "Cambridge",195 growthRate: "+6%",196 status: "Stable",197 established: "1992-01-01",198 divisions: [199 {200 id: "div-400",201 organization: "Academic Departments",202 employees: 115,203 budget: "$13.4M",204 performance: "Meeting",205 location: "Multiple",206 growthRate: "+6%",207 status: "Stable",208 established: "1992-01-15",209 departments: [210 { id: "dept-4001", organization: "Computer Science", employees: 35, budget: "$3.8M", performance: "Meeting", location: "Boston", growthRate: "+8%", status: "Expanding", established: "1998-08-24" },211 { id: "dept-4002", organization: "Business", employees: 42, budget: "$4.5M", performance: "Exceeding", location: "Chicago", growthRate: "+6%", status: "Stable", established: "1995-09-15" },212 { id: "dept-4003", organization: "Engineering", employees: 38, budget: "$5.1M", performance: "Meeting", location: "San Francisco", growthRate: "+4%", status: "Stable", established: "1992-02-11" },213 ],214 },215 ],216 },217 {218 id: "company-5",219 organization: "Industrial Systems",220 employees: 152,221 budget: "$12.9M",222 performance: "Meeting",223 location: "Detroit",224 growthRate: "+3%",225 status: "Stable",226 established: "2001-01-01",227 divisions: [228 {229 id: "div-500",230 organization: "Production",231 employees: 152,232 budget: "$12.9M",233 performance: "Meeting",234 location: "Multiple",235 growthRate: "+3%",236 status: "Stable",237 established: "2001-01-10",238 departments: [239 { id: "dept-5001", organization: "Assembly", employees: 78, budget: "$6.2M", performance: "Meeting", location: "Detroit", growthRate: "+2%", status: "Stable", established: "2001-05-18" },240 { id: "dept-5002", organization: "Quality Control", employees: 32, budget: "$2.8M", performance: "Exceeding", location: "Pittsburgh", growthRate: "+5%", status: "Hiring", established: "2003-11-24" },241 { id: "dept-5003", organization: "Logistics", employees: 42, budget: "$3.9M", performance: "Meeting", location: "Indianapolis", growthRate: "+3%", status: "Stable", established: "2005-02-08" },242 ],243 },244 ],245 },246];247248export const rowGroupingConfig = {249 headers: rowGroupingHeaders,250 rows: rowGroupingData,251 tableProps: {252 rowGrouping: ["divisions", "departments"] as string[],253 enableStickyParents: true,254 getRowId: ({ row }: { row: Record<string, unknown> }) => String(row.id),255 columnResizing: true,256 },257} as const;258
Svelte
Copy
1<script lang="ts">2 import {SimpleTable} from "@simple-table/svelte"; import type { Theme } from "@simple-table/svelte";3 import { rowGroupingConfig } from "./row-grouping.demo-data";4 import "@simple-table/svelte/styles.css";56 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();78 let tableRef: any;910 function expandAll() { tableRef?.getAPI()?.expandAll(); }11 function collapseAll() { tableRef?.getAPI()?.collapseAll(); }12 function onlyDivisions() { tableRef?.getAPI()?.collapseAll(); tableRef?.getAPI()?.expandDepth(0); }13 function divisionsAndDepts() { tableRef?.getAPI()?.setExpandedDepths(new Set([0, 1])); }14 function toggleDivisions() { tableRef?.getAPI()?.toggleDepth(0); }1516 function btnStyle(color: string) {17 return `padding:6px 12px;background:${color};color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:500`;18 }19</script>2021<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={expandAll} title="expandAll()">Expand All</button>25 <button style={btnStyle("#dc3545")} onclick={collapseAll} title="collapseAll()">Collapse All</button>26 <button style={btnStyle("#007bff")} onclick={onlyDivisions} title="expandDepth(0)">Only Divisions</button>27 <button style={btnStyle("#6c757d")} onclick={divisionsAndDepts} title="setExpandedDepths(new Set([0, 1]))">Divisions + Departments</button>28 <button style={btnStyle("#6f42c1")} onclick={toggleDivisions} title="toggleDepth(0)">Toggle Divisions</button>29 </div>30 <SimpleTable31 bind:this={tableRef}32 columns={rowGroupingConfig.headers}33 rows={rowGroupingConfig.rows}34 rowGrouping={rowGroupingConfig.tableProps.rowGrouping}35 enableStickyParents={true}36 getRowId={rowGroupingConfig.tableProps.getRowId}37 columnResizing={true}38 {height}39 {theme}40 />41</div>
Solid TSX
Copy
1import {SimpleTable} from "@simple-table/solid";import type { Theme, TableAPI } from "@simple-table/solid";2import { rowGroupingConfig } from "./row-grouping.demo-data";3import "@simple-table/solid/styles.css";45const btnStyle = (color: string) => ({6 padding: "6px 12px",7 background: color,8 color: "white",9 border: "none",10 "border-radius": "4px",11 cursor: "pointer",12 "font-size": "12px",13 "font-weight": 500,14});1516export default function RowGroupingDemo(props: { height?: string | number; theme?: Theme }) {17 let tableRef: TableAPI | undefined;1819 return (20 <div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>21 <div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap", "align-items": "center" }}>22 <span style={{ "font-size": "13px", "font-weight": 600, "margin-right": "8px" }}>Control Expansion:</span>23 <button style={btnStyle("#28a745")} onClick={() => tableRef?.expandAll()} title="expandAll()">Expand All</button>24 <button style={btnStyle("#dc3545")} onClick={() => tableRef?.collapseAll()} title="collapseAll()">Collapse All</button>25 <button style={btnStyle("#007bff")} onClick={() => { tableRef?.collapseAll(); tableRef?.expandDepth(0); }} title="expandDepth(0)">Only Divisions</button>26 <button style={btnStyle("#6c757d")} onClick={() => tableRef?.setExpandedDepths(new Set([0, 1]))} title="setExpandedDepths(new Set([0, 1]))">Divisions + Departments</button>27 <button style={btnStyle("#6f42c1")} onClick={() => tableRef?.toggleDepth(0)} title="toggleDepth(0)">Toggle Divisions</button>28 </div>29 <SimpleTable30 ref={(api) => (tableRef = api)}31 columns={rowGroupingConfig.headers}32 rows={rowGroupingConfig.rows}33 rowGrouping={rowGroupingConfig.tableProps.rowGrouping}34 enableStickyParents={true}35 getRowId={rowGroupingConfig.tableProps.getRowId}36 columnResizing37 height={props.height ?? "400px"}38 theme={props.theme}39 />40 </div>41 );42}
TypeScriptRowGroupingDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme } from "simple-table-core";3import { rowGroupingConfig } from "./row-grouping.demo-data";4import "simple-table-core/styles.css";56export function renderRowGroupingDemo(7 container: HTMLElement,8 options?: { height?: string | number; theme?: Theme }9): SimpleTableVanilla {10 const wrapper = document.createElement("div");11 wrapper.style.cssText = "display:flex;flex-direction:column;gap:12px";1213 const controls = document.createElement("div");14 controls.style.cssText = "display:flex;gap:8px;flex-wrap:wrap;align-items:center";1516 const label = document.createElement("span");17 label.textContent = "Control Expansion:";18 label.style.cssText = "font-size:13px;font-weight:600;margin-right:8px";19 controls.appendChild(label);2021 const tableContainer = document.createElement("div");22 wrapper.appendChild(controls);23 wrapper.appendChild(tableContainer);24 container.appendChild(wrapper);2526 const table = new SimpleTableVanilla(tableContainer, {27 columns: rowGroupingConfig.headers,28 rows: rowGroupingConfig.rows,29 height: options?.height ?? "400px",30 theme: options?.theme,31 rowGrouping: rowGroupingConfig.tableProps.rowGrouping,32 enableStickyParents: true,33 getRowId: rowGroupingConfig.tableProps.getRowId,34 columnResizing: true,35 });3637 const api = table.getAPI();3839 const buttons: Array<{ label: string; color: string; action: () => void }> = [40 { label: "Expand All", color: "#28a745", action: () => api.expandAll() },41 { label: "Collapse All", color: "#dc3545", action: () => api.collapseAll() },42 { label: "Only Divisions", color: "#007bff", action: () => { api.collapseAll(); api.expandDepth(0); } },43 { label: "Divisions + Departments", color: "#6c757d", action: () => api.setExpandedDepths(new Set([0, 1])) },44 { label: "Toggle Divisions", color: "#6f42c1", action: () => api.toggleDepth(0) },45 ];4647 for (const { label: text, color, action } of buttons) {48 const btn = document.createElement("button");49 btn.textContent = text;50 btn.style.cssText = `padding:6px 12px;background:${color};color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:500`;51 btn.addEventListener("click", action);52 controls.appendChild(btn);53 }5455 return table;56}575859// row-grouping.demo-data.ts60// Self-contained demo table setup for this example.61import type { ColumnDef } from "simple-table-core";626364export const rowGroupingHeaders: ColumnDef[] = [65 { accessor: "organization", label: "Organization", width: 200, expandable: true, type: "string" },66 { accessor: "employees", label: "Employees", width: 100, type: "number" },67 { accessor: "budget", label: "Annual Budget", width: 140, type: "string" },68 { accessor: "performance", label: "Performance", width: 120, type: "string" },69 { accessor: "location", label: "Location", width: 130, type: "string" },70 { accessor: "growthRate", label: "Growth", width: 90, type: "string" },71 { accessor: "status", label: "Status", width: 110, type: "string" },72 { accessor: "established", label: "Est. Date", width: 110, type: "date" },73];7475export const rowGroupingData = [76 {77 id: "company-1",78 organization: "TechSolutions Inc.",79 employees: 137,80 budget: "$15.0M",81 performance: "Exceeding",82 location: "San Francisco",83 growthRate: "+9%",84 status: "Expanding",85 established: "2018-01-01",86 divisions: [87 {88 id: "div-100",89 organization: "Engineering Division",90 employees: 97,91 budget: "$10.6M",92 performance: "Exceeding",93 location: "Multiple",94 growthRate: "+11%",95 status: "Expanding",96 established: "2018-01-15",97 departments: [98 { id: "dept-1001", organization: "Frontend", employees: 28, budget: "$2.8M", performance: "Exceeding", location: "San Francisco", growthRate: "+12%", status: "Hiring", established: "2019-05-16" },99 { id: "dept-1002", organization: "Backend", employees: 32, budget: "$3.4M", performance: "Meeting", location: "Seattle", growthRate: "+8%", status: "Stable", established: "2018-03-22" },100 { id: "dept-1003", organization: "DevOps", employees: 15, budget: "$1.9M", performance: "Exceeding", location: "Remote", growthRate: "+15%", status: "Hiring", established: "2020-11-05" },101 { id: "dept-1004", organization: "Mobile", employees: 22, budget: "$2.5M", performance: "Meeting", location: "Austin", growthRate: "+10%", status: "Restructuring", established: "2019-08-12" },102 ],103 },104 {105 id: "div-101",106 organization: "Product Division",107 employees: 40,108 budget: "$4.4M",109 performance: "Meeting",110 location: "Multiple",111 growthRate: "+5%",112 status: "Stable",113 established: "2019-01-10",114 departments: [115 { id: "dept-1101", organization: "Design", employees: 17, budget: "$1.8M", performance: "Meeting", location: "Portland", growthRate: "+6%", status: "Stable", established: "2019-02-28" },116 { id: "dept-1102", organization: "Research", employees: 9, budget: "$1.4M", performance: "Below Target", location: "Boston", growthRate: "+3%", status: "Reviewing", established: "2020-07-15" },117 { id: "dept-1103", organization: "QA Testing", employees: 14, budget: "$1.2M", performance: "Meeting", location: "Chicago", growthRate: "+5%", status: "Stable", established: "2019-11-01" },118 ],119 },120 ],121 },122 {123 id: "company-2",124 organization: "HealthFirst Group",125 employees: 138,126 budget: "$22.4M",127 performance: "Meeting",128 location: "Boston",129 growthRate: "+8%",130 status: "Stable",131 established: "2010-01-01",132 divisions: [133 {134 id: "div-200",135 organization: "Hospital Operations",136 employees: 106,137 budget: "$13.1M",138 performance: "Meeting",139 location: "Multiple",140 growthRate: "+6%",141 status: "Expanding",142 established: "2010-01-05",143 departments: [144 { id: "dept-2001", organization: "Emergency", employees: 48, budget: "$5.2M", performance: "Meeting", location: "New York", growthRate: "+4%", status: "Critical", established: "2010-06-14" },145 { id: "dept-2002", organization: "Cardiology", employees: 32, budget: "$4.8M", performance: "Exceeding", location: "Chicago", growthRate: "+9%", status: "Expanding", established: "2012-03-25" },146 { id: "dept-2003", organization: "Pediatrics", employees: 26, budget: "$3.1M", performance: "Meeting", location: "Boston", growthRate: "+7%", status: "Stable", established: "2014-08-30" },147 ],148 },149 {150 id: "div-201",151 organization: "Research & Development",152 employees: 32,153 budget: "$9.3M",154 performance: "Exceeding",155 location: "Multiple",156 growthRate: "+15%",157 status: "Hiring",158 established: "2017-01-10",159 departments: [160 { id: "dept-2101", organization: "Clinical Trials", employees: 18, budget: "$4.2M", performance: "Exceeding", location: "San Diego", growthRate: "+12%", status: "Expanding", established: "2017-04-18" },161 { id: "dept-2102", organization: "Genomics", employees: 14, budget: "$5.1M", performance: "Exceeding", location: "Cambridge", growthRate: "+18%", status: "Hiring", established: "2019-02-21" },162 ],163 },164 ],165 },166 {167 id: "company-3",168 organization: "Global Finance",169 employees: 121,170 budget: "$15.5M",171 performance: "Meeting",172 location: "New York",173 growthRate: "+3%",174 status: "Restructuring",175 established: "2005-01-01",176 divisions: [177 {178 id: "div-300",179 organization: "Banking Operations",180 employees: 121,181 budget: "$15.5M",182 performance: "Meeting",183 location: "Multiple",184 growthRate: "+3%",185 status: "Stable",186 established: "2005-01-15",187 departments: [188 { id: "dept-3001", organization: "Retail Banking", employees: 56, budget: "$4.8M", performance: "Meeting", location: "New York", growthRate: "+2%", status: "Stable", established: "2005-11-08" },189 { id: "dept-3002", organization: "Investment", employees: 38, budget: "$7.2M", performance: "Exceeding", location: "Chicago", growthRate: "+11%", status: "Hiring", established: "2008-05-12" },190 { id: "dept-3003", organization: "Loans", employees: 27, budget: "$3.5M", performance: "Below Target", location: "Dallas", growthRate: "-3%", status: "Restructuring", established: "2010-03-17" },191 ],192 },193 ],194 },195 {196 id: "company-4",197 organization: "Apex University",198 employees: 115,199 budget: "$13.4M",200 performance: "Meeting",201 location: "Cambridge",202 growthRate: "+6%",203 status: "Stable",204 established: "1992-01-01",205 divisions: [206 {207 id: "div-400",208 organization: "Academic Departments",209 employees: 115,210 budget: "$13.4M",211 performance: "Meeting",212 location: "Multiple",213 growthRate: "+6%",214 status: "Stable",215 established: "1992-01-15",216 departments: [217 { id: "dept-4001", organization: "Computer Science", employees: 35, budget: "$3.8M", performance: "Meeting", location: "Boston", growthRate: "+8%", status: "Expanding", established: "1998-08-24" },218 { id: "dept-4002", organization: "Business", employees: 42, budget: "$4.5M", performance: "Exceeding", location: "Chicago", growthRate: "+6%", status: "Stable", established: "1995-09-15" },219 { id: "dept-4003", organization: "Engineering", employees: 38, budget: "$5.1M", performance: "Meeting", location: "San Francisco", growthRate: "+4%", status: "Stable", established: "1992-02-11" },220 ],221 },222 ],223 },224 {225 id: "company-5",226 organization: "Industrial Systems",227 employees: 152,228 budget: "$12.9M",229 performance: "Meeting",230 location: "Detroit",231 growthRate: "+3%",232 status: "Stable",233 established: "2001-01-01",234 divisions: [235 {236 id: "div-500",237 organization: "Production",238 employees: 152,239 budget: "$12.9M",240 performance: "Meeting",241 location: "Multiple",242 growthRate: "+3%",243 status: "Stable",244 established: "2001-01-10",245 departments: [246 { id: "dept-5001", organization: "Assembly", employees: 78, budget: "$6.2M", performance: "Meeting", location: "Detroit", growthRate: "+2%", status: "Stable", established: "2001-05-18" },247 { id: "dept-5002", organization: "Quality Control", employees: 32, budget: "$2.8M", performance: "Exceeding", location: "Pittsburgh", growthRate: "+5%", status: "Hiring", established: "2003-11-24" },248 { id: "dept-5003", organization: "Logistics", employees: 42, budget: "$3.9M", performance: "Meeting", location: "Indianapolis", growthRate: "+3%", status: "Stable", established: "2005-02-08" },249 ],250 },251 ],252 },253];254255export const rowGroupingConfig = {256 headers: rowGroupingHeaders,257 rows: rowGroupingData,258 tableProps: {259 rowGrouping: ["divisions", "departments"] as string[],260 enableStickyParents: true,261 getRowId: ({ row }: { row: Record<string, unknown> }) => String(row.id),262 columnResizing: true,263 },264} as const;265
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";import type { Theme, OnRowGroupExpandProps } from "@simple-table/react";3import {4 dynamicRowLoadingConfig,5 generateInitialRegions,6 fetchStoresForRegion,7 fetchProductsForStore,8} from "./dynamic-row-loading.demo-data";9import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data";10import "@simple-table/react/styles.css";1112const DynamicRowLoadingDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => {13 const [rows, setRows] = useState<DynamicRegion[]>(() => generateInitialRegions());1415 const handleRowExpand = useCallback(16 async ({ row, depth, groupingKey, isExpanded, setLoading, setError, setEmpty, rowIndexPath }: OnRowGroupExpandProps) => {17 if (!isExpanded) return;18 if (groupingKey && row[groupingKey] && (row[groupingKey] as unknown[]).length > 0) return;1920 try {21 if (depth === 0 && groupingKey === "stores") {22 setLoading(true);23 const region = row as DynamicRegion;24 const stores = await fetchStoresForRegion(region.id);25 setLoading(false);26 if (stores.length === 0) { setEmpty(true, "No stores found for this region"); return; }27 setRows((prevRows) => {28 const newRows = [...prevRows];29 newRows[rowIndexPath[0]].stores = stores;30 return newRows;31 });32 } else if (depth === 1 && groupingKey === "products") {33 setLoading(true);34 const store = row as DynamicStore;35 const products = await fetchProductsForStore(store.id);36 setLoading(false);37 if (products.length === 0) { setEmpty(true, "No products found for this store"); return; }38 setRows((prevRows) => {39 const newRows = [...prevRows];40 const region = newRows[rowIndexPath[0]];41 if (region.stores && region.stores[rowIndexPath[1]]) {42 region.stores[rowIndexPath[1]].products = products;43 }44 return newRows;45 });46 }47 } catch (error) {48 setLoading(false);49 setError(error instanceof Error ? error.message : "Failed to load data");50 }51 },52 [],53 );5455 return (56 <SimpleTable57 columnResizing={dynamicRowLoadingConfig.tableProps.columnResizing}58 columns={dynamicRowLoadingConfig.headers}59 enableColumnEditor={dynamicRowLoadingConfig.tableProps.enableColumnEditor}60 expandAll={dynamicRowLoadingConfig.tableProps.expandAll}61 height={height}62 onRowGroupExpand={handleRowExpand}63 rowGrouping={dynamicRowLoadingConfig.tableProps.rowGrouping}64 getRowId={dynamicRowLoadingConfig.tableProps.getRowId}65 rows={rows}66 selectableCells={dynamicRowLoadingConfig.tableProps.selectableCells}67 theme={theme}68 oddEvenRowBackground={dynamicRowLoadingConfig.tableProps.oddEvenRowBackground}69 />70 );71};7273export default DynamicRowLoadingDemo;
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="dynamicRowLoadingConfig.tableProps.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";import type { Theme, OnRowGroupExpandProps } from "@simple-table/vue";21import {22 dynamicRowLoadingConfig,23 generateInitialRegions,24 fetchStoresForRegion,25 fetchProductsForStore,26} from "./dynamic-row-loading.demo-data";27import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data";28import "@simple-table/vue/styles.css";2930withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), { height: "400px" });3132const rows = ref<DynamicRegion[]>(generateInitialRegions());3334async function handleRowExpand({35 row,36 depth,37 groupingKey,38 isExpanded,39 setLoading,40 setError,41 setEmpty,42 rowIndexPath,43}: OnRowGroupExpandProps) {44 if (!isExpanded) return;45 if (groupingKey && row[groupingKey] && (row[groupingKey] as unknown[]).length > 0) return;4647 try {48 if (depth === 0 && groupingKey === "stores") {49 setLoading(true);50 const stores = await fetchStoresForRegion((row as DynamicRegion).id);51 setLoading(false);52 if (stores.length === 0) {53 setEmpty(true, "No stores found");54 return;55 }56 const newRows = [...rows.value];57 newRows[rowIndexPath[0]].stores = stores;58 rows.value = newRows;59 } else if (depth === 1 && groupingKey === "products") {60 setLoading(true);61 const products = await fetchProductsForStore((row as DynamicStore).id);62 setLoading(false);63 if (products.length === 0) {64 setEmpty(true, "No products found");65 return;66 }67 const newRows = [...rows.value];68 const region = newRows[rowIndexPath[0]];69 if (region.stores && region.stores[rowIndexPath[1]]) {70 region.stores[rowIndexPath[1]].products = products;71 }72 rows.value = newRows;73 }74 } catch (error) {75 setLoading(false);76 setError(error instanceof Error ? error.message : "Failed to load data");77 }78}79</script>
Angulardynamic-row-loading-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, 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, DynamicStore } 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[] = dynamicRowLoadingConfig.headers;38 rows: DynamicRegion[] = generateInitialRegions();39 readonly grouping = ["stores", "products"];40 readonly getRowId = ({ row }: { row: Record<string, unknown> }) => row["id"] as string;4142 handleRowExpand = async ({43 row,44 depth,45 groupingKey,46 isExpanded,47 setLoading,48 setError,49 setEmpty,50 rowIndexPath,51 }: OnRowGroupExpandProps) => {52 if (!isExpanded) return;53 if (groupingKey && row[groupingKey] && (row[groupingKey] as unknown[]).length > 0) return;5455 try {56 if (depth === 0 && groupingKey === "stores") {57 setLoading(true);58 const stores = await fetchStoresForRegion((row as DynamicRegion).id);59 setLoading(false);60 if (stores.length === 0) {61 setEmpty(true, "No stores found");62 return;63 }64 const newRows = [...this.rows];65 newRows[rowIndexPath[0]].stores = stores;66 this.rows = newRows;67 } else if (depth === 1 && groupingKey === "products") {68 setLoading(true);69 const products = await fetchProductsForStore((row as DynamicStore).id);70 setLoading(false);71 if (products.length === 0) {72 setEmpty(true, "No products found");73 return;74 }75 const newRows = [...this.rows];76 const region = newRows[rowIndexPath[0]];77 if (region.stores && region.stores[rowIndexPath[1]]) {78 region.stores[rowIndexPath[1]].products = products;79 }80 this.rows = newRows;81 }82 } catch (error) {83 setLoading(false);84 setError(error instanceof Error ? error.message : "Failed to load data");85 }86 };87}888990// dynamic-row-loading.demo-data.ts91// Self-contained demo table setup for this example.92import type { AngularColumnDef, Row } from "@simple-table/angular";939495export interface DynamicRegion extends Row {96 id: string;97 name: string;98 type: "region";99 totalSales: number;100 totalRevenue: number;101 activeStores: number;102 avgRating: string;103 lastUpdate: string;104 stores?: DynamicStore[];105}106107export interface DynamicStore extends Row {108 id: string;109 name: string;110 type: "store";111 totalSales: number;112 totalRevenue: number;113 activeStores?: number;114 avgRating: string;115 lastUpdate: string;116 products?: DynamicProduct[];117}118119export interface DynamicProduct extends Row {120 id: string;121 name: string;122 type: "product";123 totalSales: number;124 totalRevenue: number;125 activeStores?: number;126 avgRating: string;127 lastUpdate: string;128}129130export const dynamicRowLoadingHeaders: AngularColumnDef[] = [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" as const,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" as const,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" as const,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"] as string[],288 getRowId: ({ row }: { row: Record<string, unknown> }) => row.id as string,289 expandAll: false,290 columnResizing: true,291 selectableCells: true,292 oddEvenRowBackground: true,293 enableColumnEditor: true,294 },295} as const;296
Svelte
Copy
1<script lang="ts">2 import {SimpleTable} from "@simple-table/svelte"; import type { Theme, OnRowGroupExpandProps } from "@simple-table/svelte";3 import {4 dynamicRowLoadingConfig,5 generateInitialRegions,6 fetchStoresForRegion,7 fetchProductsForStore,8 } from "./dynamic-row-loading.demo-data";9 import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data";10 import "@simple-table/svelte/styles.css";1112 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();1314 let rows = $state<DynamicRegion[]>(generateInitialRegions());1516 async function handleRowExpand({17 row,18 depth,19 groupingKey,20 isExpanded,21 setLoading,22 setError,23 setEmpty,24 rowIndexPath,25 }: OnRowGroupExpandProps) {26 if (!isExpanded) return;27 if (groupingKey && row[groupingKey] && (row[groupingKey] as unknown[]).length > 0) return;2829 try {30 if (depth === 0 && groupingKey === "stores") {31 setLoading(true);32 const stores = await fetchStoresForRegion((row as DynamicRegion).id);33 setLoading(false);34 if (stores.length === 0) {35 setEmpty(true, "No stores found");36 return;37 }38 const newRows = [...rows];39 newRows[rowIndexPath[0]].stores = stores;40 rows = newRows;41 } else if (depth === 1 && groupingKey === "products") {42 setLoading(true);43 const products = await fetchProductsForStore((row as DynamicStore).id);44 setLoading(false);45 if (products.length === 0) {46 setEmpty(true, "No products found");47 return;48 }49 const newRows = [...rows];50 const region = newRows[rowIndexPath[0]];51 if (region.stores && region.stores[rowIndexPath[1]]) {52 region.stores[rowIndexPath[1]].products = products;53 }54 rows = newRows;55 }56 } catch (error) {57 setLoading(false);58 setError(error instanceof Error ? error.message : "Failed to load data");59 }60 }61</script>6263<SimpleTable64 columnResizing={dynamicRowLoadingConfig.tableProps.columnResizing}65 columns={dynamicRowLoadingConfig.headers}66 enableColumnEditor={dynamicRowLoadingConfig.tableProps.enableColumnEditor}67 expandAll={dynamicRowLoadingConfig.tableProps.expandAll}68 {height}69 onRowGroupExpand={handleRowExpand}70 rowGrouping={dynamicRowLoadingConfig.tableProps.rowGrouping}71 getRowId={dynamicRowLoadingConfig.tableProps.getRowId}72 rows={rows}73 selectableCells={dynamicRowLoadingConfig.tableProps.selectableCells}74 {theme}75 oddEvenRowBackground={dynamicRowLoadingConfig.tableProps.oddEvenRowBackground}76/>
Solid TSX
Copy
1import { createSignal } from "solid-js";2import {SimpleTable} from "@simple-table/solid";import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid";3import {4 dynamicRowLoadingConfig,5 generateInitialRegions,6 fetchStoresForRegion,7 fetchProductsForStore,8} from "./dynamic-row-loading.demo-data";9import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data";10import "@simple-table/solid/styles.css";1112export default function DynamicRowLoadingDemo(props: { height?: string | number; theme?: Theme }) {13 const [rows, setRows] = createSignal<DynamicRegion[]>(generateInitialRegions());1415 const handleRowExpand = async ({16 row,17 depth,18 groupingKey,19 isExpanded,20 setLoading,21 setError,22 setEmpty,23 rowIndexPath,24 }: OnRowGroupExpandProps) => {25 if (!isExpanded) return;26 if (groupingKey && row[groupingKey] && (row[groupingKey] as unknown[]).length > 0) return;2728 try {29 if (depth === 0 && groupingKey === "stores") {30 setLoading(true);31 const stores = await fetchStoresForRegion((row as DynamicRegion).id);32 setLoading(false);33 if (stores.length === 0) {34 setEmpty(true, "No stores found");35 return;36 }37 setRows((prev) => {38 const newRows = [...prev];39 newRows[rowIndexPath[0]].stores = stores;40 return newRows;41 });42 } else if (depth === 1 && groupingKey === "products") {43 setLoading(true);44 const products = await fetchProductsForStore((row as DynamicStore).id);45 setLoading(false);46 if (products.length === 0) {47 setEmpty(true, "No products found");48 return;49 }50 setRows((prev) => {51 const newRows = [...prev];52 const region = newRows[rowIndexPath[0]];53 if (region.stores && region.stores[rowIndexPath[1]]) {54 region.stores[rowIndexPath[1]].products = products;55 }56 return newRows;57 });58 }59 } catch (error) {60 setLoading(false);61 setError(error instanceof Error ? error.message : "Failed to load data");62 }63 };6465 return (66 <SimpleTable67 columnResizing={dynamicRowLoadingConfig.tableProps.columnResizing}68 columns={dynamicRowLoadingConfig.headers}69 enableColumnEditor={dynamicRowLoadingConfig.tableProps.enableColumnEditor}70 expandAll={dynamicRowLoadingConfig.tableProps.expandAll}71 height={props.height ?? "400px"}72 onRowGroupExpand={handleRowExpand}73 rowGrouping={dynamicRowLoadingConfig.tableProps.rowGrouping}74 getRowId={dynamicRowLoadingConfig.tableProps.getRowId}75 rows={rows()}76 selectableCells={dynamicRowLoadingConfig.tableProps.selectableCells}77 theme={props.theme}78 oddEvenRowBackground={dynamicRowLoadingConfig.tableProps.oddEvenRowBackground}79 />80 );81}
TypeScriptDynamicRowLoadingDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme, OnRowGroupExpandProps } from "simple-table-core";3import {4 dynamicRowLoadingConfig,5 generateInitialRegions,6 fetchStoresForRegion,7 fetchProductsForStore,8} from "./dynamic-row-loading.demo-data";9import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data";10import "simple-table-core/styles.css";1112export function renderDynamicRowLoadingDemo(13 container: HTMLElement,14 options?: { height?: string | number; theme?: Theme }15): SimpleTableVanilla {16 let rows: DynamicRegion[] = generateInitialRegions();1718 const handleRowExpand = async ({19 row,20 depth,21 groupingKey,22 isExpanded,23 setLoading,24 setError,25 setEmpty,26 rowIndexPath,27 }: OnRowGroupExpandProps) => {28 if (!isExpanded) return;29 if (groupingKey && row[groupingKey] && (row[groupingKey] as unknown[]).length > 0) return;3031 try {32 if (depth === 0 && groupingKey === "stores") {33 setLoading(true);34 const stores = await fetchStoresForRegion((row as DynamicRegion).id);35 setLoading(false);36 if (stores.length === 0) {37 setEmpty(true, "No stores found");38 return;39 }40 rows[rowIndexPath[0]].stores = stores;41 table.updateConfig({ rows: [...rows] });42 } else if (depth === 1 && groupingKey === "products") {43 setLoading(true);44 const products = await fetchProductsForStore((row as DynamicStore).id);45 setLoading(false);46 if (products.length === 0) {47 setEmpty(true, "No products found");48 return;49 }50 const region = rows[rowIndexPath[0]];51 if (region.stores && region.stores[rowIndexPath[1]]) {52 region.stores[rowIndexPath[1]].products = products;53 }54 table.updateConfig({ rows: [...rows] });55 }56 } catch (error) {57 setLoading(false);58 setError(error instanceof Error ? error.message : "Failed to load data");59 }60 };6162 const table = new SimpleTableVanilla(container, {63 columnResizing: dynamicRowLoadingConfig.tableProps.columnResizing,64 columns: dynamicRowLoadingConfig.headers,65 enableColumnEditor: dynamicRowLoadingConfig.tableProps.enableColumnEditor,66 expandAll: dynamicRowLoadingConfig.tableProps.expandAll,67 height: options?.height ?? "400px",68 onRowGroupExpand: handleRowExpand,69 rowGrouping: dynamicRowLoadingConfig.tableProps.rowGrouping,70 getRowId: dynamicRowLoadingConfig.tableProps.getRowId,71 rows: rows,72 selectableCells: dynamicRowLoadingConfig.tableProps.selectableCells,73 theme: options?.theme,74 oddEvenRowBackground: dynamicRowLoadingConfig.tableProps.oddEvenRowBackground,75 });7677 return table;78}798081// dynamic-row-loading.demo-data.ts82// Self-contained demo table setup for this example.83import type { ColumnDef, Row } from "simple-table-core";848586export interface DynamicRegion extends Row {87 id: string;88 name: string;89 type: "region";90 totalSales: number;91 totalRevenue: number;92 activeStores: number;93 avgRating: string;94 lastUpdate: string;95 stores?: DynamicStore[];96}9798export interface DynamicStore extends Row {99 id: string;100 name: string;101 type: "store";102 totalSales: number;103 totalRevenue: number;104 activeStores?: number;105 avgRating: string;106 lastUpdate: string;107 products?: DynamicProduct[];108}109110export interface DynamicProduct extends Row {111 id: string;112 name: string;113 type: "product";114 totalSales: number;115 totalRevenue: number;116 activeStores?: number;117 avgRating: string;118 lastUpdate: string;119}120121export const dynamicRowLoadingHeaders: ColumnDef[] = [122 { accessor: "name", label: "Name", width: 280, expandable: true, type: "string", pinned: "left" },123 { accessor: "type", label: "Type", width: 100, type: "string" },124 {125 accessor: "totalSales", label: "Total Sales", width: 120, type: "number", align: "right",126 aggregation: { type: "sum" },127 valueFormatter: ({ value }) => typeof value !== "number" ? "—" : value.toLocaleString(),128 },129 {130 accessor: "totalRevenue", label: "Revenue", width: 140, type: "number", align: "right",131 aggregation: { type: "sum" },132 valueFormatter: ({ value }) => typeof value !== "number" ? "—" : `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,133 },134 {135 accessor: "activeStores", label: "Stores", width: 100, type: "number", align: "right",136 valueFormatter: ({ value }) => typeof value !== "number" ? "—" : value.toLocaleString(),137 },138 { accessor: "avgRating", label: "Avg Rating", width: 120, type: "string", align: "center" },139 { accessor: "lastUpdate", label: "Last Updated", width: 130, type: "date" },140];141142const REGION_NAMES = [143 "North America - East", "North America - West", "Europe - North", "Europe - South",144 "Asia Pacific - East", "Asia Pacific - Southeast", "Middle East",145 "Latin America - North", "Latin America - South", "Africa - North", "Africa - South", "Oceania",146];147148const STORE_NAMES = [149 "Manhattan Flagship", "Brooklyn Heights", "Boston Downtown", "Miami Beach",150 "Los Angeles Beverly Hills", "San Francisco Union Square", "Seattle Downtown", "Portland Pearl District",151 "London Oxford Street", "Stockholm Gamla Stan", "Copenhagen Strøget", "Amsterdam Central",152 "Paris Champs-Élysées", "Madrid Gran Vía", "Rome Via del Corso", "Barcelona La Rambla",153 "Tokyo Shibuya", "Shanghai Nanjing Road", "Hong Kong Central", "Seoul Gangnam",154 "Singapore Orchard", "Bangkok Siam", "Kuala Lumpur Bukit Bintang", "Jakarta Grand Indonesia",155 "Dubai Mall", "Abu Dhabi Marina", "Riyadh Kingdom Centre",156 "Mexico City Reforma", "Monterrey Valle", "Guadalajara Centro",157 "São Paulo Paulista", "Buenos Aires Palermo", "Santiago Providencia",158 "Cairo City Stars", "Casablanca Morocco Mall", "Tunis Centre Urbain",159 "Johannesburg Sandton", "Cape Town V&A Waterfront",160 "Sydney Pitt Street", "Melbourne Bourke Street", "Auckland Queen Street",161];162163const PRODUCT_NAMES = [164 "Wireless Headphones Pro", "Smart Watch Elite", "USB-C Hub Deluxe", "Mechanical Keyboard RGB",165 "Ergonomic Mouse", "Webcam 4K", "Portable SSD 2TB", "Wireless Charger Pad",166 "Phone Stand Aluminum", "Bluetooth Speaker Mini", "Laptop Stand Pro", "Cable Organizer Set",167 "Gaming Mouse Elite", "Noise Cancelling Headset", "RGB Desk Mat XL", "Wireless Presenter",168 "Document Camera", "Smart Pen Digital", "Monitor Arm Dual", "Docking Station Pro",169 "Microphone USB Studio", "Tablet Stand Adjustable", "HDMI Switch 4K", "Laptop Cooling Pad",170 "Blue Light Blocking Glasses", "Anti-Glare Screen Protector", "Laptop Privacy Filter",171 "Wireless Charging Pad Trio", "MagSafe Car Mount", "Charging Cable Braided 10ft",172 "Ergonomic Vertical Mouse", "Trackball Mouse Wireless", "Gaming Mouse Pad XXL",173 "Keyboard Wrist Rest", "Monitor Privacy Filter", "Laptop Sleeve Premium",174 "Desktop Mic Arm", "Cable Management Box", "USB Hub 7-Port", "Ergonomic Chair Cushion",175 "Footrest Adjustable", "Desk Lamp LED Smart", "Portable Monitor 15.6", "Screen Cleaning Kit",176 "Desk Organizer Bamboo", "Wireless Trackpad", "Numeric Keypad Wireless",177 "Presentation Clicker", "Gaming Controller Pro", "Racing Wheel Set",178];179180const seededRandom = (seed: string) => {181 let hash = 0;182 for (let i = 0; i < seed.length; i++) {183 hash = (hash << 5) - hash + seed.charCodeAt(i);184 hash = hash & hash;185 }186 const x = Math.sin(hash) * 10000;187 return x - Math.floor(x);188};189190const getRandomInt = (seed: string, min: number, max: number) =>191 Math.floor(seededRandom(seed) * (max - min + 1)) + min;192193const getRandomRating = (seed: string) => (4.0 + seededRandom(seed + "rating") * 1.0).toFixed(1);194195const getRandomDate = (seed: string) => {196 const daysAgo = getRandomInt(seed + "date", 0, 5);197 const date = new Date();198 date.setDate(date.getDate() - daysAgo);199 return date.toISOString().split("T")[0];200};201202const generateStoresForRegion = (regionId: string): DynamicStore[] => {203 const regionIndex = parseInt(regionId.split("-")[1]);204 const numStores = getRandomInt(regionId, 3, 4);205 const startIndex = (regionIndex - 1) * 3;206 return Array.from({ length: numStores }, (_, i) => {207 const storeId = `STORE-${regionIndex}${String(i + 1).padStart(2, "0")}`;208 const storeIndex = startIndex + i;209 const totalSales = getRandomInt(storeId, 10000, 25000);210 const avgPrice = getRandomInt(storeId + "price", 25, 35);211 return {212 id: storeId,213 name: STORE_NAMES[storeIndex % STORE_NAMES.length],214 type: "store" as const,215 totalSales,216 totalRevenue: totalSales * avgPrice,217 avgRating: getRandomRating(storeId),218 lastUpdate: getRandomDate(storeId),219 };220 });221};222223const generateProductsForStore = (storeId: string): DynamicProduct[] => {224 const numProducts = getRandomInt(storeId, 3, 5);225 const storeNumber = parseInt(storeId.split("-")[1]);226 const startIndex = storeNumber * 3;227 return Array.from({ length: numProducts }, (_, i) => {228 const productId = `PROD-${storeId.split("-")[1]}-${i + 1}`;229 const totalSales = getRandomInt(productId, 2000, 8000);230 const avgPrice = getRandomInt(productId + "price", 20, 40);231 return {232 id: productId,233 name: PRODUCT_NAMES[(startIndex + i) % PRODUCT_NAMES.length],234 type: "product" as const,235 totalSales,236 totalRevenue: totalSales * avgPrice,237 avgRating: getRandomRating(productId),238 lastUpdate: getRandomDate(productId),239 };240 });241};242243const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));244245export const fetchStoresForRegion = async (regionId: string): Promise<DynamicStore[]> => {246 await delay(1500);247 return generateStoresForRegion(regionId);248};249250export const fetchProductsForStore = async (storeId: string): Promise<DynamicProduct[]> => {251 await delay(1000);252 return generateProductsForStore(storeId);253};254255export const generateInitialRegions = (): DynamicRegion[] => {256 return REGION_NAMES.map((name, index) => {257 const regionId = `REG-${index + 1}`;258 const stores = generateStoresForRegion(regionId);259 const totalSales = stores.reduce((sum, s) => sum + s.totalSales, 0);260 const totalRevenue = stores.reduce((sum, s) => sum + s.totalRevenue, 0);261 const avgRating = (stores.reduce((sum, s) => sum + parseFloat(s.avgRating), 0) / stores.length).toFixed(1);262 return {263 id: regionId,264 name,265 type: "region" as const,266 totalSales,267 totalRevenue,268 activeStores: getRandomInt(regionId, 3, 4),269 avgRating,270 lastUpdate: getRandomDate(regionId),271 };272 });273};274275export const dynamicRowLoadingConfig = {276 headers: dynamicRowLoadingHeaders,277 tableProps: {278 rowGrouping: ["stores", "products"] as string[],279 getRowId: ({ row }: { row: Record<string, unknown> }) => row.id as string,280 expandAll: false,281 columnResizing: true,282 selectableCells: true,283 oddEvenRowBackground: true,284 enableColumnEditor: true,285 },286} as const;287
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). |