Vue Integration
The @toolbox-web/grid-vue package provides Vue 3 integration for the <tbw-grid> data grid component.
Compatibility
Section titled “Compatibility”| Vue version | Support level |
|---|---|
| 3.5+ | Tested — used in demos and CI |
| 3.3 – 3.4 | Supported (minimum peer dependency) |
| < 3.3 | Not supported |
| Vue 2 | Not supported — adapter requires Vue 3 Composition API |
Installation
Section titled “Installation”npm install @toolbox-web/grid @toolbox-web/grid-vue
yarn add @toolbox-web/grid @toolbox-web/grid-vuepnpm add @toolbox-web/grid @toolbox-web/grid-vuebun add @toolbox-web/grid @toolbox-web/grid-vueRegister the grid web component and configure Vue to recognize custom elements:
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:
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
Section titled “Basic Usage”The simplest way to use the grid is with the TbwGrid component and feature props:
<script setup lang="ts">// Enable features you useimport '@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
Section titled “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 |
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. |
Programmatic Grid Access
Section titled “Programmatic Grid Access”Use the useGrid composable for programmatic control:
<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
Section titled “Feature-Scoped Composables”Feature imports export scoped composables for type-safe programmatic access to plugin functionality:
<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
Section titled “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:
<script setup>import { useGridSelection } from '@toolbox-web/grid-vue/features/selection';
// Target a specific grid by CSS selectorconst 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)
Section titled “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).
<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:
options.gridElementif explicitly passed (accepts aMaybeRef<DataGridElement>),panelRef.value.closest('tbw-grid')for inline (non-teleported) panels,- The
GRID_ELEMENT_KEYinjection 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
Section titled “Manual Plugin Instantiation”For custom configurations or third-party plugins, instantiate manually using markRaw():
<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
Section titled “Custom Icons”The grid supports two complementary ways to customize icons — see the core Icon Customization reference and the Theming Guide for the full picture:
- CSS variables (
--tbw-icon-*) — preferred for themes and static customization; no JavaScript needed. gridConfig.icons— for dynamic icons, icon libraries, orHTMLElementinstances; takes precedence over CSS.
GridIconProvider is the Vue wrapper for the JS path — it injects icons into gridConfig.icons for every descendant <TbwGrid>:
<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
Section titled “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, GetRowsParams, and GetRowsResult for the full contract — including the params.signal AbortSignal you can forward to fetch() for explicit cancellation.
<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, cacheBlockSize: 100 }" /></template>VueQuery (TanStack Query) Integration
Section titled “VueQuery (TanStack Query) Integration”If your app already uses TanStack Vue Query,
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.
<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, cacheBlockSize: 100 }" /></template>Dynamic Row Updates
Section titled “Dynamic Row Updates”<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
Section titled “Performance Tips”Use markRaw for Config Objects
Section titled “Use markRaw for Config Objects”import { markRaw } from 'vue';
// ✅ Prevents Vue from making config deeply reactiveconst config = markRaw({ columns: [...], features: { selection: true } });Use shallowRef for Large Datasets
Section titled “Use shallowRef for Large Datasets”import { shallowRef } from 'vue';
// ✅ Only tracks reference changes, not deep mutationsconst employees = shallowRef<Employee[]>([]);employees.value = [...employees.value, newEmployee];Memoize Column Configurations
Section titled “Memoize Column Configurations”import { computed, markRaw } from 'vue';
// ✅ Only recreated when dependencies changeconst columns = computed(() => markRaw([ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' },]));Troubleshooting
Section titled “Troubleshooting”Feature prop not working
Section titled “Feature prop not working”Ensure you imported the feature side-effect in your main.ts:
import '@toolbox-web/grid-vue/features/selection';tbw-grid not recognized as a component
Section titled “tbw-grid not recognized as a component”Add the isCustomElement config to your Vite setup:
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('tbw-'), }, },})Row updates not reflected
Section titled “Row updates not reflected”The grid detects updates via reference equality. Always assign a new array:
// ✅ New reference — grid re-rendersemployees.value = [...employees.value, newRow];
// ❌ Same reference — grid won't detect changeemployees.value.push(newRow);See Also
Section titled “See Also”- API Reference — Complete API documentation for all components, composables, and types
- Grid Plugins — All available plugins
- Core Features — Variable row heights, events, column config
- Common Patterns — Reusable recipes