Master-Detail Plugin
The Master-Detail plugin lets you create expandable detail rows that reveal additional content beneath each master row. Perfect for order/line-item UIs, employee/department views, or any scenario where you need to show related data without navigating away.
Installation
Section titled “Installation”import '@toolbox-web/grid/features/master-detail';Basic Usage
Section titled “Basic Usage”The key configuration is detailRenderer - a function that receives the row data and returns either an HTML string or a DOM element. This gives you complete control over what appears in the expanded detail area.
import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'orderId', header: 'Order ID' }, { field: 'customer', header: 'Customer' }, { field: 'total', header: 'Total', type: 'currency' } ], features: { masterDetail: { detailRenderer: (row) => ` <div class="order-details"> <h4>Order Items</h4> <ul> ${row.items.map(item => `<li>${item.name} - $${item.price}</li>`).join('')} </ul> </div> `, }, },};import '@toolbox-web/grid-react/features/master-detail';import { DataGrid, GridDetailPanel } from '@toolbox-web/grid-react';
function OrderGrid({ orders }) { return ( <DataGrid rows={orders} columns={[ { field: 'orderId', header: 'Order ID' }, { field: 'customer', header: 'Customer' }, { field: 'total', header: 'Total', type: 'currency' } ]} > <GridDetailPanel> {({ row }) => ( <div className="order-details"> <h4>Order Items</h4> <ul> {row.items.map(item => ( <li key={item.id}>{item.name} - ${item.price}</li> ))} </ul> </div> )} </GridDetailPanel> </DataGrid> );}<script setup>import '@toolbox-web/grid-vue/features/master-detail';import { TbwGrid, TbwGridColumn, TbwGridDetailPanel } from '@toolbox-web/grid-vue';
const orders = [ { orderId: 'ORD-001', customer: 'Alice', total: 150, items: [{ id: 1, name: 'Widget', price: 50 }, { id: 2, name: 'Gadget', price: 100 }] }, { orderId: 'ORD-002', customer: 'Bob', total: 75, items: [{ id: 3, name: 'Tool', price: 75 }] },];</script>
<template> <TbwGrid :rows="orders" master-detail> <TbwGridColumn field="orderId" header="Order ID" /> <TbwGridColumn field="customer" header="Customer" /> <TbwGridColumn field="total" header="Total" type="currency" />
<TbwGridDetailPanel v-slot="{ row }"> <div class="order-details"> <h4>Order Items</h4> <ul> <li v-for="item in row.items" :key="item.id"> {{ item.name }} - ${{ item.price }} </li> </ul> </div> </TbwGridDetailPanel> </TbwGrid></template>// Feature import - enables the [masterDetail] inputimport '@toolbox-web/grid-angular/features/master-detail';
import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
@Component({ selector: 'app-order-grid', imports: [Grid], template: ` <tbw-grid [rows]="orders" [columns]="columns" [masterDetail]="masterDetailConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class OrderGridComponent { orders = [/* order data */];
columns: ColumnConfig[] = [ { field: 'orderId', header: 'Order ID' }, { field: 'customer', header: 'Customer' }, { field: 'total', header: 'Total', type: 'currency' } ];
masterDetailConfig = { detailRenderer: (row: any) => ` <div class="order-details"> <h4>Order Items</h4> <ul> ${row.items.map((item: any) => `<li>${item.name} - $${item.price}</li>`).join('')} </ul> </div> `, };}Default Master-Detail
Section titled “Default Master-Detail”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/master-detail';
const generateOrderData = (count: number) => { const customers = ['Alice Corp', 'Bob Inc', 'Carol LLC', 'Delta Co', 'Echo Ltd', 'Foxtrot GmbH', 'Golf SA', 'Hotel AG']; const products = ['Widget A', 'Widget B', 'Gadget X', 'Gadget Y', 'Service Plan', 'Support Pack', 'License', 'Hardware Kit']; return Array.from({ length: count }, (_, i) => { const itemCount = 1 + (i % 4); const items = Array.from({ length: itemCount }, (_, j) => ({ name: products[(i + j) % products.length], qty: 1 + ((i + j) % 5), price: 25 + ((i * 7 + j * 13) % 200), })); return { id: 1001 + i, customer: customers[i % customers.length], date: `2024-${String(1 + (i % 12)).padStart(2, '0')}-${String(1 + (i % 28)).padStart(2, '0')}`, total: items.reduce((sum, item) => sum + item.qty * item.price, 0), items, }; });};
const columns = [ { field: 'id', header: 'Order ID', type: 'number' }, { field: 'customer', header: 'Customer' }, { field: 'date', header: 'Date' }, { field: 'total', header: 'Total', type: 'number', format: (v: number) => `$${v.toFixed(2)}` },];
const detailRenderer = (row: { items: { name: string; qty: number; price: number }[] }) => { const div = document.createElement('div'); div.style.cssText = 'padding: 16px;'; div.innerHTML = ` <h4 style="margin: 0 0 8px;">Order Items</h4> <table style="width: 100%; border-collapse: collapse;"> <thead><tr><th style="padding: 4px 8px; text-align: left;">Item</th><th style="padding: 4px 8px; text-align: right;">Qty</th><th style="padding: 4px 8px; text-align: right;">Price</th></tr></thead> <tbody>${row.items.map((item) => `<tr><td style="padding: 4px 8px;">${item.name}</td><td style="padding: 4px 8px; text-align: right;">${item.qty}</td><td style="padding: 4px 8px; text-align: right;">$${item.price.toFixed(2)}</td></tr>`).join('')}</tbody> </table>`; return div;};
const grid = queryGrid('tbw-grid')!;const orderData = generateOrderData(100);
function rebuild(opts: Record<string, unknown>) { const heightVal = opts.detailHeight as string ?? 'auto'; grid.gridConfig = { columns, features: { masterDetail: { animation: (opts.animation as string) ?? 'slide', detailHeight: heightVal === 'auto' ? 'auto' : Number(heightVal), expandOnRowClick: opts.expandOnRowClick as boolean ?? false, detailRenderer, } as any, }, }; grid.rows = orderData;}
rebuild({ animation: 'slide', expandOnRowClick: false, detailHeight: 'auto' });Expand on Row Click
Section titled “Expand on Row Click”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/master-detail';
// Generate sample order dataconst generateOrderData = (count: number) => { const customers = [ 'Alice Corp', 'Bob Inc', 'Carol LLC', 'Delta Co', 'Echo Ltd', 'Foxtrot GmbH', 'Golf SA', 'Hotel AG', ]; const products = [ 'Widget A', 'Widget B', 'Gadget X', 'Gadget Y', 'Service Plan', 'Support Pack', 'License', 'Hardware Kit', ];
return Array.from({ length: count }, (_, i) => { const itemCount = 1 + (i % 4); const items = Array.from({ length: itemCount }, (_, j) => ({ name: products[(i + j) % products.length], qty: 1 + ((i + j) % 5), price: 25 + ((i * 7 + j * 13) % 200), })); return { id: 1001 + i, customer: customers[i % customers.length], date: `2024-${String(1 + (i % 12)).padStart(2, '0')}-${String(1 + (i % 28)).padStart(2, '0')}`, total: items.reduce((sum, item) => sum + item.qty * item.price, 0), items, }; });};const columns = [ { field: 'id', header: 'Order ID', type: 'number' }, { field: 'customer', header: 'Customer' }, { field: 'date', header: 'Date' }, { field: 'total', header: 'Total', type: 'number', format: (v: number) => `$${v.toFixed(2)}` },];// Shared detail rendererconst detailRenderer = (row: { items: { name: string; qty: number; price: number }[] }) => { const div = document.createElement('div'); div.style.cssText = 'padding: 16px;'; div.innerHTML = ` <h4 style="margin: 0 0 8px;">Order Items</h4> <table style="width: 100%; border-collapse: collapse;"> <thead> <tr> <th style="padding: 4px 8px; text-align: left;">Item</th> <th style="padding: 4px 8px; text-align: right;">Qty</th> <th style="padding: 4px 8px; text-align: right;">Price</th> </tr> </thead> <tbody> ${row.items .map( (item) => ` <tr> <td style="padding: 4px 8px;">${item.name}</td> <td style="padding: 4px 8px; text-align: right;">${item.qty}</td> <td style="padding: 4px 8px; text-align: right;">$${item.price.toFixed(2)}</td> </tr> `, ) .join('')} </tbody> </table> `; return div;};
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns, features: { masterDetail: { detailHeight: "auto", expandOnRowClick: true, detailRenderer, }, }, }; grid.rows = generateOrderData(50);Fixed Detail Height
Section titled “Fixed Detail Height”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/master-detail';
// Generate sample order dataconst generateOrderData = (count: number) => { const customers = [ 'Alice Corp', 'Bob Inc', 'Carol LLC', 'Delta Co', 'Echo Ltd', 'Foxtrot GmbH', 'Golf SA', 'Hotel AG', ]; const products = [ 'Widget A', 'Widget B', 'Gadget X', 'Gadget Y', 'Service Plan', 'Support Pack', 'License', 'Hardware Kit', ];
return Array.from({ length: count }, (_, i) => { const itemCount = 1 + (i % 4); const items = Array.from({ length: itemCount }, (_, j) => ({ name: products[(i + j) % products.length], qty: 1 + ((i + j) % 5), price: 25 + ((i * 7 + j * 13) % 200), })); return { id: 1001 + i, customer: customers[i % customers.length], date: `2024-${String(1 + (i % 12)).padStart(2, '0')}-${String(1 + (i % 28)).padStart(2, '0')}`, total: items.reduce((sum, item) => sum + item.qty * item.price, 0), items, }; });};const columns = [ { field: 'id', header: 'Order ID', type: 'number' }, { field: 'customer', header: 'Customer' }, { field: 'date', header: 'Date' }, { field: 'total', header: 'Total', type: 'number', format: (v: number) => `$${v.toFixed(2)}` },];// Shared detail rendererconst detailRenderer = (row: { items: { name: string; qty: number; price: number }[] }) => { const div = document.createElement('div'); div.style.cssText = 'padding: 16px;';
const h4 = document.createElement('h4'); h4.style.cssText = 'margin: 0 0 8px;'; h4.textContent = 'Order Items'; div.appendChild(h4);
const table = document.createElement('table'); table.style.cssText = 'width: 100%; border-collapse: collapse;';
const thead = table.createTHead(); const headerRow = thead.insertRow(); for (const [text, align] of [['Item', 'left'], ['Qty', 'right'], ['Price', 'right']] as const) { const th = document.createElement('th'); th.style.cssText = `padding: 4px 8px; text-align: ${align};`; th.textContent = text; headerRow.appendChild(th); }
const tbody = table.createTBody(); for (const item of row.items) { const tr = tbody.insertRow(); const tdName = tr.insertCell(); tdName.style.cssText = 'padding: 4px 8px;'; tdName.textContent = item.name;
const tdQty = tr.insertCell(); tdQty.style.cssText = 'padding: 4px 8px; text-align: right;'; tdQty.textContent = String(item.qty);
const tdPrice = tr.insertCell(); tdPrice.style.cssText = 'padding: 4px 8px; text-align: right;'; tdPrice.textContent = `$${item.price.toFixed(2)}`; }
div.appendChild(table); return div;};
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns, features: { masterDetail: { detailHeight: 150, expandOnRowClick: false, detailRenderer, }, }, }; grid.rows = generateOrderData(50);Configuration Options
Section titled “Configuration Options”See MasterDetailConfig for the full list of options and their defaults.
Animation Options
Section titled “Animation Options”The animation option controls how detail rows appear/disappear:
// Slide animation (default)features: { masterDetail: { animation: 'slide', ... } }
// Fade animationfeatures: { masterDetail: { animation: 'fade', ... } }
// No animationfeatures: { masterDetail: { animation: false, ... } }Nested Grid Example
Section titled “Nested Grid Example”features: { masterDetail: { detailRenderer: (row) => { const childGrid = document.createElement('tbw-grid'); childGrid.style.height = '200px'; childGrid.gridConfig = { columns: [...] }; childGrid.rows = row.items || []; return childGrid; }, },},Programmatic API
Section titled “Programmatic API”See MasterDetailPlugin for the full list of methods.
const plugin = grid.getPluginByName('masterDetail');
plugin.expand(rowIndex);plugin.collapse(rowIndex);plugin.toggle(rowIndex);plugin.expandAll();plugin.collapseAll();plugin.isExpanded(rowIndex); // booleanStyling
Section titled “Styling”The master-detail plugin supports CSS custom properties for theming. Override these on tbw-grid or a parent container:
CSS Custom Properties
Section titled “CSS Custom Properties”| Property | Default | Description |
|---|---|---|
--tbw-master-detail-bg | var(--tbw-color-row-alt) | Detail row background |
--tbw-master-detail-border | var(--tbw-color-border) | Detail row border |
--tbw-detail-padding | 1em | Detail content padding |
--tbw-detail-max-height | 31.25rem (~500px) | Max height for animation |
--tbw-animation-duration | 200ms | Expand/collapse animation |
--tbw-animation-easing | ease-out | Animation curve |
Example
Section titled “Example”tbw-grid { /* Custom master-detail styling */ --tbw-master-detail-bg: #f8f9fa; --tbw-master-detail-border: #dee2e6; --tbw-detail-padding: 1.5rem; --tbw-animation-duration: 300ms;}CSS Classes
Section titled “CSS Classes”The master-detail plugin uses these class names:
| Class | Element |
|---|---|
.master-detail-expander | Expander cell container |
.master-detail-toggle | Expand/collapse icon |
.master-detail-row | Detail row container |
.master-detail-row.tbw-expanding | Row during expand animation |
.master-detail-row.tbw-collapsing | Row during collapse animation |
.master-detail-cell | Detail content wrapper |
Events
Section titled “Events”Click the expand arrow to open/close row details.
| Event | Detail | Description |
|---|---|---|
detail-expand | { rowIndex, row, expanded } | Fired when a detail row is expanded/collapsed |
See Also
Section titled “See Also”- Tree — Hierarchical tree data
- Row Grouping — Group rows by field values
- Selection — Row and cell selection