# Master-Detail Plugin

> Show expandable detail rows beneath data rows.

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

```ts
import '@toolbox-web/grid/features/master-detail';
```

## 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.

#### TypeScript

```ts
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>
      `,
    },
  },
};
```

#### React

```tsx
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' }
      ]}
      style={{ height: '400px' }}
    >
      <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>
  );
}
```

#### Vue

```html
<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>
```

#### Angular

```typescript
// Feature imports - GridMasterDetailDirective owns the [masterDetail] input,
// GridDetailView is the structural directive used inside <tbw-grid> for templating.
import {
  GridMasterDetailDirective,
  GridDetailView,
} from '@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, GridMasterDetailDirective, GridDetailView],
  template: `
    <tbw-grid
      [rows]="orders"
      [columns]="columns"
      [masterDetail]="true"
      style="height: 400px; display: block;">
      <ng-template tbwDetailView let-row let-toggle="toggle">
        <div class="order-details">
          <h4>Order Items</h4>
          <ul>
            <li *ngFor="let item of row.items">
              {{ item.name }} - \${{ item.price }}
            </li>
          </ul>
          <button (click)="toggle()">Close</button>
        </div>
      </ng-template>
    </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' }
  ];
}
```

Use the [`GridDetailView`](/grid/angular/api/directives/griddetailview.md) directive
(`<ng-template tbwDetailView>`) for Angular-idiomatic detail panels with
template context (`let-row`, `let-toggle="toggle"`). For non-Angular consumers
of the same plugin, an inline `detailRenderer: (row) => HTMLElement | string`
is also accepted in `gridConfig.masterDetail`.

## Demos

### Default Master-Detail

```ts
// MasterDetailDefaultDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/master-detail';

const container = document.getElementById('master-detail-default-demo');
if (container) {
  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', container)!;
  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' });

  container.addEventListener('control-change', ((e: CustomEvent) => {
    rebuild(e.detail.allValues);
  }) as EventListener);
}
```

## Configuration Options

See [`MasterDetailConfig`](./interfaces/masterdetailconfig/) for the full list of options and their defaults.

### Animation Options

The `animation` option controls how detail rows appear/disappear:

```ts
// Slide animation (default)
features: { masterDetail: { animation: 'slide', ... } }

// Fade animation
features: { masterDetail: { animation: 'fade', ... } }

// No animation
features: { masterDetail: { animation: false, ... } }
```

:::note
Animation respects the grid-level `animation.mode` setting. If the grid has `animation: { mode: 'off' }`, plugin animations are disabled.
:::

## Server-Side Data

> **Requires:** `ServerSidePlugin` — [see Server-Side Plugin](/grid/plugins/server-side.md)

When `ServerSidePlugin` is loaded, master rows are lazily loaded via virtual scrolling (infinite scroll) or pagination — the same way flat data, Tree, or Row Grouping work. MasterDetail does not claim root data, so master rows flow through ServerSide's block cache unchanged.

Detail data can be fetched on demand or embedded in the master row:
- **Async detail data**: On detail expand, MasterDetailPlugin queries `datasource:fetch-children`. ServerSide calls `getChildRows()` and delivers the result.
- **Embedded detail data**: If detail data is already in the master row object (e.g. `row.items`), `detailRenderer` accesses it directly — no `getChildRows()` needed.

When ServerSide is not present, `detailRenderer` works synchronously from the row object.

```ts
import '@toolbox-web/grid/features/master-detail';
import '@toolbox-web/grid/features/server-side';
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: {
    serverSide: {
      pageSize: 50,
      dataSource: {
        async getRows(params) {
          const res = await fetch(`/api/orders?start=${params.startNode}&end=${params.endNode}`);
          const data = await res.json();
          return { rows: data.orders, totalNodeCount: data.total };
        },
        async getChildRows(params) {
          const { row } = params.context;
          const res = await fetch(`/api/orders/${row.id}/items`);
          return { rows: await res.json() };
        },
      },
    },
    masterDetail: {
      detailRenderer: (row, rowIndex) => {
        const plugin = grid.getPluginByName('masterDetail');

        if (plugin.isDetailLoading(rowIndex)) {
          return '<div class="loading">Loading order items…</div>';
        }

        const items = plugin.getDetailData(rowIndex);
        if (!items) {
          return '<div class="loading">Loading…</div>';
        }

        return `
          <div class="order-details">
            <h4>Order Items</h4>
            <ul>
              ${items.map((item: any) => `<li>${item.name} — $${item.price}</li>`).join('')}
            </ul>
          </div>
        `;
      },
    },
  },
};
```

**Data flow:**
1. ServerSide fetches master rows in blocks (virtual scroll or pagination) → renders normally (MasterDetail does not claim root data)
2. User scrolls → ServerSide loads more master rows on demand
3. On detail expand → MasterDetailPlugin queries `datasource:fetch-children` with `{ source: 'master-detail', row, rowIndex }`
4. ServerSide calls `getChildRows()` → broadcasts `datasource:children`
5. MasterDetailPlugin stores the data and re-renders the detail panel
6. `detailRenderer` uses `getDetailData(rowIndex)` to access the async data

**Async API:**
```ts
const plugin = grid.getPluginByName('masterDetail');
plugin.getDetailData(rowIndex);     // Get fetched detail data (or undefined if not loaded)
plugin.isDetailLoading(rowIndex);   // Check if detail data is currently loading
```

:::note
Child rows are fetched as a single batch — no pagination. If a detail row has many children, limit the response server-side.
:::

## Nested Grid Example

```ts
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

See [`MasterDetailPlugin`](./classes/masterdetailplugin/) for the full list of methods.

```ts
const plugin = grid.getPluginByName('masterDetail');

plugin.expand(rowIndex);
plugin.collapse(rowIndex);
plugin.toggle(rowIndex);
plugin.expandAll();
plugin.collapseAll();
plugin.isExpanded(rowIndex); // boolean
```

## Styling

The master-detail plugin supports CSS custom properties for theming. Override these on `tbw-grid` or a parent container:

### 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

```css
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

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 |
| `.tbw-row-expanded` | Applied to a master `.data-grid-row` while its detail panel is showing. Use this for theming expanded master rows instead of `[aria-expanded="true"]` (which lives on the toggle button, not the row). |

## Events

| Event           | Detail                                         | Description                          |
| --------------- | ---------------------------------------------- | ------------------------------------ |
| `detail-expand` | `{ rowIndex, row, expanded }`                  | Fired when a detail row is expanded/collapsed |

## See Also

- **[Tree](/grid/plugins/tree.md)** — Hierarchical tree data
- **[Row Grouping](../grouping-rows/)** — Group rows by field values; master-detail toggles appear on data rows within groups
- **[Selection](/grid/plugins/selection.md)** — Row and cell selection
