Documentation
Row Grouping
Row grouping allows you to organize your data into expandable hierarchical structures, making it easier to navigate large datasets with natural parent-child relationships.
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 defaultHeaders={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;
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 :default-headers="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>
1import { Component, Input, ViewChild } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularHeaderObject, 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 [defaultHeaders]="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: AngularHeaderObject[] = 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 { AngularHeaderObject } from "@simple-table/angular";555657export const rowGroupingHeaders: AngularHeaderObject[] = [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
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 defaultHeaders={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>
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 defaultHeaders={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}
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 defaultHeaders: 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 { HeaderObject } from "simple-table-core";626364export const rowGroupingHeaders: HeaderObject[] = [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
Basic Setup
To enable row grouping, add the expandable: true property to your column header, structure your data with nested arrays, and specify the grouping hierarchy.
💡 Use Cases
- Organize teams by department and show individual members
- Display projects with their milestones and tasks
- Show product categories with subcategories and items
- Group transactions by account, invoice, and line items
🔑 Recommended: Use getRowId
When using row grouping with external sorting or dynamic data, it's highly recommended to provide the getRowId prop. This ensures stable row identification across data updates:
- Maintains correct expansion state when data is sorted or filtered
- Provides stable
rowIdPathin onRowGroupExpand - Prevents row group collapse when row order changes
- Essential for tables with external sorting enabled
Example: getRowId={({ row }) => row.id as string} or getRowId={({ row }) => row.uuid as string}
🎯 Need Different Columns at Each Level?
Row grouping shows child rows with the same columns as parent rows. If you need each level to have its own independent column structure (e.g., companies with 9 columns, divisions with 6 columns, teams with 19 columns), check out Nested Tables.
Row Grouping Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
HeaderObject.expandableboolean | Optional | Makes a column expandable for grouping. This allows users to expand/collapse hierarchical data in that column. | |
rowGroupingstring[] | Optional | Array of property names that define the hierarchy levels. The order determines the nesting depth (first element is level 1, second is level 2, etc.). | |
expandAllboolean | Optional | When true, all grouped rows are expanded by default on table load. When false, rows start collapsed. | |
onRowGroupExpand | Optional | Callback function triggered when a grouped row is expanded or collapsed. Receives detailed information including helper functions for managing loading, error, and empty states. The rowIndexPath array (v2.2.9+: contains ONLY numeric indices) provides a direct path to update nested data. The optional rowIdPath (when getRowId is provided) offers stable ID-based navigation. Perfect for lazy-loading hierarchical data on demand. | |
loadingStateRendererstring | ReactNode | Optional | Custom content to render when a row is in loading state (set via setLoading helper in onRowGroupExpand). Can be a string or React component. If not provided, a default skeleton loading state will be shown automatically. | |
errorStateRendererstring | ReactNode | Optional | Custom content to render when a row has an error state (set via setError helper in onRowGroupExpand). Can be a string or React component. | |
emptyStateRendererstring | ReactNode | Optional | Custom content to render when a row has no children data (set via setEmpty helper in onRowGroupExpand). Can be a string or React component. | |
canExpandRowGroup(row: Row) => boolean | Optional | Function to conditionally control whether a specific row group can be expanded. Return true to allow expansion, false to disable it. Useful for permission-based access, hiding empty groups, or business logic-based restrictions. | |
enableStickyParentsboolean | Optional | Beta feature: When enabled, parent rows with children will remain sticky at the top while scrolling down through their child rows. This helps maintain context when navigating deep hierarchical data. Defaults to false as this is still a beta feature. |
Programmatic Control
Version 2.1.0 introduces powerful programmatic control over row grouping expansion. You can now control which hierarchy levels are expanded or collapsed using the table ref API. These methods give you fine-grained control over the visibility of nested data.
🎯 New Table API Methods (v2.1.0)
expandAll()- Expand all rows at all depthscollapseAll()- Collapse all rows at all depthsexpandDepth(depth)- Expand all rows at a specific depth (0-indexed)collapseDepth(depth)- Collapse all rows at a specific depth (0-indexed)toggleDepth(depth)- Toggle expansion for a specific depthsetExpandedDepths(depths)- Set which depths are expanded (replaces current state)getExpandedDepths()- Get currently expanded depths as a SetgetGroupingProperty(depth)- Get the grouping property name for a depth indexgetGroupingDepth(property)- Get the depth index for a grouping property name
Sticky Parent Rows (Beta)
The enableStickyParents prop is a beta feature that makes parent rows stick to the top while scrolling through their children. This helps maintain context when navigating deep hierarchical data structures.
⚠️ Beta Feature
This feature is currently in beta and defaults to false. While it works well in most scenarios, there may be edge cases that need refinement. Use with caution in production environments.
Dynamic Row Loading
For large datasets, use the onRowGroupExpand callback to load nested data on-demand. This example demonstrates a three-level hierarchy (Regions → Stores → Products) where child rows are fetched from an API only when their parent is expanded. The callback provides powerful helper functions like setLoading, setError, and setEmpty for state management, plus rowIndexPath (v2.2.9+: contains only numeric indices) and rowIdPath (when getRowId is provided) for easy nested data updates.
💡 Benefits
- Faster initial load - only top-level rows are fetched
- Reduced memory usage - child data loaded as needed
- Better performance with large hierarchical datasets
- Seamless integration with server-side APIs
- Built-in state management with setLoading, setError, and setEmpty helpers
- Simple nested data updates using rowIndexPath array
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 defaultHeaders={dynamicRowLoadingConfig.headers}59 editColumns={dynamicRowLoadingConfig.tableProps.editColumns}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 useOddEvenRowBackground={dynamicRowLoadingConfig.tableProps.useOddEvenRowBackground}69 />70 );71};7273export default DynamicRowLoadingDemo;
1<template>2 <SimpleTable3 :column-resizing="dynamicRowLoadingConfig.tableProps.columnResizing"4 :default-headers="dynamicRowLoadingConfig.headers"5 :edit-columns="dynamicRowLoadingConfig.tableProps.editColumns"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 :use-odd-even-row-background="dynamicRowLoadingConfig.tableProps.useOddEvenRowBackground"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>
1import { Component, Input } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularHeaderObject, 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 [defaultHeaders]="headers"20 [editColumns]="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 [useOddEvenRowBackground]="true"30 ></simple-table>31 `,32})33export class DynamicRowLoadingDemoComponent {34 @Input() height: string | number = "400px";35 @Input() theme?: Theme;3637 headers: AngularHeaderObject[] = 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 { AngularHeaderObject, 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: AngularHeaderObject[] = [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 useOddEvenRowBackground: true,293 editColumns: true,294 },295} as const;296
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 defaultHeaders={dynamicRowLoadingConfig.headers}66 editColumns={dynamicRowLoadingConfig.tableProps.editColumns}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 useOddEvenRowBackground={dynamicRowLoadingConfig.tableProps.useOddEvenRowBackground}76/>
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 defaultHeaders={dynamicRowLoadingConfig.headers}69 editColumns={dynamicRowLoadingConfig.tableProps.editColumns}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 useOddEvenRowBackground={dynamicRowLoadingConfig.tableProps.useOddEvenRowBackground}79 />80 );81}
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 defaultHeaders: dynamicRowLoadingConfig.headers,65 editColumns: dynamicRowLoadingConfig.tableProps.editColumns,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 useOddEvenRowBackground: dynamicRowLoadingConfig.tableProps.useOddEvenRowBackground,75 });7677 return table;78}798081// dynamic-row-loading.demo-data.ts82// Self-contained demo table setup for this example.83import type { HeaderObject, 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: HeaderObject[] = [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 useOddEvenRowBackground: true,284 editColumns: true,285 },286} as const;287