Documentation
Nested Tables
Give each hierarchy level its own columns — expand a row to open a full child table.
- 1
Define child columns
Unlike row grouping, each level can have its own column set.TypeScriptCopyconst divisionColumns = [{ accessor: "divisionId", label: "Division ID", width: 120 },{ accessor: "revenue", label: "Revenue", width: 120, type: "number" },{ accessor: "headcount", label: "Headcount", width: 110, type: "number" },{ accessor: "location", label: "Location", width: "1fr" },];TypeScriptCopyconst divisionColumns = [{ accessor: "divisionId", label: "Division ID", width: 120 },{ accessor: "revenue", label: "Revenue", width: 120, type: "number" },{ accessor: "headcount", label: "Headcount", width: 110, type: "number" },{ accessor: "location", label: "Location", width: "1fr" },];TypeScriptCopyconst divisionColumns = [{ accessor: "divisionId", label: "Division ID", width: 120 },{ accessor: "revenue", label: "Revenue", width: 120, type: "number" },{ accessor: "headcount", label: "Headcount", width: 110, type: "number" },{ accessor: "location", label: "Location", width: "1fr" },];TypeScriptCopyconst divisionColumns = [{ accessor: "divisionId", label: "Division ID", width: 120 },{ accessor: "revenue", label: "Revenue", width: 120, type: "number" },{ accessor: "headcount", label: "Headcount", width: 110, type: "number" },{ accessor: "location", label: "Location", width: "1fr" },];TypeScriptCopyconst divisionColumns = [{ accessor: "divisionId", label: "Division ID", width: 120 },{ accessor: "revenue", label: "Revenue", width: 120, type: "number" },{ accessor: "headcount", label: "Headcount", width: 110, type: "number" },{ accessor: "location", label: "Location", width: "1fr" },];TypeScriptCopyconst divisionColumns = [{ accessor: "divisionId", label: "Division ID", width: 120 },{ accessor: "revenue", label: "Revenue", width: 120, type: "number" },{ accessor: "headcount", label: "Headcount", width: 110, type: "number" },{ accessor: "location", label: "Location", width: "1fr" },]; - 2
Add nestedTable to an expandable column
Setexpandable: trueandnestedTablewith those child columns.TypeScriptCopy{accessor: "companyName",label: "Company",width: 200,expandable: true,nestedTable: {columns: divisionColumns,},}TypeScriptCopy{accessor: "companyName",label: "Company",width: 200,expandable: true,nestedTable: {columns: divisionColumns,},}TypeScriptCopy{accessor: "companyName",label: "Company",width: 200,expandable: true,nestedTable: {columns: divisionColumns,},}TypeScriptCopy{accessor: "companyName",label: "Company",width: 200,expandable: true,nestedTable: {columns: divisionColumns,},}TypeScriptCopy{accessor: "companyName",label: "Company",width: 200,expandable: true,nestedTable: {columns: divisionColumns,},}TypeScriptCopy{accessor: "companyName",label: "Company",width: 200,expandable: true,nestedTable: {columns: divisionColumns,},} - 3
Shape nested data
Nest child arrays under therowGroupingkeys. Child fields match the nested table columns.TypeScriptCopy{id: "co-1",companyName: "Acme Corp",divisions: [{divisionId: "D-1",revenue: 1200000,headcount: 42,location: "Austin",},],}TypeScriptCopy{id: "co-1",companyName: "Acme Corp",divisions: [{divisionId: "D-1",revenue: 1200000,headcount: 42,location: "Austin",},],}TypeScriptCopy{id: "co-1",companyName: "Acme Corp",divisions: [{divisionId: "D-1",revenue: 1200000,headcount: 42,location: "Austin",},],}TypeScriptCopy{id: "co-1",companyName: "Acme Corp",divisions: [{divisionId: "D-1",revenue: 1200000,headcount: 42,location: "Austin",},],}TypeScriptCopy{id: "co-1",companyName: "Acme Corp",divisions: [{divisionId: "D-1",revenue: 1200000,headcount: 42,location: "Austin",},],}TypeScriptCopy{id: "co-1",companyName: "Acme Corp",divisions: [{divisionId: "D-1",revenue: 1200000,headcount: 42,location: "Austin",},],} - 4
Wire rowGrouping on the parent table
PassrowGroupingandgetRowIdso expansion stays stable.React TSXCopy<SimpleTablecolumns={companyColumns}rows={rows}rowGrouping={["divisions"]}getRowId={({ row }) => String(row.id)}/>Vue SFCCopy<SimpleTable:columns="companyColumns":rows="rows":row-grouping="['divisions']":get-row-id="(ctx) => String(ctx.row.id)"/>AngularCopy<simple-table[columns]="companyColumns"[rows]="rows"[rowGrouping]="['divisions']"[getRowId]="getRowId"></simple-table>SvelteCopy<SimpleTablecolumns={companyColumns}{rows}rowGrouping={["divisions"]}getRowId={({ row }) => String(row.id)}/>Solid TSXCopy<SimpleTablecolumns={companyColumns}rows={rows()}rowGrouping={["divisions"]}getRowId={({ row }) => String(row.id)}/>TypeScriptCopynew SimpleTableVanilla(container, {columns: companyColumns,rows,rowGrouping: ["divisions"],getRowId: ({ row }) => String(row.id),});
Patterns
Configure the nested table
nestedTable accepts most SimpleTable props (selection, resizing, theme, pagination, and more). rows and state renderers come from the parent and are not set here.
TypeScript
Copy
{accessor: "companyName",label: "Company",expandable: true,nestedTable: {columns: divisionColumns,enableRowSelection: true,columnResizing: true,autoExpandColumns: true,},}
TypeScript
Copy
{accessor: "companyName",label: "Company",expandable: true,nestedTable: {columns: divisionColumns,enableRowSelection: true,columnResizing: true,autoExpandColumns: true,},}
TypeScript
Copy
{accessor: "companyName",label: "Company",expandable: true,nestedTable: {columns: divisionColumns,enableRowSelection: true,columnResizing: true,autoExpandColumns: true,},}
TypeScript
Copy
{accessor: "companyName",label: "Company",expandable: true,nestedTable: {columns: divisionColumns,enableRowSelection: true,columnResizing: true,autoExpandColumns: true,},}
TypeScript
Copy
{accessor: "companyName",label: "Company",expandable: true,nestedTable: {columns: divisionColumns,enableRowSelection: true,columnResizing: true,autoExpandColumns: true,},}
TypeScript
Copy
{accessor: "companyName",label: "Company",expandable: true,nestedTable: {columns: divisionColumns,enableRowSelection: true,columnResizing: true,autoExpandColumns: true,},}
Multi-level nesting
Add nestedTable on a column inside the child columns, and extend rowGrouping accordingly.
TypeScript
Copy
// Parent column{accessor: "companyName",expandable: true,nestedTable: {columns: [{accessor: "divisionName",expandable: true,nestedTable: { columns: teamColumns },},// ...],},}// Parent tablerowGrouping: ["divisions", "teams"]
TypeScript
Copy
// Parent column{accessor: "companyName",expandable: true,nestedTable: {columns: [{accessor: "divisionName",expandable: true,nestedTable: { columns: teamColumns },},// ...],},}// Parent tablerowGrouping: ["divisions", "teams"]
TypeScript
Copy
// Parent column{accessor: "companyName",expandable: true,nestedTable: {columns: [{accessor: "divisionName",expandable: true,nestedTable: { columns: teamColumns },},// ...],},}// Parent tablerowGrouping: ["divisions", "teams"]
TypeScript
Copy
// Parent column{accessor: "companyName",expandable: true,nestedTable: {columns: [{accessor: "divisionName",expandable: true,nestedTable: { columns: teamColumns },},// ...],},}// Parent tablerowGrouping: ["divisions", "teams"]
TypeScript
Copy
// Parent column{accessor: "companyName",expandable: true,nestedTable: {columns: [{accessor: "divisionName",expandable: true,nestedTable: { columns: teamColumns },},// ...],},}// Parent tablerowGrouping: ["divisions", "teams"]
TypeScript
Copy
// Parent column{accessor: "companyName",expandable: true,nestedTable: {columns: [{accessor: "divisionName",expandable: true,nestedTable: { columns: teamColumns },},// ...],},}// Parent tablerowGrouping: ["divisions", "teams"]
Lazy-load nested rows
Use onRowGroupExpand on the parent (or on a nested level) to fetch children when a row expands — same helpers as row grouping (setLoading, setError, setEmpty).
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);}},});
Example
Pre-loaded divisions under each company.
React TSX
Copy
1import { useMemo } from "react";2import {SimpleTable} from "@simple-table/react";import type { Theme } from "@simple-table/react";3import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data";4import "@simple-table/react/styles.css";56const NestedTablesDemo = ({ height = "500px", theme }: { height?: string | number; theme?: Theme }) => {7 const sampleData = useMemo(() => generateNestedTablesData(25), []);89 return (10 <SimpleTable11 autoExpandColumns={nestedTablesConfig.tableProps.autoExpandColumns}12 columns={nestedTablesConfig.headers}13 rows={sampleData}14 rowGrouping={nestedTablesConfig.tableProps.rowGrouping}15 getRowId={nestedTablesConfig.tableProps.getRowId}16 expandAll={nestedTablesConfig.tableProps.expandAll}17 columnResizing={nestedTablesConfig.tableProps.columnResizing}18 height={height}19 theme={theme}20 />21 );22};2324export default NestedTablesDemo;
Vue SFC
Copy
1<template>2 <SimpleTable3 :auto-expand-columns="nestedTablesConfig.tableProps.autoExpandColumns"4 :columns="nestedTablesConfig.headers"5 :rows="sampleData"6 :row-grouping="nestedTablesConfig.tableProps.rowGrouping"7 :get-row-id="nestedTablesConfig.tableProps.getRowId"8 :expand-all="nestedTablesConfig.tableProps.expandAll"9 :column-resizing="nestedTablesConfig.tableProps.columnResizing"10 :height="height"11 :theme="theme"12 />13</template>1415<script setup lang="ts">16import { computed } from "vue";17import {SimpleTable} from "@simple-table/vue";import type { Theme } from "@simple-table/vue";18import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data";19import "@simple-table/vue/styles.css";2021withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), { height: "500px" });2223const sampleData = computed(() => generateNestedTablesData(25));24</script>
Angularnested-tables-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, Theme } from "@simple-table/angular";3import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data";4import "@simple-table/angular/styles.css";56@Component({7 selector: "nested-tables-demo",8 standalone: true,9 imports: [SimpleTableComponent],10 template: `11 <simple-table12 [autoExpandColumns]="true"13 [columns]="headers"14 [rows]="sampleData"15 [rowGrouping]="grouping"16 [getRowId]="getRowId"17 [expandAll]="false"18 [columnResizing]="true"19 [height]="height"20 [theme]="theme"21 ></simple-table>22 `,23})24export class NestedTablesDemoComponent {25 @Input() height: string | number = "500px";26 @Input() theme?: Theme;2728 readonly headers: AngularColumnDef[] = nestedTablesConfig.headers;29 readonly sampleData = generateNestedTablesData(25);30 readonly grouping = ["divisions"];31 readonly getRowId = ({ row }: { row: Record<string, unknown> }) => row["id"] as string;32}333435// nested-tables.demo-data.ts36// Self-contained demo table setup for this example.37import type { AngularColumnDef } from "@simple-table/angular";383940const industries = ["Technology", "Financial Services", "Healthcare", "Manufacturing", "Retail", "Energy", "Telecommunications", "Pharmaceuticals", "Automotive", "Aerospace", "Biotechnology", "E-commerce"];41const cities = ["San Francisco, CA", "New York, NY", "Boston, MA", "Seattle, WA", "Austin, TX", "Chicago, IL", "Los Angeles, CA", "Denver, CO", "Miami, FL", "Atlanta, GA", "Portland, OR", "Dallas, TX"];42const firstNames = ["Jane", "John", "Emily", "Michael", "Sarah", "David", "Lisa", "Robert", "Maria", "James", "Jennifer", "William", "Patricia", "Richard", "Linda"];43const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Anderson", "Taylor", "Thomas", "Moore"];44const divisionTypes = ["Cloud Services", "AI Research", "Consumer Products", "Investment Banking", "Retail Banking", "Research & Development", "Operations", "Sales & Marketing", "Customer Success", "Engineering", "Product Development", "Analytics", "Infrastructure", "Security", "Data Science"];45const companyNames = ["TechCorp", "FinanceHub", "HealthTech", "GlobalSystems", "InnovateLabs", "FutureTech", "DataWorks", "CloudFirst", "SmartSolutions", "NextGen", "PrimeVentures", "AlphaGroup", "BetaSystems", "GammaIndustries", "DeltaCorp"];46const suffixes = ["Global", "Inc", "Solutions", "Systems", "Ventures", "Group", "Industries", "Technologies"];4748const randomElement = <T,>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];49const randomInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min;5051const generateDivision = (divisionIndex: number, companyIndex: number) => ({52 divisionId: `DIV-${String(companyIndex * 10 + divisionIndex).padStart(3, "0")}`,53 divisionName: randomElement(divisionTypes),54 revenue: `$${randomInt(5, 25)}B`,55 profitMargin: `${randomInt(15, 50)}%`,56 headcount: randomInt(50, 500),57 location: randomElement(cities),58});5960const generateCompany = (companyIndex: number) => {61 const divisions = Array.from({ length: randomInt(3, 7) }, (_, i) => generateDivision(i, companyIndex));62 return {63 id: companyIndex + 1,64 companyName: `${randomElement(companyNames)} ${randomElement(suffixes)}`,65 industry: randomElement(industries),66 founded: randomInt(1985, 2020),67 headquarters: randomElement(cities),68 stockSymbol: Array.from({ length: 4 }, () => String.fromCharCode(65 + randomInt(0, 25))).join(""),69 marketCap: `$${randomInt(10, 200)}B`,70 ceo: `${randomElement(firstNames)} ${randomElement(lastNames)}`,71 revenue: `$${randomInt(5, 60)}B`,72 employees: randomInt(5000, 100000),73 divisions,74 };75};7677export const generateNestedTablesData = (count: number = 25) => Array.from({ length: count }, (_, i) => generateCompany(i));7879export const nestedTablesDivisionHeaders: AngularColumnDef[] = [80 { accessor: "divisionId", label: "Division ID", width: 120 },81 { accessor: "revenue", label: "Revenue", width: 120 },82 { accessor: "profitMargin", label: "Profit Margin", width: 130 },83 { accessor: "headcount", label: "Headcount", width: 110, type: "number" },84 { accessor: "location", label: "Location", width: "1fr" },85];8687export const nestedTablesHeaders: AngularColumnDef[] = [88 {89 accessor: "companyName",90 label: "Company",91 width: 200,92 expandable: true,93 nestedTable: { columns: nestedTablesDivisionHeaders },94 },95 { accessor: "stockSymbol", label: "Symbol", width: 100 },96 { accessor: "marketCap", label: "Market Cap", width: 120 },97 { accessor: "revenue", label: "Revenue", width: 120 },98 { accessor: "employees", label: "Employees", width: 120, type: "number" },99];100101export const nestedTablesConfig = {102 headers: nestedTablesHeaders,103 tableProps: {104 rowGrouping: ["divisions"] as string[],105 getRowId: ({ row }: { row: Record<string, unknown> }) => row.id as string,106 expandAll: false,107 columnResizing: true,108 autoExpandColumns: true,109 },110} as const;111
Svelte
Copy
1<script lang="ts">2 import {SimpleTable} from "@simple-table/svelte"; import type { Theme } from "@simple-table/svelte";3 import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data";4 import "@simple-table/svelte/styles.css";56 let { height = "500px", theme }: { height?: string | number; theme?: Theme } = $props();78 const sampleData = generateNestedTablesData(25);9</script>1011<SimpleTable12 autoExpandColumns={nestedTablesConfig.tableProps.autoExpandColumns}13 columns={nestedTablesConfig.headers}14 rows={sampleData}15 rowGrouping={nestedTablesConfig.tableProps.rowGrouping}16 getRowId={nestedTablesConfig.tableProps.getRowId}17 expandAll={nestedTablesConfig.tableProps.expandAll}18 columnResizing={nestedTablesConfig.tableProps.columnResizing}19 {height}20 {theme}21/>
Solid TSX
Copy
1import { createMemo } from "solid-js";2import {SimpleTable} from "@simple-table/solid";import type { Theme } from "@simple-table/solid";3import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data";4import "@simple-table/solid/styles.css";56export default function NestedTablesDemo(props: { height?: string | number; theme?: Theme }) {7 const sampleData = createMemo(() => generateNestedTablesData(25));89 return (10 <SimpleTable11 autoExpandColumns={nestedTablesConfig.tableProps.autoExpandColumns}12 columns={nestedTablesConfig.headers}13 rows={sampleData()}14 rowGrouping={nestedTablesConfig.tableProps.rowGrouping}15 getRowId={nestedTablesConfig.tableProps.getRowId}16 expandAll={nestedTablesConfig.tableProps.expandAll}17 columnResizing={nestedTablesConfig.tableProps.columnResizing}18 height={props.height ?? "500px"}19 theme={props.theme}20 />21 );22}
TypeScriptNestedTablesDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme } from "simple-table-core";3import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data";4import "simple-table-core/styles.css";56export function renderNestedTablesDemo(7 container: HTMLElement,8 options?: { height?: string | number; theme?: Theme }9): SimpleTableVanilla {10 const sampleData = generateNestedTablesData(25);1112 return new SimpleTableVanilla(container, {13 autoExpandColumns: nestedTablesConfig.tableProps.autoExpandColumns,14 columns: nestedTablesConfig.headers,15 rows: sampleData,16 rowGrouping: nestedTablesConfig.tableProps.rowGrouping,17 getRowId: nestedTablesConfig.tableProps.getRowId,18 expandAll: nestedTablesConfig.tableProps.expandAll,19 columnResizing: nestedTablesConfig.tableProps.columnResizing,20 height: options?.height ?? "500px",21 theme: options?.theme,22 });23}242526// nested-tables.demo-data.ts27// Self-contained demo table setup for this example.28import type { ColumnDef } from "simple-table-core";293031const industries = ["Technology", "Financial Services", "Healthcare", "Manufacturing", "Retail", "Energy", "Telecommunications", "Pharmaceuticals", "Automotive", "Aerospace", "Biotechnology", "E-commerce"];32const cities = ["San Francisco, CA", "New York, NY", "Boston, MA", "Seattle, WA", "Austin, TX", "Chicago, IL", "Los Angeles, CA", "Denver, CO", "Miami, FL", "Atlanta, GA", "Portland, OR", "Dallas, TX"];33const firstNames = ["Jane", "John", "Emily", "Michael", "Sarah", "David", "Lisa", "Robert", "Maria", "James", "Jennifer", "William", "Patricia", "Richard", "Linda"];34const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Anderson", "Taylor", "Thomas", "Moore"];35const divisionTypes = ["Cloud Services", "AI Research", "Consumer Products", "Investment Banking", "Retail Banking", "Research & Development", "Operations", "Sales & Marketing", "Customer Success", "Engineering", "Product Development", "Analytics", "Infrastructure", "Security", "Data Science"];36const companyNames = ["TechCorp", "FinanceHub", "HealthTech", "GlobalSystems", "InnovateLabs", "FutureTech", "DataWorks", "CloudFirst", "SmartSolutions", "NextGen", "PrimeVentures", "AlphaGroup", "BetaSystems", "GammaIndustries", "DeltaCorp"];37const suffixes = ["Global", "Inc", "Solutions", "Systems", "Ventures", "Group", "Industries", "Technologies"];3839const randomElement = <T,>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];40const randomInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min;4142const generateDivision = (divisionIndex: number, companyIndex: number) => ({43 divisionId: `DIV-${String(companyIndex * 10 + divisionIndex).padStart(3, "0")}`,44 divisionName: randomElement(divisionTypes),45 revenue: `$${randomInt(5, 25)}B`,46 profitMargin: `${randomInt(15, 50)}%`,47 headcount: randomInt(50, 500),48 location: randomElement(cities),49});5051const generateCompany = (companyIndex: number) => {52 const divisions = Array.from({ length: randomInt(3, 7) }, (_, i) => generateDivision(i, companyIndex));53 return {54 id: companyIndex + 1,55 companyName: `${randomElement(companyNames)} ${randomElement(suffixes)}`,56 industry: randomElement(industries),57 founded: randomInt(1985, 2020),58 headquarters: randomElement(cities),59 stockSymbol: Array.from({ length: 4 }, () => String.fromCharCode(65 + randomInt(0, 25))).join(""),60 marketCap: `$${randomInt(10, 200)}B`,61 ceo: `${randomElement(firstNames)} ${randomElement(lastNames)}`,62 revenue: `$${randomInt(5, 60)}B`,63 employees: randomInt(5000, 100000),64 divisions,65 };66};6768export const generateNestedTablesData = (count: number = 25) => Array.from({ length: count }, (_, i) => generateCompany(i));6970export const nestedTablesDivisionHeaders: ColumnDef[] = [71 { accessor: "divisionId", label: "Division ID", width: 120 },72 { accessor: "revenue", label: "Revenue", width: 120 },73 { accessor: "profitMargin", label: "Profit Margin", width: 130 },74 { accessor: "headcount", label: "Headcount", width: 110, type: "number" },75 { accessor: "location", label: "Location", width: "1fr" },76];7778export const nestedTablesHeaders: ColumnDef[] = [79 {80 accessor: "companyName",81 label: "Company",82 width: 200,83 expandable: true,84 nestedTable: { columns: nestedTablesDivisionHeaders },85 },86 { accessor: "stockSymbol", label: "Symbol", width: 100 },87 { accessor: "marketCap", label: "Market Cap", width: 120 },88 { accessor: "revenue", label: "Revenue", width: 120 },89 { accessor: "employees", label: "Employees", width: 120, type: "number" },90];9192export const nestedTablesConfig = {93 headers: nestedTablesHeaders,94 tableProps: {95 rowGrouping: ["divisions"] as string[],96 getRowId: ({ row }: { row: Record<string, unknown> }) => row.id as string,97 expandAll: false,98 columnResizing: true,99 autoExpandColumns: true,100 },101} as const;102
Dynamic loading example
Divisions load when a company row expands.
React TSX
Copy
1import { useState, useCallback } from "react";2import {SimpleTable} from "@simple-table/react";import type { Theme, OnRowGroupExpandProps } from "@simple-table/react";3import {4 dynamicNestedTablesConfig,5 dynamicNestedTablesData,6 fetchDivisionsForCompany,7} from "./dynamic-nested-tables.demo-data";8import type { DynamicCompany } from "./dynamic-nested-tables.demo-data";9import "@simple-table/react/styles.css";1011const DynamicNestedTablesDemo = ({ height = "500px", theme }: { height?: string | number; theme?: Theme }) => {12 const [rows, setRows] = useState<DynamicCompany[]>([...dynamicNestedTablesData]);1314 const handleCompanyExpand = useCallback(15 async ({ row, groupingKey, isExpanded, rowIndexPath, setLoading, setError, setEmpty }: OnRowGroupExpandProps) => {16 if (!isExpanded) return;17 try {18 if (groupingKey === "divisions") {19 const company = row as DynamicCompany;20 if (company.divisions && company.divisions.length > 0) return;21 setLoading(true);22 const divisions = await fetchDivisionsForCompany(company.id);23 if (divisions.length === 0) {24 setEmpty(true, "No divisions found for this company");25 return;26 }27 setRows((prevRows) => {28 const newRows = [...prevRows];29 const companyIndex = rowIndexPath[0];30 newRows[companyIndex] = { ...newRows[companyIndex], divisions };31 return newRows;32 });33 }34 } catch (error) {35 setLoading(false);36 setError(error instanceof Error ? error.message : "Failed to load divisions");37 }38 },39 [],40 );4142 return (43 <SimpleTable44 autoExpandColumns={dynamicNestedTablesConfig.tableProps.autoExpandColumns}45 columns={dynamicNestedTablesConfig.headers}46 expandAll={dynamicNestedTablesConfig.tableProps.expandAll}47 height={height}48 rowGrouping={dynamicNestedTablesConfig.tableProps.rowGrouping}49 getRowId={dynamicNestedTablesConfig.tableProps.getRowId}50 rows={rows}51 onRowGroupExpand={handleCompanyExpand}52 theme={theme}53 />54 );55};5657export default DynamicNestedTablesDemo;
Vue SFC
Copy
1<template>2 <SimpleTable3 :auto-expand-columns="dynamicNestedTablesConfig.tableProps.autoExpandColumns"4 :columns="dynamicNestedTablesConfig.headers"5 :expand-all="dynamicNestedTablesConfig.tableProps.expandAll"6 :height="height"7 :row-grouping="dynamicNestedTablesConfig.tableProps.rowGrouping"8 :get-row-id="dynamicNestedTablesConfig.tableProps.getRowId"9 :rows="rows"10 :on-row-group-expand="handleCompanyExpand"11 :theme="theme"12 />13</template>1415<script setup lang="ts">16import { ref } from "vue";17import {SimpleTable} from "@simple-table/vue";import type { Theme, OnRowGroupExpandProps } from "@simple-table/vue";18import {19 dynamicNestedTablesConfig,20 dynamicNestedTablesData,21 fetchDivisionsForCompany,22} from "./dynamic-nested-tables.demo-data";23import type { DynamicCompany } from "./dynamic-nested-tables.demo-data";24import "@simple-table/vue/styles.css";2526withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), { height: "500px" });2728const rows = ref<DynamicCompany[]>([...dynamicNestedTablesData]);2930async function handleCompanyExpand({31 row,32 groupingKey,33 isExpanded,34 rowIndexPath,35 setLoading,36 setError,37 setEmpty,38}: OnRowGroupExpandProps) {39 if (!isExpanded) return;40 try {41 if (groupingKey === "divisions") {42 const company = row as DynamicCompany;43 if (company.divisions && company.divisions.length > 0) return;44 setLoading(true);45 const divisions = await fetchDivisionsForCompany(company.id);46 if (divisions.length === 0) {47 setEmpty(true, "No divisions found for this company");48 return;49 }50 const newRows = [...rows.value];51 newRows[rowIndexPath[0]] = { ...newRows[rowIndexPath[0]], divisions };52 rows.value = newRows;53 }54 } catch (error) {55 setLoading(false);56 setError(error instanceof Error ? error.message : "Failed to load divisions");57 }58}59</script>
Angulardynamic-nested-tables-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 dynamicNestedTablesConfig,5 dynamicNestedTablesData,6 fetchDivisionsForCompany,7} from "./dynamic-nested-tables.demo-data";8import type { DynamicCompany } from "./dynamic-nested-tables.demo-data";9import "@simple-table/angular/styles.css";1011@Component({12 selector: "dynamic-nested-tables-demo",13 standalone: true,14 imports: [SimpleTableComponent],15 template: `16 <simple-table17 [autoExpandColumns]="tableProps.autoExpandColumns"18 [columns]="headers"19 [expandAll]="tableProps.expandAll"20 [height]="height"21 [rowGrouping]="tableProps.rowGrouping"22 [getRowId]="tableProps.getRowId"23 [rows]="rows"24 [onRowGroupExpand]="handleCompanyExpand"25 [theme]="theme"26 ></simple-table>27 `,28})29export class DynamicNestedTablesDemoComponent {30 @Input() height: string | number = "500px";31 @Input() theme?: Theme;3233 headers: AngularColumnDef[] = dynamicNestedTablesConfig.headers;34 rows: DynamicCompany[] = [...dynamicNestedTablesData];35 readonly tableProps = dynamicNestedTablesConfig.tableProps;3637 handleCompanyExpand = async ({38 row,39 groupingKey,40 isExpanded,41 rowIndexPath,42 setLoading,43 setError,44 setEmpty,45 }: OnRowGroupExpandProps) => {46 if (!isExpanded) return;47 try {48 if (groupingKey === "divisions") {49 const company = row as DynamicCompany;50 if (company.divisions && company.divisions.length > 0) return;51 setLoading(true);52 const divisions = await fetchDivisionsForCompany(company.id);53 if (divisions.length === 0) {54 setEmpty(true, "No divisions found for this company");55 return;56 }57 const newRows = [...this.rows];58 newRows[rowIndexPath[0]] = { ...newRows[rowIndexPath[0]], divisions };59 this.rows = newRows;60 }61 } catch (error) {62 setLoading(false);63 setError(error instanceof Error ? error.message : "Failed to load divisions");64 }65 };66}676869// dynamic-nested-tables.demo-data.ts70// Self-contained demo table setup for this example.71import type { AngularColumnDef, Row } from "@simple-table/angular";727374export interface DynamicCompany extends Row {75 id: string;76 companyName: string;77 industry: string;78 revenue: string;79 employees: number;80 divisions?: DynamicDivision[];81}8283export interface DynamicDivision extends Row {84 id: string;85 divisionName: string;86 revenue: string;87 profitMargin: string;88 headcount: number;89 location: string;90}9192const simulateDelay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));9394export const fetchDivisionsForCompany = async (companyId: string): Promise<DynamicDivision[]> => {95 await simulateDelay(800);96 const divisionCount = Math.floor(Math.random() * 3) + 2;97 const divisionNames = ["Cloud Services", "AI Research", "Consumer Products", "Investment Banking", "Operations", "Engineering"];98 const locations = ["San Francisco, CA", "New York, NY", "Boston, MA", "Seattle, WA", "Austin, TX", "Chicago, IL"];99100 return Array.from({ length: divisionCount }, (_, i) => ({101 id: `${companyId}-div-${i}`,102 divisionName: divisionNames[i % divisionNames.length],103 revenue: `$${Math.floor(Math.random() * 50) + 10}M`,104 profitMargin: `${Math.floor(Math.random() * 30) + 10}%`,105 headcount: Math.floor(Math.random() * 400) + 50,106 location: locations[i % locations.length],107 }));108};109110export const dynamicNestedTablesData: DynamicCompany[] = [111 { id: "comp-1", companyName: "TechCorp Global", industry: "Technology", revenue: "$250M", employees: 1200 },112 { id: "comp-2", companyName: "FinanceHub Inc", industry: "Financial Services", revenue: "$180M", employees: 850 },113 { id: "comp-3", companyName: "HealthTech Solutions", industry: "Healthcare", revenue: "$320M", employees: 1500 },114 { id: "comp-4", companyName: "RetailMax Corporation", industry: "Retail", revenue: "$420M", employees: 2100 },115 { id: "comp-5", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },116 { id: "comp-6", companyName: "MediaVision Studios", industry: "Entertainment", revenue: "$290M", employees: 950 },117 { id: "comp-7", companyName: "AutoDrive Industries", industry: "Automotive", revenue: "$680M", employees: 3200 },118 { id: "comp-8", companyName: "CloudNet Services", industry: "Technology", revenue: "$195M", employees: 720 },119 { id: "comp-9", companyName: "HealthCare Solutions", industry: "Healthcare", revenue: "$380M", employees: 1300 },120 { id: "comp-10", companyName: "EducationTech Innovations", industry: "Education", revenue: "$240M", employees: 1050 },121 { id: "comp-11", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },122 { id: "comp-12", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },123 { id: "comp-13", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },124 { id: "comp-14", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },125 { id: "comp-15", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },126];127128export const dynamicNestedTablesDivisionHeaders: AngularColumnDef[] = [129 { accessor: "divisionName", label: "Division", width: 200 },130 { accessor: "revenue", label: "Revenue", width: 120 },131 { accessor: "profitMargin", label: "Profit Margin", width: 130 },132 { accessor: "headcount", label: "Headcount", width: 110, type: "number" },133 { accessor: "location", label: "Location", width: 180 },134];135136export const dynamicNestedTablesCompanyHeaders: AngularColumnDef[] = [137 {138 accessor: "companyName",139 label: "Company",140 width: 200,141 expandable: true,142 nestedTable: {143 columns: dynamicNestedTablesDivisionHeaders,144 expandAll: false,145 autoExpandColumns: true,146 },147 },148 { accessor: "industry", label: "Industry", width: 150 },149 { accessor: "revenue", label: "Revenue", width: 120 },150 { accessor: "employees", label: "Employees", width: 120, type: "number" },151];152153export const dynamicNestedTablesConfig = {154 headers: dynamicNestedTablesCompanyHeaders,155 rows: dynamicNestedTablesData,156 tableProps: {157 rowGrouping: ["divisions"] as string[],158 getRowId: ({ row }: { row: Record<string, unknown> }) => row.id as string,159 expandAll: false,160 autoExpandColumns: true,161 },162} as const;163
Svelte
Copy
1<script lang="ts">2 import {SimpleTable} from "@simple-table/svelte"; import type { Theme, OnRowGroupExpandProps } from "@simple-table/svelte";3 import {4 dynamicNestedTablesConfig,5 dynamicNestedTablesData,6 fetchDivisionsForCompany,7 } from "./dynamic-nested-tables.demo-data";8 import type { DynamicCompany } from "./dynamic-nested-tables.demo-data";9 import "@simple-table/svelte/styles.css";1011 let { height = "500px", theme }: { height?: string | number; theme?: Theme } = $props();1213 let rows = $state<DynamicCompany[]>([...dynamicNestedTablesData]);1415 async function handleCompanyExpand({16 row,17 groupingKey,18 isExpanded,19 rowIndexPath,20 setLoading,21 setError,22 setEmpty,23 }: OnRowGroupExpandProps) {24 if (!isExpanded) return;25 try {26 if (groupingKey === "divisions") {27 const company = row as DynamicCompany;28 if (company.divisions && company.divisions.length > 0) return;29 setLoading(true);30 const divisions = await fetchDivisionsForCompany(company.id);31 if (divisions.length === 0) {32 setEmpty(true, "No divisions found for this company");33 return;34 }35 const newRows = [...rows];36 newRows[rowIndexPath[0]] = { ...newRows[rowIndexPath[0]], divisions };37 rows = newRows;38 }39 } catch (error) {40 setLoading(false);41 setError(error instanceof Error ? error.message : "Failed to load divisions");42 }43 }44</script>4546<SimpleTable47 autoExpandColumns={dynamicNestedTablesConfig.tableProps.autoExpandColumns}48 columns={dynamicNestedTablesConfig.headers}49 expandAll={dynamicNestedTablesConfig.tableProps.expandAll}50 {height}51 rowGrouping={dynamicNestedTablesConfig.tableProps.rowGrouping}52 getRowId={dynamicNestedTablesConfig.tableProps.getRowId}53 rows={rows}54 onRowGroupExpand={handleCompanyExpand}55 {theme}56/>
Solid TSX
Copy
1import { createSignal } from "solid-js";2import {SimpleTable} from "@simple-table/solid";import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid";3import {4 dynamicNestedTablesConfig,5 dynamicNestedTablesData,6 fetchDivisionsForCompany,7} from "./dynamic-nested-tables.demo-data";8import type { DynamicCompany } from "./dynamic-nested-tables.demo-data";9import "@simple-table/solid/styles.css";1011export default function DynamicNestedTablesDemo(props: { height?: string | number; theme?: Theme }) {12 const [rows, setRows] = createSignal<DynamicCompany[]>([...dynamicNestedTablesData]);1314 const handleCompanyExpand = async ({15 row,16 groupingKey,17 isExpanded,18 rowIndexPath,19 setLoading,20 setError,21 setEmpty,22 }: OnRowGroupExpandProps) => {23 if (!isExpanded) return;24 try {25 if (groupingKey === "divisions") {26 const company = row as DynamicCompany;27 if (company.divisions && company.divisions.length > 0) return;28 setLoading(true);29 const divisions = await fetchDivisionsForCompany(company.id);30 if (divisions.length === 0) {31 setEmpty(true, "No divisions found for this company");32 return;33 }34 setRows((prev) => {35 const newRows = [...prev];36 newRows[rowIndexPath[0]] = { ...newRows[rowIndexPath[0]], divisions };37 return newRows;38 });39 }40 } catch (error) {41 setLoading(false);42 setError(error instanceof Error ? error.message : "Failed to load divisions");43 }44 };4546 return (47 <SimpleTable48 autoExpandColumns={dynamicNestedTablesConfig.tableProps.autoExpandColumns}49 columns={dynamicNestedTablesConfig.headers}50 expandAll={dynamicNestedTablesConfig.tableProps.expandAll}51 height={props.height ?? "500px"}52 rowGrouping={dynamicNestedTablesConfig.tableProps.rowGrouping}53 getRowId={dynamicNestedTablesConfig.tableProps.getRowId}54 rows={rows()}55 onRowGroupExpand={handleCompanyExpand}56 theme={props.theme}57 />58 );59}
TypeScriptDynamicNestedTablesDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme, OnRowGroupExpandProps } from "simple-table-core";3import {4 dynamicNestedTablesConfig,5 dynamicNestedTablesData,6 fetchDivisionsForCompany,7} from "./dynamic-nested-tables.demo-data";8import type { DynamicCompany } from "./dynamic-nested-tables.demo-data";9import "simple-table-core/styles.css";1011export function renderDynamicNestedTablesDemo(12 container: HTMLElement,13 options?: { height?: string | number; theme?: Theme }14): SimpleTableVanilla {15 let rows: DynamicCompany[] = [...dynamicNestedTablesData];1617 const handleCompanyExpand = async ({18 row,19 groupingKey,20 isExpanded,21 rowIndexPath,22 setLoading,23 setError,24 setEmpty,25 }: OnRowGroupExpandProps) => {26 if (!isExpanded) return;27 try {28 if (groupingKey === "divisions") {29 const company = row as DynamicCompany;30 if (company.divisions && company.divisions.length > 0) return;31 setLoading(true);32 const divisions = await fetchDivisionsForCompany(company.id);33 if (divisions.length === 0) {34 setEmpty(true, "No divisions found for this company");35 return;36 }37 rows[rowIndexPath[0]] = { ...rows[rowIndexPath[0]], divisions };38 table.updateConfig({ rows: [...rows] });39 }40 } catch (error) {41 setLoading(false);42 setError(error instanceof Error ? error.message : "Failed to load divisions");43 }44 };4546 const table = new SimpleTableVanilla(container, {47 autoExpandColumns: dynamicNestedTablesConfig.tableProps.autoExpandColumns,48 columns: dynamicNestedTablesConfig.headers,49 expandAll: dynamicNestedTablesConfig.tableProps.expandAll,50 height: options?.height ?? "500px",51 rowGrouping: dynamicNestedTablesConfig.tableProps.rowGrouping,52 getRowId: dynamicNestedTablesConfig.tableProps.getRowId,53 rows: rows,54 onRowGroupExpand: handleCompanyExpand,55 theme: options?.theme,56 });5758 return table;59}606162// dynamic-nested-tables.demo-data.ts63// Self-contained demo table setup for this example.64import type { ColumnDef, Row } from "simple-table-core";656667export interface DynamicCompany extends Row {68 id: string;69 companyName: string;70 industry: string;71 revenue: string;72 employees: number;73 divisions?: DynamicDivision[];74}7576export interface DynamicDivision extends Row {77 id: string;78 divisionName: string;79 revenue: string;80 profitMargin: string;81 headcount: number;82 location: string;83}8485const simulateDelay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));8687export const fetchDivisionsForCompany = async (companyId: string): Promise<DynamicDivision[]> => {88 await simulateDelay(800);89 const divisionCount = Math.floor(Math.random() * 3) + 2;90 const divisionNames = ["Cloud Services", "AI Research", "Consumer Products", "Investment Banking", "Operations", "Engineering"];91 const locations = ["San Francisco, CA", "New York, NY", "Boston, MA", "Seattle, WA", "Austin, TX", "Chicago, IL"];9293 return Array.from({ length: divisionCount }, (_, i) => ({94 id: `${companyId}-div-${i}`,95 divisionName: divisionNames[i % divisionNames.length],96 revenue: `$${Math.floor(Math.random() * 50) + 10}M`,97 profitMargin: `${Math.floor(Math.random() * 30) + 10}%`,98 headcount: Math.floor(Math.random() * 400) + 50,99 location: locations[i % locations.length],100 }));101};102103export const dynamicNestedTablesData: DynamicCompany[] = [104 { id: "comp-1", companyName: "TechCorp Global", industry: "Technology", revenue: "$250M", employees: 1200 },105 { id: "comp-2", companyName: "FinanceHub Inc", industry: "Financial Services", revenue: "$180M", employees: 850 },106 { id: "comp-3", companyName: "HealthTech Solutions", industry: "Healthcare", revenue: "$320M", employees: 1500 },107 { id: "comp-4", companyName: "RetailMax Corporation", industry: "Retail", revenue: "$420M", employees: 2100 },108 { id: "comp-5", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },109 { id: "comp-6", companyName: "MediaVision Studios", industry: "Entertainment", revenue: "$290M", employees: 950 },110 { id: "comp-7", companyName: "AutoDrive Industries", industry: "Automotive", revenue: "$680M", employees: 3200 },111 { id: "comp-8", companyName: "CloudNet Services", industry: "Technology", revenue: "$195M", employees: 720 },112 { id: "comp-9", companyName: "HealthCare Solutions", industry: "Healthcare", revenue: "$380M", employees: 1300 },113 { id: "comp-10", companyName: "EducationTech Innovations", industry: "Education", revenue: "$240M", employees: 1050 },114 { id: "comp-11", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },115 { id: "comp-12", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },116 { id: "comp-13", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },117 { id: "comp-14", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },118 { id: "comp-15", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 },119];120121export const dynamicNestedTablesDivisionHeaders: ColumnDef[] = [122 { accessor: "divisionName", label: "Division", width: 200 },123 { accessor: "revenue", label: "Revenue", width: 120 },124 { accessor: "profitMargin", label: "Profit Margin", width: 130 },125 { accessor: "headcount", label: "Headcount", width: 110, type: "number" },126 { accessor: "location", label: "Location", width: 180 },127];128129export const dynamicNestedTablesCompanyHeaders: ColumnDef[] = [130 {131 accessor: "companyName",132 label: "Company",133 width: 200,134 expandable: true,135 nestedTable: {136 columns: dynamicNestedTablesDivisionHeaders,137 expandAll: false,138 autoExpandColumns: true,139 },140 },141 { accessor: "industry", label: "Industry", width: 150 },142 { accessor: "revenue", label: "Revenue", width: 120 },143 { accessor: "employees", label: "Employees", width: 120, type: "number" },144];145146export const dynamicNestedTablesConfig = {147 headers: dynamicNestedTablesCompanyHeaders,148 rows: dynamicNestedTablesData,149 tableProps: {150 rowGrouping: ["divisions"] as string[],151 getRowId: ({ row }: { row: Record<string, unknown> }) => row.id as string,152 expandAll: false,153 autoExpandColumns: true,154 },155} as const;156
Props
Nested Tables Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
ColumnDef.nestedTable | Optional | Renders an independent child table on expand. Requires expandable: true and matching rowGrouping. | |
nestedTable.columns | Required | Column defs for the nested table — can differ entirely from the parent. | |
rowGroupingstring[] | Optional | Nested array property names that define the hierarchy (same as row grouping). | |
getRowId(props: { row: Row }) => string | number | null | undefined | Optional | Stable row id so expansion survives sort and data updates. | |
onRowGroupExpand | Optional | Lazy-load children on expand. Can be set per nesting level. |