Skip to content

Vue Integration

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

Terminal window
npm install @toolbox-web/grid @toolbox-web/grid-vue
yarn add @toolbox-web/grid @toolbox-web/grid-vue
pnpm add @toolbox-web/grid @toolbox-web/grid-vue
bun add @toolbox-web/grid @toolbox-web/grid-vue

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

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:

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-'),
},
},
}),
],
});

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

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

Each feature prop enables a specific plugin with simplified configuration:

Feature PropFeature ImportDescription
selectionfeatures/selectionCell, row, or range selection
editingfeatures/editingInline cell editing
multi-sortfeatures/multi-sortMulti-column sorting
filteringfeatures/filteringColumn filtering
clipboardfeatures/clipboardCopy/paste support
context-menufeatures/context-menuRight-click context menu
reorder-columnsfeatures/reorder-columnsColumn drag-to-reorder
visibilityfeatures/visibilityColumn visibility panel
pinned-columnsfeatures/pinned-columnsSticky left/right columns
pinned-rowsfeatures/pinned-rowsSticky top/bottom rows
grouping-columnsfeatures/grouping-columnsMulti-level column headers
grouping-rowsfeatures/grouping-rowsRow grouping
column-virtualizationfeatures/column-virtualizationVirtualize columns for wide grids
reorder-rowsfeatures/reorder-rowsRow drag-to-reorder
treefeatures/treeHierarchical tree view
master-detailfeatures/master-detailExpandable detail rows
responsivefeatures/responsiveCard layout for narrow viewports
undo-redofeatures/undo-redoEdit undo/redo
exportfeatures/exportCSV/Excel export
printfeatures/printPrint support
pivotfeatures/pivotPivot table functionality
server-sidefeatures/server-sideServer-side data loading

Core Config Props (no feature import needed):

PropTypeDescription
sortablebooleanGrid-wide sorting toggle (default: true)
filterablebooleanGrid-wide filtering toggle (default: true). Requires FilteringPlugin.
selectablebooleanGrid-wide selection toggle (default: true). Requires SelectionPlugin.

Use the TbwGridColumn component with the #cell slot to customize how cell values are displayed:

<script setup lang="ts">
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
import { ref } from 'vue';
import StatusBadge from './StatusBadge.vue';
interface Employee {
id: number;
name: string;
status: 'active' | 'inactive' | 'pending';
}
const employees = ref<Employee[]>([
{ id: 1, name: 'Alice', status: 'active' },
{ id: 2, name: 'Bob', status: 'inactive' },
{ id: 3, name: 'Charlie', status: 'pending' },
]);
const columns = [
{ field: 'id', header: 'ID', width: 70 },
{ field: 'name', header: 'Name' },
{ field: 'status', header: 'Status' },
];
</script>
<template>
<TbwGrid :rows="employees" :columns="columns" style="height: 400px; display: block">
<!-- Custom renderer for the status column -->
<TbwGridColumn field="status">
<template #cell="{ value, row }">
<StatusBadge :status="value" />
</template>
</TbwGridColumn>
</TbwGrid>
</template>

Slot Props:

PropTypeDescription
valueTValueThe cell value
rowTRowThe full row data object
columnColumnConfigColumn configuration

Use the #editor slot for inline cell editing. The slot provides commit and cancel functions:

<script setup lang="ts">
import '@toolbox-web/grid-vue/features/editing';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
import { ref } from 'vue';
import StatusBadge from './StatusBadge.vue';
import StatusSelectEditor from './StatusSelectEditor.vue';
interface Employee {
id: number;
name: string;
status: 'active' | 'inactive' | 'pending';
}
const employees = ref<Employee[]>([/* ... */]);
const columns = [
{ field: 'id', header: 'ID', width: 70 },
{ field: 'name', header: 'Name', editable: true },
{ field: 'status', header: 'Status', editable: true },
];
</script>
<template>
<TbwGrid
:rows="employees"
:columns="columns"
editing="dblclick"
style="height: 400px; display: block"
>
<TbwGridColumn field="status">
<template #cell="{ value }">
<StatusBadge :status="value" />
</template>
<template #editor="{ value, commit, cancel }">
<StatusSelectEditor
:value="value"
@commit="commit"
@cancel="cancel"
/>
</template>
</TbwGridColumn>
</TbwGrid>
</template>

Editor Slot Props:

PropTypeDescription
valueTValueThe current cell value
rowTRowThe full row data object
columnColumnConfigColumn configuration
commit(value: TValue) => voidCall to save the new value
cancel() => voidCall to cancel editing

Example editor component:

StatusSelectEditor.vue
<script setup lang="ts">
defineProps<{
value: string;
}>();
const emit = defineEmits<{
commit: [value: string];
cancel: [];
}>();
function onChange(event: Event) {
const target = event.target as HTMLSelectElement;
emit('commit', target.value);
}
</script>
<template>
<select :value="value" @change="onChange" @keydown.escape="emit('cancel')">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
</template>

Listen to grid events using standard Vue event binding. Events emit CustomEvent objects — access data via event.detail:

<script setup lang="ts">
import { TbwGrid } from '@toolbox-web/grid-vue';
import type { CellClickDetail, CellCommitDetail, SortChangeDetail } from '@toolbox-web/grid';
function onCellClick(event: CustomEvent<CellClickDetail>) {
const { row, column, value } = event.detail;
console.log('Cell clicked:', { row, column, value });
}
function onCellCommit(event: CustomEvent<CellCommitDetail>) {
const { row, column, newValue, oldValue } = event.detail;
console.log('Cell edited:', { row, column, newValue, oldValue });
}
function onSortChange(event: CustomEvent<SortChangeDetail>) {
const { sortState } = event.detail;
console.log('Sort changed:', sortState);
}
</script>
<template>
<TbwGrid
:rows="employees"
:columns="columns"
@cell-click="onCellClick"
@cell-commit="onCellCommit"
@sort-change="onSortChange"
/>
</template>

Note: Unlike React (which unwraps event.detail as the first argument), the Vue adapter passes the full CustomEvent object to handlers. Access event data via event.detail.

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>

For programmatic event handling beyond template bindings, use useGridEvent. It provides type-safe event subscription with automatic cleanup:

<script setup lang="ts">
import '@toolbox-web/grid-vue/features/editing';
import { TbwGrid, useGrid, useGridEvent } from '@toolbox-web/grid-vue';
import type { CellCommitDetail } from '@toolbox-web/grid';
import { ref } from 'vue';
const editHistory = ref<string[]>([]);
const employees = ref<Employee[]>([/* ... */]);
const { gridElement } = useGrid();
useGridEvent('cell-commit', (event: CustomEvent<CellCommitDetail<Employee>>) => {
const detail = event.detail;
editHistory.value.push(
`${detail.column.field}: ${detail.oldValue}${detail.newValue}`
);
}, gridElement);
</script>
<template>
<div>
<ul><li v-for="(entry, i) in editHistory" :key="i">{{ entry }}</li></ul>
<TbwGrid :rows="employees" :columns="columns" editing="dblclick" />
</div>
</template>

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>
ComposableImportKey Methods
useGridSelection()features/selectionselectAll, clearSelection, getSelection, getSelectedRows
useGridFiltering()features/filteringsetFilter, clearAllFilters, getFilters, getFilteredRowCount
useGridExport()features/exportexportToCsv, exportToExcel, exportToJson, isExporting
useGridPrint()features/printprint, isPrinting
useGridUndoRedo()features/undo-redoundo, redo, canUndo, canRedo

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.

Use GridIconProvider to override grid icons application-wide:

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

Use TbwGridResponsiveCard for mobile-friendly card layouts:

<script setup lang="ts">
import '@toolbox-web/grid-vue/features/responsive';
import { TbwGrid, TbwGridResponsiveCard } from '@toolbox-web/grid-vue';
import { ref } from 'vue';
const employees = ref<Employee[]>([/* ... */]);
</script>
<template>
<TbwGrid :rows="employees" :columns="columns" :responsive="{ breakpoint: 768, cardTitleField: 'name' }">
<TbwGridResponsiveCard v-slot="{ row }">
<div class="card">
<h3>{{ row.name }}</h3>
<p>{{ row.department }} · ${{ row.salary.toLocaleString() }}</p>
</div>
</TbwGridResponsiveCard>
</TbwGrid>
</template>
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/multi-sort';
import { TbwGrid } from '@toolbox-web/grid-vue';
import type { SortState, SortChangeDetail } from '@toolbox-web/grid';
import { ref, watch } from 'vue';
const loading = ref(true);
const employees = ref<Employee[]>([]);
const sortState = ref<SortState[]>([]);
async function fetchData(sort: SortState[]) {
loading.value = true;
try {
const params = new URLSearchParams();
if (sort.length > 0) {
params.set('sortField', sort[0].field);
params.set('sortDir', sort[0].direction);
}
const response = await fetch(`/api/employees?${params}`);
const { data } = await response.json();
employees.value = data;
} finally {
loading.value = false;
}
}
watch(sortState, (newSort) => fetchData(newSort), { immediate: true });
function handleSortChange(event: CustomEvent<SortChangeDetail>) {
sortState.value = event.detail.sortState;
}
</script>
<template>
<TbwGrid
:rows="employees"
:columns="columns"
:multi-sort="{ mode: 'external', sortState: sortState }"
@sort-change="handleSortChange"
/>
</template>
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/editing';
import { TbwGrid } from '@toolbox-web/grid-vue';
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
import type { CellCommitDetail } from '@toolbox-web/grid';
const queryClient = useQueryClient();
const { data: employees, isLoading } = useQuery({
queryKey: ['employees'],
queryFn: () => fetch('/api/employees').then(r => r.json()),
});
const updateMutation = useMutation({
mutationFn: (employee: Partial<Employee>) =>
fetch(`/api/employees/${employee.id}`, {
method: 'PATCH',
body: JSON.stringify(employee),
}),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['employees'] }),
});
function handleCellCommit(event: CustomEvent<CellCommitDetail<Employee>>) {
const detail = event.detail;
updateMutation.mutate({
id: detail.row.id,
[detail.column.field as keyof Employee]: detail.newValue,
});
}
</script>
<template>
<div v-if="isLoading">Loading...</div>
<TbwGrid
v-else
:rows="employees ?? []"
:columns="columns"
editing="dblclick"
@cell-commit="handleCellCommit"
/>
</template>
<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>
import { markRaw } from 'vue';
// ✅ Prevents Vue from making config deeply reactive
const config = markRaw({ columns: [...], features: { selection: true } });
import { shallowRef } from 'vue';
// ✅ Only tracks reference changes, not deep mutations
const employees = shallowRef<Employee[]>([]);
employees.value = [...employees.value, newEmployee];
import { computed, markRaw } from 'vue';
// ✅ Only recreated when dependencies change
const columns = computed(() => markRaw([
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name' },
]));

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

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

Add the isCustomElement config to your Vite setup:

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

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

// ✅ New reference — grid re-renders
employees.value = [...employees.value, newRow];
// ❌ Same reference — grid won't detect change
employees.value.push(newRow);
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt