# Vue Integration

> Install and configure @toolbox-web/grid-vue — feature props, slot renderers, editors, composables, and event handling.

The `@toolbox-web/grid-vue` package provides Vue 3 integration for the `<tbw-grid>` data grid component.

:::note[Where to find feature docs]
This page covers Vue-specific setup and APIs (feature props, composables, slot renderers). Core grid features and plugins — cell renderers, editors, events, master-detail, sorting, filtering, selection, etc. — are documented on the [Core](/grid/core.md) and [Plugins](/grid/plugins.md) pages, each with a Vue tab and runnable demos.
:::

## Compatibility

| Vue version | Support level |
| ----------- | ------------- |
| 3.5+        | **Tested** — used in demos and CI |
| 3.3 – 3.4   | Supported (minimum peer dependency) |
| &lt; 3.3    | Not supported |
| Vue 2       | Not supported — adapter requires Vue 3 Composition API |

## Installation

#### npm

  ```bash
  npm install @toolbox-web/grid @toolbox-web/grid-vue
  ```

#### yarn

  ```bash
  yarn add @toolbox-web/grid @toolbox-web/grid-vue
  ```

#### pnpm

  ```bash
  pnpm add @toolbox-web/grid @toolbox-web/grid-vue
  ```

#### bun

  ```bash
  bun add @toolbox-web/grid @toolbox-web/grid-vue
  ```

## Setup

Register the grid web component and configure Vue to recognize custom elements:

```typescript
// main.ts
import '@toolbox-web/grid';

// Enable features via side-effect imports
// Only import the features you use - this keeps your bundle small!
import '@toolbox-web/grid-vue/features/selection';
import '@toolbox-web/grid-vue/features/editing';
import '@toolbox-web/grid-vue/features/multi-sort';
import '@toolbox-web/grid-vue/features/filtering';
```

**Important**: Configure Vue to treat `tbw-*` tags as custom elements in your `vite.config.ts`:

```typescript
// vite.config.ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // Tell Vue that tbw-* tags are web components
          isCustomElement: (tag) => tag.startsWith('tbw-'),
        },
      },
    }),
  ],
});
```

## Basic Usage

The simplest way to use the grid is with the `TbwGrid` component and feature props:

```html
<script setup lang="ts">
// Enable features you use
import '@toolbox-web/grid-vue/features/selection';
import '@toolbox-web/grid-vue/features/editing';
import '@toolbox-web/grid-vue/features/multi-sort';
import '@toolbox-web/grid-vue/features/filtering';

import { TbwGrid } from '@toolbox-web/grid-vue';
import type { ColumnConfig } from '@toolbox-web/grid';
import { ref } from 'vue';

interface Employee {
  id: number;
  name: string;
  department: string;
  salary: number;
}

const employees = ref<Employee[]>([
  { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
  { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 },
  { id: 3, name: 'Charlie', department: 'Sales', salary: 85000 },
]);

const columns: ColumnConfig<Employee>[] = [
  { field: 'id', header: 'ID', type: 'number', width: 70 },
  { field: 'name', header: 'Name', editable: true, sortable: true },
  { field: 'department', header: 'Department', editable: true, sortable: true },
  { field: 'salary', header: 'Salary', type: 'number', format: (v: number) => '$' + v.toLocaleString() },
];
</script>

<template>
  <TbwGrid
    :rows="employees"
    :columns="columns"
    selection="range"
    editing="dblclick"
    :multi-sort="true"
    :filtering="{ debounceMs: 200 }"
    style="height: 400px; display: block"
  />
</template>
```

## Feature Prop Reference

Each feature prop enables a specific plugin with simplified configuration:

| Feature Prop | Feature Import | Description |
|--------------|----------------|-------------|
| `selection` | `features/selection` | Cell, row, or range selection |
| `editing` | `features/editing` | Inline cell editing |
| `multi-sort` | `features/multi-sort` | Multi-column sorting |
| `filtering` | `features/filtering` | Column filtering |
| `clipboard` | `features/clipboard` | Copy/paste support |
| `context-menu` | `features/context-menu` | Right-click context menu |
| `reorder-columns` | `features/reorder-columns` | Column drag-to-reorder |
| `visibility` | `features/visibility` | Column visibility panel |
| `pinned-columns` | `features/pinned-columns` | Sticky left/right columns |
| `pinned-rows` | `features/pinned-rows` | Sticky top/bottom rows |
| `grouping-columns` | `features/grouping-columns` | Multi-level column headers |
| `grouping-rows` | `features/grouping-rows` | Row grouping |
| `column-virtualization` | `features/column-virtualization` | Virtualize columns for wide grids |
| `reorder-rows` | `features/reorder-rows` | Row drag-to-reorder |
| `tree` | `features/tree` | Hierarchical tree view |
| `master-detail` | `features/master-detail` | Expandable detail rows |
| `responsive` | `features/responsive` | Card layout for narrow viewports |
| `undo-redo` | `features/undo-redo` | Edit undo/redo |
| `export` | `features/export` | CSV/Excel export |
| `print` | `features/print` | Print support |
| `pivot` | `features/pivot` | Pivot table functionality |
| `server-side` | `features/server-side` | Server-side data loading |
| `sticky-rows` | `features/sticky-rows` | Pin selected data rows below the header on scroll |
| `row-drag-drop` | `features/row-drag-drop` | Row drag-and-drop (within and across grids) |
| `tooltip` | `features/tooltip` | Cell / header tooltips |

**Core Config Props (no feature import needed):**

| Prop | Type | Description |
|------|------|-------------|
| `sortable` | `boolean` | Grid-wide sorting toggle (default: true) |
| `filterable` | `boolean` | Grid-wide filtering toggle (default: true). Requires FilteringPlugin. |
| `selectable` | `boolean` | Grid-wide selection toggle (default: true). Requires SelectionPlugin. |

## Header & Toolbar Content

Inject Vue components into the grid's shell header (left zone) or toolbar (right zone) via `<TbwGridHeaderContent>` and `<TbwGridToolbarContent>`. These wrap the imperative `registerHeaderContent` / `registerToolbarContent` APIs using Vue's built-in `<Teleport>`, so slot content keeps full reactivity (props, refs, computed, slots, provide/inject).

```html
<script setup lang="ts">
import { TbwGrid, TbwGridHeaderContent, TbwGridToolbarContent } from '@toolbox-web/grid-vue';
import { ref } from 'vue';
const year = ref(new Date().getFullYear());
</script>

<template>
  <TbwGrid :rows="rows" :grid-config="config">
    <TbwGridHeaderContent id="calendar-nav" :order="0">
      <HeaderNav :year="year" @year-change="year = $event" />
    </TbwGridHeaderContent>
    <TbwGridToolbarContent id="calendar-buttons" :order="0">
      <ToolbarNav @prev="prev" @today="today" @next="next" />
    </TbwGridToolbarContent>
  </TbwGrid>
</template>
```

**Props:** `id` (optional — auto-generated if omitted), `order` (default `100`, lower = first). Slot content updates reactively without re-registering with the grid.

## Programmatic Grid Access

Use the `useGrid` composable for programmatic control:

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

interface Employee {
  id: number;
  name: string;
}

const employees = ref<Employee[]>([/* ... */]);
const { gridElement, forceLayout } = useGrid();

function refreshGrid() {
  forceLayout();
}

async function exportCsv() {
  const grid = gridElement.value;
  if (!grid) return;

  const exportPlugin = grid.getPluginByName('export');
  await exportPlugin?.exportToCsv({ filename: 'employees.csv' });
}
</script>

<template>
  <div>
    <button @click="refreshGrid">Refresh</button>
    <button @click="exportCsv">Export CSV</button>
    <TbwGrid :rows="employees" :columns="columns" />
  </div>
</template>
```

## Feature-Scoped Composables

Feature imports export **scoped composables** for type-safe programmatic access to plugin functionality:

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/export';
import { useGridExport } from '@toolbox-web/grid-vue/features/export';
import { TbwGrid } from '@toolbox-web/grid-vue';
import { ref } from 'vue';

const employees = ref<Employee[]>([/* ... */]);
const { exportToCsv, exportToExcel, isExporting } = useGridExport();
</script>

<template>
  <div>
    <button @click="exportToCsv('employees.csv')" :disabled="isExporting()">Export CSV</button>
    <TbwGrid :rows="employees" :columns="columns" export />
  </div>
</template>
```

### Available Feature Composables

See the **API Reference > Composables** section for detailed method signatures, return types, and examples.

All composables accept an optional `selector` parameter to target a specific grid via DOM query instead of Vue's provide/inject. Use when a component contains multiple grids:

```html
<script setup>
import { useGridSelection } from '@toolbox-web/grid-vue/features/selection';

// Target a specific grid by CSS selector
const selection = useGridSelection('tbw-grid.primary');
</script>
```

| Composable | Import | Key Methods |
|------------|--------|-------------|
| `useGridSelection()` | `features/selection` | `selectAll`, `clearSelection`, `getSelection`, `getSelectedRows` |
| `useGridFiltering()` | `features/filtering` | `setFilter`, `clearAllFilters`, `getFilters`, `getFilteredRowCount` |
| `useGridExport()` | `features/export` | `exportToCsv`, `exportToExcel`, `exportToJson`, `isExporting` |
| `useGridPrint()` | `features/print` | `print`, `isPrinting` |
| `useGridUndoRedo()` | `features/undo-redo` | `undo`, `redo`, `canUndo`, `canRedo` |

## Overlay Editors (`useGridOverlay`)

Custom Vue editors that open a popover, listbox, calendar, or other floating panel typically render that panel through `<Teleport>` so it can escape the cell's overflow clipping. Without help, the grid sees a click into the teleported panel as a click _outside_ the editor and commits-and-exits the row before the user can pick an option.

The `useGridOverlay` composable bridges that gap by registering the panel with the grid as an external focus container — the Vue equivalent of Angular's `BaseOverlayEditor.initOverlay()` and React's `useGridOverlay`. Pair it with `aria-expanded` / `aria-controls` on the trigger so even editors that forget to call the composable get correct keyboard / pointer behaviour out of the box (the editing plugin honours those attributes as a generic fallback — see [issue #251](https://github.com/datafabric/toolbox-web/issues/251)).

```html
<script setup lang="ts">
import { ref, useId } from 'vue';
import { useGridOverlay, type GridEditorContext } from '@toolbox-web/grid-vue';

const props = defineProps<{ ctx: GridEditorContext<MyRow> }>();
const open = ref(false);
const panel = ref<HTMLElement | null>(null);
const listboxId = useId();

// Registers panel.value with the grid while open.value === true; unregisters
// automatically on close or unmount.
useGridOverlay(panel, { open });
</script>

<template>
  <input
    role="combobox"
    :aria-expanded="open"
    :aria-controls="listboxId"
    :value="ctx.value ?? ''"
    @click="open = true"
    @keydown.escape="ctx.cancel()"
  />
  <Teleport to="body" v-if="open">
    <div ref="panel" :id="listboxId" role="listbox" class="my-listbox">
      <!-- options that call ctx.commit(option) on click -->
    </div>
  </Teleport>
</template>
```

`useGridOverlay(panelRef, options?)` resolves the owning `<tbw-grid>` in this order:

1. `options.gridElement` if explicitly passed (accepts a `MaybeRef<DataGridElement>`),
2. `panelRef.value.closest('tbw-grid')` for inline (non-teleported) panels,
3. The `GRID_ELEMENT_KEY` injection populated by `<TbwGrid>` / `<GridProvider>`.

`options.open` accepts a `MaybeRef<boolean>` so reactive refs and plain booleans both work. The grid host is also exposed on `ColumnEditorContext.grid` for editors that need to call grid APIs directly without using the composable.

## Manual Plugin Instantiation

For custom configurations or third-party plugins, instantiate manually using `markRaw()`:

```html
<script setup lang="ts">
import { TbwGrid, type GridConfig } from '@toolbox-web/grid-vue';
import { SelectionPlugin, EditingPlugin, ClipboardPlugin } from '@toolbox-web/grid/all';
import { markRaw, ref } from 'vue';

const employees = ref<Employee[]>([/* ... */]);

const config = markRaw<GridConfig<Employee>>({
  columns: [
    { field: 'id', header: 'ID' },
    { field: 'name', header: 'Name', editable: true },
  ],
  plugins: [
    new SelectionPlugin({ mode: 'range', checkbox: true }),
    new EditingPlugin({ editOn: 'dblclick' }),
    new ClipboardPlugin({ includeHeaders: true }),
  ],
});
</script>

<template>
  <TbwGrid :rows="employees" :grid-config="config" />
</template>
```

> **Important**: Always use `markRaw()` for grid configs containing plugins. Vue's reactivity system can interfere with plugin class instances.

Feature props and manual plugins can be mixed — the grid merges them.

## Custom Icons

The grid supports two complementary ways to customize icons — see the [Theming Guide → Icon Customization](/grid/guides/theming.md#icon-customization) for both the CSS and JavaScript approaches:

- **CSS variables** (`--tbw-icon-*`) — preferred for themes and static customization; no JavaScript needed.
- **`gridConfig.icons`** — for dynamic icons, icon libraries, or `HTMLElement` instances; takes precedence over CSS.

`GridIconProvider` is the Vue wrapper for the JS path — it injects `icons` into `gridConfig.icons` for every descendant `<TbwGrid>`:

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

<template>
  <GridIconProvider :icons="{ sortAsc: '↑', sortDesc: '↓' }">
    <TbwGrid :rows="employees" :columns="columns" />
  </GridIconProvider>
</template>
```

## Server-Side Data Loading

For backends that own paging / sorting / filtering, use the `serverSide` feature prop. Your `getRows` handler receives the current `sortModel`, `filterModel`, and block range — return rows and the total count. See [`ServerSideDataSource`](/grid/plugins/server-side/interfaces/serversidedatasource.md), [`GetRowsParams`](/grid/plugins/server-side/interfaces/getrowsparams.md), and [`GetRowsResult`](/grid/plugins/server-side/interfaces/getrowsresult.md) for the full contract — including the `params.signal` `AbortSignal` you can forward to `fetch()` for explicit cancellation.

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/server-side';
import '@toolbox-web/grid-vue/features/multi-sort';
import { TbwGrid } from '@toolbox-web/grid-vue';
import type { ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side';

const dataSource: ServerSideDataSource<Employee> = {
  getRows: async ({ startNode, endNode, sortModel, filterModel, signal }) => {
    const params = new URLSearchParams({
      from: String(startNode),
      to: String(endNode),
    });
    if (sortModel?.length) {
      params.set(
        'sort',
        sortModel.map((s) => `${s.field}:${s.direction}`).join(','),
      );
    }
    if (filterModel) params.set('filter', JSON.stringify(filterModel));
    const response = await fetch(`/api/employees?${params}`, { signal });
    const { rows, totalNodeCount } = await response.json();
    return { rows, totalNodeCount };
  },
};
</script>

<template>
  <TbwGrid
    :columns="columns"
    multi-sort
    :server-side="{ dataSource, pageSize: 100 }"
  />
</template>
```

:::tip
Avoid `gridConfig.sortHandler` for server-side sort — it's bypassed when the multi-sort feature is loaded and only carries a single field. The `serverSide` feature is the supported path and works with both single- and multi-column sort.
:::

### VueQuery (TanStack Query) Integration

If your app already uses [TanStack Vue Query](https://tanstack.com/query/latest/docs/framework/vue/overview),
one useful pairing is to back `ServerSidePlugin` with Vue Query's request cache.
Each block the grid asks for becomes a query keyed by the request shape
(`startNode`, `endNode`, `sortModel`, `filterModel`) — so scrolling back to a
previously fetched range is served from cache instead of re-hitting the API,
and you get retries, in-flight deduplication, and devtools for free.

The pattern: keep `getRows` thin and delegate fetching to `queryClient.fetchQuery`
with a stable key per block.

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/server-side';
import '@toolbox-web/grid-vue/features/multi-sort';
import { TbwGrid } from '@toolbox-web/grid-vue';
import type {
  GetRowsParams,
  GetRowsResult,
  ServerSideDataSource,
} from '@toolbox-web/grid/plugins/server-side';
import { useQueryClient } from '@tanstack/vue-query';

async function fetchEmployeeBlock(
  params: GetRowsParams,
): Promise<GetRowsResult<Employee>> {
  const search = new URLSearchParams({
    from: String(params.startNode),
    to: String(params.endNode),
  });
  if (params.sortModel?.length) {
    search.set('sort', params.sortModel.map((s) => `${s.field}:${s.direction}`).join(','));
  }
  if (params.filterModel) search.set('filter', JSON.stringify(params.filterModel));
  const response = await fetch(`/api/employees?${search}`, { signal: params.signal });
  return response.json();
}

const queryClient = useQueryClient();

const dataSource: ServerSideDataSource<Employee> = {
  // Each block becomes a cached query — Vue Query dedupes in-flight
  // requests, retries failed ones, and serves repeated scrolls from cache.
  getRows: (params) =>
    queryClient.fetchQuery({
      queryKey: [
        'employees',
        params.startNode,
        params.endNode,
        params.sortModel,
        params.filterModel,
      ],
      queryFn: ({ signal }) => fetchEmployeeBlock({ ...params, signal }),
      staleTime: 60_000,
    }),
};
</script>

<template>
  <TbwGrid
    :columns="columns"
    multi-sort
    :server-side="{ dataSource, pageSize: 100 }"
  />
</template>
```

:::tip
When the user mutates a row (commit, paste, etc.), invalidate the query to
trigger a refresh: `queryClient.invalidateQueries({ queryKey: ['employees'] })`.
The grid will re-request the affected blocks on next scroll.
:::

## Dynamic Row Updates

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

const employees = ref<Employee[]>([
  { id: 1, name: 'Alice', department: 'Engineering' },
  { id: 2, name: 'Bob', department: 'Design' },
]);

let nextId = 3;

function handleAdd() {
  employees.value = [
    ...employees.value,
    { id: nextId++, name: 'New Employee', department: 'TBD' },
  ];
}

function removeFirst() {
  employees.value = employees.value.slice(1);
}
</script>

<template>
  <div>
    <button @click="handleAdd">Add</button>
    <button @click="removeFirst">Remove First</button>
    <TbwGrid :rows="employees" :columns="columns" />
  </div>
</template>
```

## Performance Tips

### Use `markRaw` for Config Objects

```typescript
import { markRaw } from 'vue';

// ✅ Prevents Vue from making config deeply reactive
const config = markRaw({ columns: [...], features: { selection: true } });
```

### Use `shallowRef` for Large Datasets

```typescript
import { shallowRef } from 'vue';

// ✅ Only tracks reference changes, not deep mutations
const employees = shallowRef<Employee[]>([]);
employees.value = [...employees.value, newEmployee];
```

### Memoize Column Configurations

```typescript
import { computed, markRaw } from 'vue';

// ✅ Only recreated when dependencies change
const columns = computed(() => markRaw([
  { field: 'id', header: 'ID' },
  { field: 'name', header: 'Name' },
]));
```

## Troubleshooting

### Feature prop not working

Ensure you imported the feature side-effect in your `main.ts`:

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

### `tbw-grid` not recognized as a component

Add the `isCustomElement` config to your Vite setup:

```typescript
// vite.config.ts
vue({
  template: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith('tbw-'),
    },
  },
})
```

### Row updates not reflected

The grid detects updates via reference equality. Always assign a new array:

```typescript
// ✅ New reference — grid re-renders
employees.value = [...employees.value, newRow];

// ❌ Same reference — grid won't detect change
employees.value.push(newRow);
```

## See Also

- **API Reference** — Browse all components, composables, and types via the Vue API section in the sidebar
- **[Grid Plugins](/grid/plugins.md)** — All available plugins
- **[Core Features](/grid/core.md)** — Variable row heights, events, column config
- **[Common Patterns](/grid/guides/common-patterns.md)** — Reusable recipes
