# Common Patterns

> Practical recipes for combining grid features. Covers data browsing, editable grids, master-detail, grouping, export, and more.

Real-world grids rarely use a single feature in isolation. This guide shows **tested combinations** that solve everyday requirements.

## Data Browsing & Selection

**Goal:** Let users sort, filter, and select rows from a large dataset.

#### TypeScript

```typescript
import '@toolbox-web/grid';
import '@toolbox-web/grid/features/selection';
import '@toolbox-web/grid/features/filtering';
import '@toolbox-web/grid/features/multi-sort';
import { queryGrid } from '@toolbox-web/grid';
import type { ColumnConfig } from '@toolbox-web/grid';

const grid = queryGrid<Employee>('#grid');
grid.gridConfig = {
  columns: [
    { field: 'id', header: 'ID', type: 'number', sortable: true },
    { field: 'name', header: 'Name', sortable: true, filterable: true },
    { field: 'department', header: 'Department', filterable: true },
    { field: 'salary', header: 'Salary', type: 'number', sortable: true },
  ],
  features: {
    selection: { mode: 'row', multiSelect: true },
    filtering: { debounceMs: 200 },
    multiSort: true,
  },
};

// React to selected rows
grid.on('selection-change', () => {
  const sel = grid.getPluginByName('selection');
  const rows = sel?.getSelectedRows<Employee>() ?? [];
  console.log('Selected:', rows);
});
```

#### React

```tsx
import '@toolbox-web/grid-react/features/selection';
import '@toolbox-web/grid-react/features/filtering';
import '@toolbox-web/grid-react/features/multi-sort';
import { DataGrid, GridColumn, useGrid } from '@toolbox-web/grid-react';

function EmployeeGrid({ employees }) {
  const gridRef = useGrid<Employee>();

  return (
    <DataGrid
      ref={gridRef}
      rows={employees}
      selection={{ mode: 'row', multiSelect: true }}
      filtering={{ debounceMs: 200 }}
      multiSort
      onSelectionChange={() => {
        const rows = gridRef.current?.getPluginByName('selection')?.getSelectedRows<Employee>() ?? [];
        console.log('Selected:', rows);
      }}
    >
      <GridColumn field="id" header="ID" type="number" sortable />
      <GridColumn field="name" header="Name" sortable filterable />
      <GridColumn field="department" header="Department" filterable />
      <GridColumn field="salary" header="Salary" type="number" sortable />
    </DataGrid>
  );
}
```

#### Vue

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/selection';
import '@toolbox-web/grid-vue/features/filtering';
import '@toolbox-web/grid-vue/features/multi-sort';
import { TbwGrid, TbwGridColumn, useGrid } from '@toolbox-web/grid-vue';

const gridRef = useGrid<Employee>();

function onSelectionChange() {
  const rows = gridRef.value?.getPluginByName('selection')?.getSelectedRows<Employee>() ?? [];
  console.log('Selected:', rows);
}
</script>

<template>
  <TbwGrid ref="gridRef" :rows="employees" selection="row" filtering multi-sort @selection-change="onSelectionChange">
    <TbwGridColumn field="id" header="ID" type="number" sortable />
    <TbwGridColumn field="name" header="Name" sortable filterable />
    <TbwGridColumn field="department" header="Department" filterable />
    <TbwGridColumn field="salary" header="Salary" type="number" sortable />
  </TbwGrid>
</template>
```

#### Angular

```typescript
import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';
import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering';
import { GridMultiSortDirective } from '@toolbox-web/grid-angular/features/multi-sort';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridSelectionDirective, GridFilteringDirective, GridMultiSortDirective],
  template: `
    <tbw-grid
      [rows]="employees"
      [columns]="columns"
      selection="row"
      [filtering]="{ debounceMs: 200 }"
      multiSort
      (selection-change)="onSelectionChange()"
    />
  `,
})
export class EmployeeGridComponent {
  columns = [
    { field: 'id', header: 'ID', type: 'number', sortable: true },
    { field: 'name', header: 'Name', sortable: true, filterable: true },
    { field: 'department', header: 'Department', filterable: true },
    { field: 'salary', header: 'Salary', type: 'number', sortable: true },
  ];

  onSelectionChange() {
    // Access via ViewChild or queryGrid
  }
}
```

:::tip
Use `getSelectedRows()` instead of `getSelectedRowIndices()` — row objects are stable identifiers even after sorting and filtering.
:::

## Editable Grid with Undo

**Goal:** Inline cell editing with full undo/redo support.

#### TypeScript

```typescript
import '@toolbox-web/grid/features/editing';
import '@toolbox-web/grid/features/undo-redo';
import '@toolbox-web/grid/features/selection';

grid.gridConfig = {
  columns: [
    { field: 'id', header: 'ID', type: 'number' },
    { field: 'name', header: 'Name', editable: true },
    { field: 'email', header: 'Email', editable: true },
    { field: 'active', header: 'Active', type: 'boolean', editable: true },
  ],
  features: {
    editing: { editOn: 'dblclick', dirtyTracking: true },
    undoRedo: true,
    selection: 'cell',
  },
  getRowId: (row) => row.id,                 // Required for dirty tracking
};

// Validate before committing
grid.on('cell-commit', (detail, e) => {
  const { field, value } = detail;
  if (field === 'email' && !value.includes('@')) {
    e.preventDefault(); // Reject invalid edit
  }
});

// Track dirty state
grid.on('dirty-change', () => {
  const editing = grid.getPluginByName('editing');
  const dirtyRows = editing?.getDirtyRows() ?? [];
  saveButton.disabled = dirtyRows.length === 0;
});
```

#### React

```tsx
import '@toolbox-web/grid-react/features/editing';
import '@toolbox-web/grid-react/features/undo-redo';
import '@toolbox-web/grid-react/features/selection';
import { DataGrid, GridColumn, useGrid } from '@toolbox-web/grid-react';
import type { CellCommitDetail } from '@toolbox-web/grid';

function EditableGrid({ employees }) {
  const gridRef = useGrid<Employee>();

  const handleCellCommit = useCallback((detail: CellCommitDetail<Employee>, event?: CustomEvent) => {
    if (detail.field === 'email' && !detail.value.includes('@')) {
      event?.preventDefault(); // Reject invalid edit
    }
  }, []);

  const handleDirtyChange = useCallback(() => {
    const editing = gridRef.current?.getPluginByName('editing');
    const dirtyRows = editing?.getDirtyRows() ?? [];
    setSaveDisabled(dirtyRows.length === 0);
  }, []);

  return (
    <DataGrid
      ref={gridRef}
      rows={employees}
      editing="dblclick"
      dirtyTracking
      undoRedo
      selection="cell"
      getRowId={(row) => row.id}
      onCellCommit={handleCellCommit}
      onDirtyChange={handleDirtyChange}
    >
      <GridColumn field="id" header="ID" type="number" />
      <GridColumn field="name" header="Name" editable />
      <GridColumn field="email" header="Email" editable />
      <GridColumn field="active" header="Active" type="boolean" editable />
    </DataGrid>
  );
}
```

#### Vue

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/editing';
import '@toolbox-web/grid-vue/features/undo-redo';
import '@toolbox-web/grid-vue/features/selection';
import { TbwGrid, TbwGridColumn, useGrid } from '@toolbox-web/grid-vue';
import type { CellCommitDetail } from '@toolbox-web/grid';

const gridRef = useGrid<Employee>();
const saveDisabled = ref(true);

function onCellCommit(detail: CellCommitDetail) {
  if (detail.field === 'email' && !detail.value.includes('@')) {
    detail.preventDefault();
  }
}

function onDirtyChange() {
  const editing = gridRef.value?.getPluginByName('editing');
  saveDisabled.value = (editing?.getDirtyRows() ?? []).length === 0;
}
</script>

<template>
  <TbwGrid
    ref="gridRef"
    :rows="employees"
    editing="dblclick"
    dirty-tracking
    undo-redo
    selection="cell"
    :get-row-id="(row) => row.id"
    @cell-commit="onCellCommit"
    @dirty-change="onDirtyChange"
  >
    <TbwGridColumn field="id" header="ID" type="number" />
    <TbwGridColumn field="name" header="Name" editable />
    <TbwGridColumn field="email" header="Email" editable />
    <TbwGridColumn field="active" header="Active" type="boolean" editable />
  </TbwGrid>
</template>
```

#### Angular

```typescript
import { GridEditingDirective } from '@toolbox-web/grid-angular/features/editing';
import { GridUndoRedoDirective } from '@toolbox-web/grid-angular/features/undo-redo';
import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridEditingDirective, GridUndoRedoDirective, GridSelectionDirective],
  template: `
    <tbw-grid
      [rows]="employees"
      [columns]="columns"
      editing="dblclick"
      dirtyTracking
      undoRedo
      selection="cell"
      [getRowId]="getRowId"
      (cell-commit)="onCellCommit($event)"
      (dirty-change)="onDirtyChange()"
    />
  `,
})
export class EditableGridComponent {
  columns = [
    { field: 'id', header: 'ID', type: 'number' },
    { field: 'name', header: 'Name', editable: true },
    { field: 'email', header: 'Email', editable: true },
    { field: 'active', header: 'Active', type: 'boolean', editable: true },
  ];
  getRowId = (row: any) => row.id;

  onCellCommit(event: CustomEvent) {
    const { field, value } = event.detail;
    if (field === 'email' && !value.includes('@')) {
      event.preventDefault();
    }
  }

  onDirtyChange() {
    // Access editing plugin for dirty rows
  }
}
```

:::tip
When using the features API, plugin dependencies are resolved automatically — no need to worry about ordering.
If using the advanced plugins API directly, `EditingPlugin` must be loaded **before** `UndoRedoPlugin`.
:::

## Grouped Data with Aggregates

**Goal:** Group rows by a field and show aggregated values (sum, average, count).

#### TypeScript

```typescript
import '@toolbox-web/grid/features/grouping-rows';
import '@toolbox-web/grid/features/selection';

const columns = [
  { field: 'department', header: 'Department' },
  { field: 'name', header: 'Name' },
  {
    field: 'salary',
    header: 'Salary',
    type: 'number',
    format: (value) => `$${value.toLocaleString()}`,
  },
  { field: 'id', header: 'Count', type: 'number' },
];

grid.gridConfig = {
  columns,
  features: {
    groupingRows: {
      groupOn: (row) => [row.department],
      defaultExpanded: true,
      // Aggregators live on the grouping config, keyed by field name.
      // Built-ins: 'sum' | 'avg' | 'min' | 'max' | 'count' (or a custom function).
      aggregators: { salary: 'avg', id: 'count' },
    },
    selection: 'row',
  },
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/grouping-rows';
import '@toolbox-web/grid-react/features/selection';
import { DataGrid, GridColumn } from '@toolbox-web/grid-react';

function DepartmentGrid({ rows }) {
  return (
    <DataGrid
      rows={rows}
      groupingRows={{
        groupOn: (row) => [row.department],
        defaultExpanded: true,
        aggregators: { salary: 'avg', id: 'count' },
      }}
      selection={{ mode: 'row' }}
    >
      <GridColumn field="department" header="Department" />
      <GridColumn field="name" header="Name" />
      <GridColumn
        field="salary"
        header="Salary"
        type="number"
        format={(value) => `$${value.toLocaleString()}`}
      />
      <GridColumn field="id" header="Count" type="number" />
    </DataGrid>
  );
}
```

#### Vue

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/grouping-rows';
import '@toolbox-web/grid-vue/features/selection';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';

const formatSalary = (value) => `$${value.toLocaleString()}`;
const groupingRows = {
  groupOn: (row) => [row.department],
  defaultExpanded: true,
  aggregators: { salary: 'avg', id: 'count' },
};
</script>

<template>
  <TbwGrid
    :rows="rows"
    :grouping-rows="groupingRows"
    :selection="{ mode: 'row' }"
  >
    <TbwGridColumn field="department" header="Department" />
    <TbwGridColumn field="name" header="Name" />
    <TbwGridColumn
      field="salary"
      header="Salary"
      type="number"
      :format="formatSalary"
    />
    <TbwGridColumn field="id" header="Count" type="number" />
  </TbwGrid>
</template>
```

#### Angular

```typescript
import { GridGroupingRowsDirective } from '@toolbox-web/grid-angular/features/grouping-rows';
import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridGroupingRowsDirective, GridSelectionDirective],
  template: `
    <tbw-grid
      [rows]="rows"
      [columns]="columns"
      [groupingRows]="groupingRows"
      [selection]="{ mode: 'row' }"
    />
  `,
})
export class DepartmentGridComponent {
  groupingRows = {
    groupOn: (row: any) => [row.department],
    defaultExpanded: true,
    aggregators: { salary: 'avg', id: 'count' },
  };
  columns = [
    { field: 'department', header: 'Department' },
    { field: 'name', header: 'Name' },
    {
      field: 'salary', header: 'Salary', type: 'number',
      format: (value: number) => `$${value.toLocaleString()}`,
    },
    { field: 'id', header: 'Count', type: 'number' },
  ];
}
```

## Export Selected Rows

**Goal:** Let users filter data, select a subset, and export it.

#### TypeScript

```typescript
import '@toolbox-web/grid/features/export';
import '@toolbox-web/grid/features/selection';
import '@toolbox-web/grid/features/filtering';

grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Name', filterable: true },
    { field: 'email', header: 'Email' },
    { field: 'department', header: 'Department', filterable: true },
  ],
  features: {
    selection: { mode: 'row', multiSelect: true },
    filtering: true,
    // Always export only selected rows. Omit `onlySelected` (or pass `rowIndices`
    // at call time) to export the full filtered/visible dataset instead.
    export: { onlySelected: true },
  },
};

// Export button
exportButton.addEventListener('click', () => {
  const exp = grid.getPluginByName('export');
  exp?.exportCsv({ fileName: 'employees' }); // .csv extension is added automatically
});
```

#### React

```tsx
import '@toolbox-web/grid-react/features/export';
import '@toolbox-web/grid-react/features/selection';
import '@toolbox-web/grid-react/features/filtering';
import { DataGrid, GridColumn, useGrid } from '@toolbox-web/grid-react';

function ExportableGrid({ employees }) {
  const gridRef = useGrid<Employee>();

  const handleExport = () => {
    const exp = gridRef.current?.getPluginByName('export');
    exp?.exportCsv({ fileName: 'employees' });
  };

  return (
    <>
      <button onClick={handleExport}>Export CSV</button>
      <DataGrid ref={gridRef} rows={employees} selection="row" filtering export>
        <GridColumn field="name" header="Name" filterable />
        <GridColumn field="email" header="Email" />
        <GridColumn field="department" header="Department" filterable />
      </DataGrid>
    </>
  );
}
```

#### Vue

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/export';
import '@toolbox-web/grid-vue/features/selection';
import '@toolbox-web/grid-vue/features/filtering';
import { TbwGrid, TbwGridColumn, useGrid } from '@toolbox-web/grid-vue';

const gridRef = useGrid<Employee>();

function handleExport() {
  gridRef.value?.getPluginByName('export')?.exportCsv({ fileName: 'employees' });
}
</script>

<template>
  <button @click="handleExport">Export CSV</button>
  <TbwGrid ref="gridRef" :rows="employees" selection="row" filtering export>
    <TbwGridColumn field="name" header="Name" filterable />
    <TbwGridColumn field="email" header="Email" />
    <TbwGridColumn field="department" header="Department" filterable />
  </TbwGrid>
</template>
```

#### Angular

```typescript
import { GridExportDirective } from '@toolbox-web/grid-angular/features/export';
import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';
import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridExportDirective, GridSelectionDirective, GridFilteringDirective],
  template: `
    <button (click)="handleExport()">Export CSV</button>
    <tbw-grid #grid [rows]="employees" [columns]="columns" selection="row" filtering export />
  `,
})
export class ExportableGridComponent {
  @ViewChild('grid') gridEl!: ElementRef;
  columns = [
    { field: 'name', header: 'Name', filterable: true },
    { field: 'email', header: 'Email' },
    { field: 'department', header: 'Department', filterable: true },
  ];

  handleExport() {
    this.gridEl.nativeElement.getPluginByName('export')?.exportCsv({ fileName: 'employees' });
  }
}
```

## Master-Detail

**Goal:** Expand a row to show related detail records.

#### TypeScript

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

grid.gridConfig = {
  columns: [
    { field: 'orderId', header: 'Order', type: 'number' },
    { field: 'customer', header: 'Customer' },
    { field: 'total', header: 'Total', type: 'number' },
  ],
  features: {
    masterDetail: {
      // Vanilla signature: (row, rowIndex) => HTMLElement | string
      detailRenderer: (row) => {
        const container = document.createElement('div');
        container.style.padding = '1rem';
        container.innerHTML = `<strong>Order #${row.orderId}</strong>`;

        // Nested grid for order items
        const detail = document.createElement('tbw-grid') as any;
        detail.style.height = '200px';
        detail.style.display = 'block';
        detail.columns = [
          { field: 'item', header: 'Item' },
          { field: 'qty', header: 'Qty', type: 'number' },
          { field: 'price', header: 'Price', type: 'number' },
        ];
        detail.rows = row.items; // Nested data
        container.appendChild(detail);
        return container;
      },
    },
  },
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/master-detail';
import { DataGrid, GridColumn, GridDetailPanel } from '@toolbox-web/grid-react';
import type { DetailPanelContext } from '@toolbox-web/grid-react';

function OrderGrid({ orders }: { orders: Order[] }) {
  return (
    <DataGrid rows={orders}>
      <GridColumn field="orderId" header="Order" type="number" />
      <GridColumn field="customer" header="Customer" />
      <GridColumn field="total" header="Total" type="number" />

      <GridDetailPanel>
        {(ctx: DetailPanelContext<Order>) => (
          <div style={{ padding: '1rem' }}>
            <strong>Order #{ctx.row.orderId}</strong>
            <DataGrid rows={ctx.row.items} style={{ height: 200, display: 'block' }}>
              <GridColumn field="item" header="Item" />
              <GridColumn field="qty" header="Qty" type="number" />
              <GridColumn field="price" header="Price" type="number" />
            </DataGrid>
          </div>
        )}
      </GridDetailPanel>
    </DataGrid>
  );
}
```

#### Vue

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/master-detail';
import { TbwGrid, TbwGridColumn, TbwGridDetailPanel } from '@toolbox-web/grid-vue';
</script>

<template>
  <TbwGrid :rows="orders">
    <TbwGridColumn field="orderId" header="Order" type="number" />
    <TbwGridColumn field="customer" header="Customer" />
    <TbwGridColumn field="total" header="Total" type="number" />

    <TbwGridDetailPanel>
      <template #default="{ row }">
        <div style="padding: 1rem">
          <strong>Order #{{ row.orderId }}</strong>
          <TbwGrid :rows="row.items" style="height: 200px; display: block">
            <TbwGridColumn field="item" header="Item" />
            <TbwGridColumn field="qty" header="Qty" type="number" />
            <TbwGridColumn field="price" header="Price" type="number" />
          </TbwGrid>
        </div>
      </template>
    </TbwGridDetailPanel>
  </TbwGrid>
</template>
```

#### Angular

```typescript
import { GridDetailView } from '@toolbox-web/grid-angular/features/master-detail';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridDetailView],
  template: `
    <tbw-grid [rows]="orders" [columns]="columns">
      <tbw-grid-detail>
        <ng-template let-row>
          <div style="padding: 1rem">
            <strong>Order #{{ row.orderId }}</strong>
            <tbw-grid [rows]="row.items" [columns]="itemColumns"
                      style="height: 200px; display: block">
            </tbw-grid>
          </div>
        </ng-template>
      </tbw-grid-detail>
    </tbw-grid>
  `,
})
export class OrderGridComponent {
  columns = [
    { field: 'orderId', header: 'Order', type: 'number' },
    { field: 'customer', header: 'Customer' },
    { field: 'total', header: 'Total', type: 'number' },
  ];
  itemColumns = [
    { field: 'item', header: 'Item' },
    { field: 'qty', header: 'Qty', type: 'number' },
    { field: 'price', header: 'Price', type: 'number' },
  ];
}
```

## High-Volume Data

**Goal:** Handle 10k+ rows with pinned identifier columns and column virtualization.

#### TypeScript

```typescript
import '@toolbox-web/grid/features/pinned-columns';
import '@toolbox-web/grid/features/column-virtualization';
import '@toolbox-web/grid/features/multi-sort';
import '@toolbox-web/grid/features/filtering';

grid.gridConfig = {
  columns: [
    { field: 'id', header: 'ID', type: 'number', pinned: 'left', width: 80 },
    { field: 'name', header: 'Name', pinned: 'left', width: 180 },
    // ... many more columns
  ],
  features: {
    pinnedColumns: true,
    columnVirtualization: true,
    multiSort: true,
    filtering: { debounceMs: 300 },
  },
  fitMode: 'fixed', // Use natural column widths instead of stretching to fill
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/pinned-columns';
import '@toolbox-web/grid-react/features/column-virtualization';
import '@toolbox-web/grid-react/features/multi-sort';
import '@toolbox-web/grid-react/features/filtering';
import { DataGrid, GridColumn } from '@toolbox-web/grid-react';

function LargeDataGrid({ rows }) {
  return (
    <DataGrid
      rows={rows}
      pinnedColumns
      columnVirtualization
      multiSort
      filtering={{ debounceMs: 300 }}
      fitMode="fixed"
    >
      <GridColumn field="id" header="ID" type="number" pinned="left" width={80} />
      <GridColumn field="name" header="Name" pinned="left" width={180} />
      {/* ... many more columns */}
    </DataGrid>
  );
}
```

#### Vue

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/pinned-columns';
import '@toolbox-web/grid-vue/features/column-virtualization';
import '@toolbox-web/grid-vue/features/multi-sort';
import '@toolbox-web/grid-vue/features/filtering';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
</script>

<template>
  <TbwGrid
    :rows="rows"
    pinned-columns
    column-virtualization
    multi-sort
    :filtering="{ debounceMs: 300 }"
    fit-mode="fixed"
  >
    <TbwGridColumn field="id" header="ID" type="number" pinned="left" :width="80" />
    <TbwGridColumn field="name" header="Name" pinned="left" :width="180" />
    <!-- ... many more columns -->
  </TbwGrid>
</template>
```

#### Angular

```typescript
import { GridPinnedColumnsDirective } from '@toolbox-web/grid-angular/features/pinned-columns';
import { GridColumnVirtualizationDirective } from '@toolbox-web/grid-angular/features/column-virtualization';
import { GridMultiSortDirective } from '@toolbox-web/grid-angular/features/multi-sort';
import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridPinnedColumnsDirective, GridColumnVirtualizationDirective, GridMultiSortDirective, GridFilteringDirective],
  template: `
    <tbw-grid
      [rows]="rows"
      [columns]="columns"
      pinnedColumns
      columnVirtualization
      multiSort
      [filtering]="{ debounceMs: 300 }"
      fitMode="fixed"
    />
  `,
})
export class LargeDataGridComponent {
  columns = [
    { field: 'id', header: 'ID', type: 'number', pinned: 'left', width: 80 },
    { field: 'name', header: 'Name', pinned: 'left', width: 180 },
    // ... many more columns
  ];
}
```

:::tip
For datasets over 50k rows, consider `ServerSidePlugin` to paginate and filter on the server instead of loading all data upfront.
:::

---

## Real-Time / Streaming Data

**Goal:** Push live updates from a WebSocket, SSE, or polling source into the grid efficiently.

Use `applyTransaction()` to batch add, update, and remove operations into a single render cycle.
For high-frequency streams (many messages per second), `applyTransactionAsync()` automatically
merges all calls within one animation frame.

```typescript
import { createGrid } from '@toolbox-web/grid';
import type { RowTransaction } from '@toolbox-web/grid';

const grid = createGrid<Trade>('#my-grid');

// --- Low-to-moderate frequency: applyTransaction ---
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  grid.applyTransaction({
    add:    msg.type === 'add'    ? [msg.row]                               : undefined,
    update: msg.type === 'update' ? [{ id: msg.id, changes: msg.changes }]  : undefined,
    remove: msg.type === 'remove' ? [{ id: msg.id }]                        : undefined,
  });
};

// --- High frequency: applyTransactionAsync ---
// Merges rapid calls within a single animation frame
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  grid.applyTransactionAsync({
    update: [{ id: msg.id, changes: msg.changes }],
  });
};
```

### Transaction shape

```typescript
interface RowTransaction<T> {
  add?:    T[];                           // Appended to the end
  update?: { id: string; changes: Partial<T> }[];  // In-place mutation by row ID
  remove?: { id: string }[];              // Removed by row ID
}
```

Operations are applied in order: **removes → updates → adds**. This ensures updates don't target
rows about to be removed, and new rows don't collide with existing IDs.

### Choosing between sync and async

| Scenario | Method | Animations |
|----------|--------|------------|
| User action, moderate stream (< 10 msg/s) | `applyTransaction()` | Yes (configurable) |
| High-frequency ticker (100+ msg/s) | `applyTransactionAsync()` | Disabled (batched) |

:::tip
Both methods return a `TransactionResult` with the actual `added`, `updated`, and `removed` row objects — useful for logging or post-processing.
:::

## Event Handling Recipes

Practical patterns for wiring grid events into your application — listening across frameworks, reacting to scroll, and acting after a render flush.

### Listening to Events Across Frameworks

The grid dispatches standard `CustomEvent`s that bubble up the DOM. All events use `bubbles: true` and `composed: true`. Cancelable events (`cell-commit`, `row-commit`, `column-move`, `row-move`) honour `preventDefault()` to reject the action — see the [Cancelable Events table](/grid/api-reference.md#cancelable-events).

#### TypeScript

```typescript
import { queryGrid } from '@toolbox-web/grid';

const grid = queryGrid<Employee>('#my-grid');

// Type-safe event listening
grid.on('cell-click', ({ row, field }) => {
  console.log(row, field);
});

// Cancelable events — call preventDefault() to reject the action
grid.on('cell-commit', (detail, e) => {
  if (!isValid(detail.value)) {
    e.preventDefault(); // Rejects the edit
  }
});
```

#### React

```tsx
import { DataGrid } from '@toolbox-web/grid-react';
import type { CellClickDetail, CellCommitDetail } from '@toolbox-web/grid';

function EmployeeGrid() {
  // Event props deliver the unwrapped detail directly
  const handleCellClick = useCallback((detail: CellClickDetail<Employee>) => {
    console.log(detail.row, detail.field);
  }, []);

  const handleCellCommit = useCallback((detail: CellCommitDetail<Employee>, e?: CustomEvent) => {
    if (!isValid(detail.value)) {
      e?.preventDefault(); // Rejects the edit
    }
  }, []);

  return (
    <DataGrid
      rows={employees}
      onCellClick={handleCellClick}
      onCellCommit={handleCellCommit}
    />
  );
}
```

#### Vue

```html
<script setup lang="ts">
import { TbwGrid } from '@toolbox-web/grid-vue';
import type { CellClickDetail, CellCommitDetail } from '@toolbox-web/grid';

function onCellClick(event: CustomEvent<CellClickDetail>) {
  console.log(event.detail.row, event.detail.field);
}

function onCellCommit(event: CustomEvent<CellCommitDetail>) {
  if (!isValid(event.detail.value)) {
    event.preventDefault(); // Rejects the edit
  }
}
</script>

<template>
  <TbwGrid :rows="employees" @cell-click="onCellClick" @cell-commit="onCellCommit" />
</template>
```

#### Angular

```typescript
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { CellClickDetail, CellCommitDetail } from '@toolbox-web/grid';

@Component({
  imports: [Grid],
  template: `
    <tbw-grid
      [rows]="employees"
      (cellClick)="onCellClick($event)"
      (cellCommit)="onCellCommit($event)"
    />
  `,
})
export class EmployeeGridComponent {
  onCellClick(detail: CellClickDetail<Employee>) {
    console.log(detail.row, detail.field);
  }

  onCellCommit(detail: CellCommitDetail<Employee>) {
    if (!isValid(detail.value)) {
      // To cancel, use the native event via kebab-case binding:
      // (cell-commit)="onCellCommit($event)" → event.preventDefault()
    }
  }
}
```

:::note[camelCase vs kebab-case]
Use **camelCase** outputs like `(cellClick)` for typed, unwrapped detail. Use kebab-case `(cell-commit)` when you need `preventDefault()` on the native `CustomEvent`.
:::

### Scroll-Driven Patterns (`tbw-scroll`)

`tbw-scroll` fires (rAF-batched) whenever the grid's vertical viewport scrolls. The detail payload contains `scrollTop`, `scrollHeight`, `clientHeight`, and a `direction: 'vertical'` discriminator (reserved for future horizontal opt-in). The detail is a fresh object literal each tick — safe to retain, freeze, copy into framework state, or post to a worker.

:::tip[Server-side pagination?]
For paginated server-side data, prefer the [`ServerSidePlugin`](/grid/plugins/server-side.md) — it handles block fetching out of the box. `tbw-scroll` is the lower-level primitive for cases the plugin doesn't cover (custom load-more triggers, deferred cell content, scroll-driven UI, etc.).
:::

#### Infinite scroll / load more

```typescript
grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => {
  if (scrollTop + clientHeight >= scrollHeight - 200) {
    loadNextPage();
  }
});
```

#### Sticky scroll-progress indicator

A toolbar or progress bar that lives outside the grid and tracks scroll position:

```typescript
grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => {
  const max = scrollHeight - clientHeight;
  const pct = max > 0 ? scrollTop / max : 0;
  progressBarEl.style.width = `${pct * 100}%`;
});
```

For purely visual scroll-driven CSS effects on a different scroller, also consider the native `animation-timeline: scroll()` — no JS needed.

#### Defer heavy cell content (Angular `@defer` style)

Render expensive content (charts, images, embedded video) only when its row is near the viewport:

```typescript
grid.on('tbw-scroll', ({ scrollTop, clientHeight }) => {
  // Lazy-mount components in the visible window
  hydrateHeavyCellsBetween(scrollTop, scrollTop + clientHeight);
});
```

#### Dismiss overlays on scroll

Tooltips, popovers, context menus rendered outside the grid should typically close when the user scrolls:

```typescript
grid.on('tbw-scroll', () => closeOpenOverlays());
```

:::note[Not for per-row visibility tracking]
`tbw-scroll` fires at most once per frame, **not** per row entering or leaving the viewport. For "row N just became visible" semantics, observe rendered row elements directly in `afterRowRender` or use `IntersectionObserver` from a custom plugin.
:::

**Framework adapter names** — disambiguated to avoid colliding with native scroll events:

| Adapter | Binding |
|---------|---------|
| React | `<DataGrid onTbwScroll={...} />` |
| Vue | `<TbwGrid @tbw-scroll="..." />` |
| Angular | `<tbw-grid (tbwScroll)="..." />` |

### Render-Completed Patterns (`render`)

The `render` event fires once at the end of every render cycle (the single RAF flush in the render scheduler), **after** all plugin `afterRender` hooks have run and **after** `grid.ready()` has resolved. Use this when you need to act on the rendered DOM immediately after a programmatic mutation — without resorting to `setTimeout` or double-`requestAnimationFrame` hacks.

:::tip[`ready()` vs `render` event]
`grid.ready()` resolves **once**, after the first render. The `render` event fires on **every** flush. When you only care about a specific mutation (e.g. the row you just added), attach with `{ once: true }`.
:::

The detail payload includes the highest render phase that ran (`phase`), whether this was the first render (`initial`), the post-`processRows` row count (`rowCount`), and the current virtual window (`visibleRange`, or `null` when virtualization is disabled).

#### Focus the first input after `addRow` in full-grid edit mode

The original motivation: with `editing: { mode: 'grid' }` every row is permanently in edit mode. After inserting a new row you want the first cell's input focused — but the row doesn't exist in the DOM until the next render.

```typescript
function addEmployee() {
  grid.addRow({ id: crypto.randomUUID(), name: '', email: '' });
  grid.addEventListener(
    'render',
    () => {
      const input = grid.querySelector<HTMLInputElement>(
        '[data-row="0"][data-col="0"] input',
      );
      input?.focus();
    },
    { once: true },
  );
}
```

#### Skip cheap scroll renders

The event fires on virtualization-only re-renders too. If you only care about row/column model changes, gate on `phase`:

```typescript
import { RenderPhase } from '@toolbox-web/grid';

grid.on('render', ({ phase, rowCount }) => {
  if (phase < RenderPhase.ROWS) return; // ignore scroll/style-only flushes
  statusBar.textContent = `${rowCount} rows rendered`;
});
```

**Framework adapter names**:

| Adapter | Binding |
|---------|---------|
| React | `<DataGrid onRender={...} />` |
| Vue | `<TbwGrid @render="..." />` |
| Angular | `<tbw-grid (render)="..." />` |

## See Also

  - [Plugins Overview](/grid/plugins.md): Browse all 23 plugins
  - [Events Reference](/grid/api-reference.md#events): Complete event catalog for all plugins
  - [Performance](/grid/guides/performance.md): Virtualization, benchmarks, and optimization tips
  - [API Reference](/grid/api-reference.md): Full property and method reference
