Appearance
State & events
Grid state
api.getState() returns the complete state of the grid as a serializable object:
ts
type GridState = {
columns: ColumnState; // order, width, visibility, pinning
sort: Array<{ column: string; asc: boolean }>;
filter: Array<{ column: string; value: unknown }>;
globalFilter?: unknown;
grouping?: Array<{ column: string; asc?: boolean }>;
expanded?: Array<ItemUid>;
pagination?: PaginationState;
selection: Array<{ item: ItemUid; column: string }>;
focus: { item?: ItemUid; column?: string };
scroll: { x: number; y: number };
};api.setState(state) restores a state. All properties are optional, so you can store only the parts you need.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
Sort, filter, resize, move or hide columns and reload the page.
<button @click="reset">Reset</button>
</div>
<Datagrid
ref="grid"
:columns="columns"
:rows="rows"
:options="options"
@ready="restore"
@state-change="save"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import {
Datagrid,
type ColumnConfig,
type Core,
type GridOptions,
type GridState,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const STORAGE_KEY = "datagrid-demo-state";
const grid = ref<InstanceType<typeof Datagrid>>();
const rows = createEmployees(300);
function restore(api: Core) {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
api.setState(JSON.parse(saved));
}
}
function save(state: GridState) {
// only persist what makes sense for your application
const { columns, sort, filter, grouping } = state;
localStorage.setItem(STORAGE_KEY, JSON.stringify({ columns, sort, filter, grouping }));
}
function reset() {
localStorage.removeItem(STORAGE_KEY);
const api = grid.value!.core;
api.resetColumnState();
api.clearSort();
api.clearFilters();
api.clearRowGrouping();
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department", groupable: true },
{ field: "city", text: "City", groupable: true },
{ field: "salary", text: "Salary", type: "number" },
];
const options: GridOptions<Employee> = {
columnMenu: true,
defaultColumn: { sortable: true, filterable: true, resizeable: true, moveable: true, width: 150 },
};
</script>tsx
import { useRef } from "react";
import {
Datagrid,
type ColumnConfig,
type Core,
type DatagridHandle,
type GridOptions,
type GridState,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const STORAGE_KEY = "datagrid-demo-state";
const rows = createEmployees(300);
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department", groupable: true },
{ field: "city", text: "City", groupable: true },
{ field: "salary", text: "Salary", type: "number" },
];
const options: GridOptions<Employee> = {
columnMenu: true,
defaultColumn: { sortable: true, filterable: true, resizeable: true, moveable: true, width: 150 },
};
function restore(api: Core) {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
api.setState(JSON.parse(saved));
}
}
function save(state: GridState) {
// only persist what makes sense for your application
const { columns, sort, filter, grouping } = state;
localStorage.setItem(STORAGE_KEY, JSON.stringify({ columns, sort, filter, grouping }));
}
export default function StatePersistence() {
const grid = useRef<DatagridHandle>(null);
function reset() {
localStorage.removeItem(STORAGE_KEY);
const api = grid.current!.api;
api.resetColumnState();
api.clearSort();
api.clearFilters();
api.clearRowGrouping();
}
return (
<div className="demo">
<div className="demo-toolbar">
Sort, filter, resize, move or hide columns and reload the page.
<button onClick={reset}>Reset</button>
</div>
<Datagrid
ref={grid}
columns={columns}
rows={rows}
options={options}
onReady={restore}
onStateChange={save}
/>
</div>
);
}The state-change event (onStateChange in React) is emitted whenever the state changes (sorting, filtering, column changes, resizing, selection, …).
Named views
Users who work with the same data in different ways like to switch between saved layouts ("views"). A view is a part of the grid state: columns (order, width, visibility, pinning), sort and filter. Predefined views can be built with the API (hideColumn, moveColumn, setSortColumns, setColumnFilter), user defined views are saved with getState() and applied with setState(). Hide or move columns (column menu or drag the headers), sort or filter, enter a name and save the view – it is stored in localStorage.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<select v-model="current" @change="applyView(current)">
<optgroup label="Predefined">
<option v-for="name in Object.keys(presets)" :key="name" :value="name">{{ name }}</option>
</optgroup>
<optgroup v-if="Object.keys(views).length" label="My views">
<option v-for="name in Object.keys(views)" :key="name" :value="name">{{ name }}</option>
</optgroup>
</select>
<input v-model="newName" type="text" placeholder="View name" />
<button :disabled="!newName" @click="saveView">Save view</button>
<button :disabled="!views[current]" @click="deleteView">Delete view</button>
</div>
<Datagrid :columns="columns" :rows="rows" :options="options" @ready="api = $event" />
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import {
Datagrid,
type ColumnConfig,
type Core,
type GridOptions,
type GridState,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
type View = Pick<GridState, "columns" | "sort" | "filter">;
const STORAGE_KEY = "datagrid-demo-views";
const rows = createEmployees(300);
const api = shallowRef<Core>();
const current = ref("Default");
const newName = ref("");
const views = ref<Record<string, View>>(loadViews());
function loadViews(): Record<string, View> {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
} catch {
return {};
}
}
function reset(api: Core) {
api.resetColumnState();
api.clearSort();
api.clearFilters();
}
// predefined views are created with the api
const presets: Record<string, (api: Core) => void> = {
Default: reset,
Compensation: (api) => {
reset(api);
api.hideColumn("email");
api.hideColumn("city");
api.setSortColumns([{ column: "salary", asc: false }]);
},
"Engineering by city": (api) => {
reset(api);
api.moveColumn("city", 0);
api.setColumnFilter("department", "Engineering");
api.setSortColumns([{ column: "city", asc: true }]);
},
};
// saved views restore columns (order, width, visibility, pinning), sort and filter
function applyView(name: string) {
if (!api.value) return;
if (presets[name]) {
presets[name](api.value);
} else {
api.value.setState(views.value[name]);
}
}
function saveView() {
const { columns, sort, filter } = api.value!.getState();
views.value = { ...views.value, [newName.value]: { columns, sort, filter } };
localStorage.setItem(STORAGE_KEY, JSON.stringify(views.value));
current.value = newName.value;
newName.value = "";
}
function deleteView() {
const { [current.value]: _, ...rest } = views.value;
views.value = rest;
localStorage.setItem(STORAGE_KEY, JSON.stringify(rest));
current.value = "Default";
applyView("Default");
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "email", text: "Email", width: 240 },
{ field: "department", text: "Department" },
{ field: "city", text: "City" },
{ field: "salary", text: "Salary", type: "number" },
];
const options: GridOptions<Employee> = {
columnMenu: true,
defaultColumn: { sortable: true, filterable: true, resizeable: true, moveable: true, width: 140 },
};
</script>tsx
import { useState } from "react";
import {
Datagrid,
type ColumnConfig,
type Core,
type GridOptions,
type GridState,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
type View = Pick<GridState, "columns" | "sort" | "filter">;
const STORAGE_KEY = "datagrid-demo-views";
const rows = createEmployees(300);
function loadViews(): Record<string, View> {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
} catch {
return {};
}
}
function reset(api: Core) {
api.resetColumnState();
api.clearSort();
api.clearFilters();
}
// predefined views are created with the api
const presets: Record<string, (api: Core) => void> = {
Default: reset,
Compensation: (api) => {
reset(api);
api.hideColumn("email");
api.hideColumn("city");
api.setSortColumns([{ column: "salary", asc: false }]);
},
"Engineering by city": (api) => {
reset(api);
api.moveColumn("city", 0);
api.setColumnFilter("department", "Engineering");
api.setSortColumns([{ column: "city", asc: true }]);
},
};
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "email", text: "Email", width: 240 },
{ field: "department", text: "Department" },
{ field: "city", text: "City" },
{ field: "salary", text: "Salary", type: "number" },
];
const options: GridOptions<Employee> = {
columnMenu: true,
defaultColumn: { sortable: true, filterable: true, resizeable: true, moveable: true, width: 140 },
};
export default function StateViews() {
const [api, setApi] = useState<Core>();
const [current, setCurrent] = useState("Default");
const [newName, setNewName] = useState("");
const [views, setViews] = useState(loadViews);
// saved views restore columns (order, width, visibility, pinning), sort and filter
function applyView(name: string) {
setCurrent(name);
if (!api) return;
if (presets[name]) {
presets[name](api);
} else {
api.setState(views[name]);
}
}
function storeViews(next: Record<string, View>) {
setViews(next);
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
}
function saveView() {
const { columns, sort, filter } = api!.getState();
storeViews({ ...views, [newName]: { columns, sort, filter } });
setCurrent(newName);
setNewName("");
}
function deleteView() {
const { [current]: _, ...rest } = views;
storeViews(rest);
applyView("Default");
}
return (
<div className="demo">
<div className="demo-toolbar">
<select value={current} onChange={(e) => applyView(e.target.value)}>
<optgroup label="Predefined">
{Object.keys(presets).map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</optgroup>
{Object.keys(views).length > 0 && (
<optgroup label="My views">
{Object.keys(views).map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</optgroup>
)}
</select>
<input value={newName} onChange={(e) => setNewName(e.target.value)} type="text" placeholder="View name" />
<button disabled={!newName} onClick={saveView}>
Save view
</button>
<button disabled={!views[current]} onClick={deleteView}>
Delete view
</button>
</div>
<Datagrid columns={columns} rows={rows} options={options} onReady={setApi} />
</div>
);
}Restoring the state after navigation
When the user navigates to another page and back, the grid is created again and loses scroll position, selection and focus. Take a snapshot with getState() before the grid is destroyed and restore it in the ready event (onReady). The complete state includes the scroll position, the selected rows, the focused cell, expanded rows and the page.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<span>Sort, select rows and scroll, then</span>
<button @click="leaveAndReturn">Leave and come back</button>
<label><input v-model="restoreState" type="checkbox" /> Restore state</label>
</div>
<!-- a new key destroys the grid and creates a new one, like navigating to another page and back -->
<Datagrid
:key="visit"
ref="grid"
:columns="columns"
:rows="rows"
:options="options"
@ready="restore"
/>
<pre class="snapshot-json">{{ snapshotText }}</pre>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import {
Datagrid,
type ColumnConfig,
type Core,
type GridOptions,
type GridState,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = createEmployees(500);
const grid = ref<InstanceType<typeof Datagrid>>();
const visit = ref(0);
const restoreState = ref(true);
const snapshot = ref<GridState>();
function leaveAndReturn() {
// e.g. in onBeforeUnmount or a router guard
snapshot.value = grid.value!.getState();
visit.value++;
}
function restore(api: Core) {
if (restoreState.value && snapshot.value) {
api.setState(snapshot.value);
}
}
// the most interesting parts of the state (columns are left out)
const snapshotText = computed(() => {
if (!snapshot.value) return "No snapshot yet";
const { sort, selection, focus, scroll } = snapshot.value;
return JSON.stringify({ sort, selection, focus, scroll });
});
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 70 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
{ field: "salary", text: "Salary", type: "number" },
];
const options: GridOptions<Employee> = {
rowId: (data) => data.id,
selection: "Row",
singleSelect: false,
defaultColumn: { sortable: true, flex: 1 },
};
</script>
<style>
.snapshot-json {
margin: 0;
padding: 6px 8px;
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-family: var(--vp-font-family-mono);
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
</style>tsx
import { useRef, useState } from "react";
import {
Datagrid,
type ColumnConfig,
type Core,
type DatagridHandle,
type GridOptions,
type GridState,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const rows = createEmployees(500);
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 70 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
{ field: "salary", text: "Salary", type: "number" },
];
const options: GridOptions<Employee> = {
rowId: (data) => data.id,
selection: "Row",
singleSelect: false,
defaultColumn: { sortable: true, flex: 1 },
};
export default function StateSnapshot() {
const grid = useRef<DatagridHandle>(null);
const [visit, setVisit] = useState(0);
const [restoreState, setRestoreState] = useState(true);
const [snapshot, setSnapshot] = useState<GridState>();
function leaveAndReturn() {
// e.g. in the cleanup of an effect or a router guard
setSnapshot(grid.current!.getState());
setVisit((visit) => visit + 1);
}
function restore(api: Core) {
if (restoreState && snapshot) {
api.setState(snapshot);
}
}
// the most interesting parts of the state (columns are left out)
let snapshotText = "No snapshot yet";
if (snapshot) {
const { sort, selection, focus, scroll } = snapshot;
snapshotText = JSON.stringify({ sort, selection, focus, scroll });
}
return (
<div className="demo">
<div className="demo-toolbar">
<span>Sort, select rows and scroll, then</span>
<button onClick={leaveAndReturn}>Leave and come back</button>
<label>
<input type="checkbox" checked={restoreState} onChange={(e) => setRestoreState(e.target.checked)} /> Restore
state
</label>
</div>
{/* a new key destroys the grid and creates a new one, like navigating to another page and back */}
<Datagrid key={visit} ref={grid} columns={columns} rows={rows} options={options} onReady={restore} />
<pre className="snapshot-json">{snapshotText}</pre>
<style>{`
.snapshot-json {
margin: 0;
padding: 6px 8px;
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-family: var(--vp-font-family-mono);
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
`}</style>
</div>
);
}Events
| Vue | React | Payload |
|---|---|---|
ready | onReady | grid api (Core) |
cell-click, cell-dblclick, cell-contextmenu | onCellClick, onCellDoubleClick, onCellContextMenu | { event, row, column, data, value } |
header-contextmenu | onHeaderContextMenu | { event, column } |
cell-value-changed | onCellValueChanged | { item, column, data, oldValue, newValue, source } |
selection-change | onSelectionChange | { keys, rows, cells } |
sort-change | onSortedColumnsChange | [{ column, asc }] |
filter-change | onColumnFilterChange | [{ column, value }] |
group-change | onColumnGroupsChange | column ids |
page-change | onPageChange | pagination state |
column-resize | onColumnResize | column (after resizing) |
row-drop | onRowDrop | { row, target, position } |
state-change | onStateChange | grid state |
update:rows | onRowsChange | rows after editing / pasting |
The demo logs the events while you work with the grid. state-change is emitted for almost every interaction, so it is off by default.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<label><input v-model="showStateChange" type="checkbox" /> Log state-change</label>
<button @click="entries = []">Clear</button>
<span>Click, sort, filter, group, resize, select or edit (double click a salary).</span>
</div>
<Datagrid
:columns="columns"
:rows="rows"
:options="options"
@ready="log('ready', 'grid initialized')"
@cell-click="({ column, value }) => log('cell-click', `${column.id} = ${value}`)"
@cell-dblclick="({ column, value }) => log('cell-dblclick', `${column.id} = ${value}`)"
@cell-contextmenu="({ column }) => log('cell-contextmenu', column.id)"
@header-contextmenu="({ column }) => log('header-contextmenu', column.id)"
@selection-change="({ keys }) => log('selection-change', `${keys.length} rows`)"
@sort-change="(sort) => log('sort-change', JSON.stringify(sort))"
@filter-change="(filter) => log('filter-change', JSON.stringify(filter))"
@group-change="(groups) => log('group-change', JSON.stringify(groups))"
@page-change="({ pageIndex }) => log('page-change', `page ${pageIndex + 1}`)"
@column-resize="(column) => log('column-resize', `${column.id} ${column.width}px`)"
@cell-value-changed="(e) => log('cell-value-changed', `${e.column.id}: ${e.oldValue} → ${e.newValue}`)"
@update:rows="(rows) => log('update:rows', `${rows.length} rows`)"
@state-change="() => showStateChange && log('state-change', 'getState() changed')"
/>
<div class="demo-log events-log">
<div v-for="entry in entries" :key="entry.id">
<b>{{ entry.name }}</b> {{ entry.detail }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = createEmployees(100);
const showStateChange = ref(false);
const entries = ref<Array<{ id: number; name: string; detail: string }>>([]);
let id = 0;
function log(name: string, detail: string) {
entries.value = [{ id: ++id, name, detail }, ...entries.value.slice(0, 49)];
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department", groupable: true },
{ field: "salary", text: "Salary", type: "number", editable: true },
];
const options: GridOptions<Employee> = {
selection: "Row",
singleSelect: false,
pagination: { enabled: true, pageSize: 20 },
defaultColumn: { sortable: true, filterable: true, resizeable: true, flex: 1 },
};
</script>
<style>
.demo-log.events-log {
height: 110px;
max-height: none;
}
</style>tsx
import { useRef, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const rows = createEmployees(100);
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department", groupable: true },
{ field: "salary", text: "Salary", type: "number", editable: true },
];
const options: GridOptions<Employee> = {
selection: "Row",
singleSelect: false,
pagination: { enabled: true, pageSize: 20 },
defaultColumn: { sortable: true, filterable: true, resizeable: true, flex: 1 },
};
export default function EventsLog() {
const [showStateChange, setShowStateChange] = useState(false);
const [entries, setEntries] = useState<Array<{ id: number; name: string; detail: string }>>([]);
const id = useRef(0);
function log(name: string, detail: string) {
const entry = { id: ++id.current, name, detail };
setEntries((entries) => [entry, ...entries.slice(0, 49)]);
}
return (
<div className="demo">
<div className="demo-toolbar">
<label>
<input type="checkbox" checked={showStateChange} onChange={(e) => setShowStateChange(e.target.checked)} />{" "}
Log state-change
</label>
<button onClick={() => setEntries([])}>Clear</button>
<span>Click, sort, filter, group, resize, select or edit (double click a salary).</span>
</div>
<Datagrid
columns={columns}
rows={rows}
options={options}
onReady={() => log("onReady", "grid initialized")}
onCellClick={({ column, value }) => log("onCellClick", `${column.id} = ${value}`)}
onCellDoubleClick={({ column, value }) => log("onCellDoubleClick", `${column.id} = ${value}`)}
onCellContextMenu={({ column }) => log("onCellContextMenu", column.id)}
onHeaderContextMenu={({ column }) => log("onHeaderContextMenu", column.id)}
onSelectionChange={({ keys }) => log("onSelectionChange", `${keys.length} rows`)}
onSortedColumnsChange={(sort) => log("onSortedColumnsChange", JSON.stringify(sort))}
onColumnFilterChange={(filter) => log("onColumnFilterChange", JSON.stringify(filter))}
onColumnGroupsChange={(groups) => log("onColumnGroupsChange", JSON.stringify(groups))}
onPageChange={({ pageIndex }) => log("onPageChange", `page ${pageIndex + 1}`)}
onColumnResize={(column) => log("onColumnResize", `${column.id} ${column.width}px`)}
onCellValueChanged={(e) => log("onCellValueChanged", `${e.column.id}: ${e.oldValue} → ${e.newValue}`)}
onRowsChange={(rows) => log("onRowsChange", `${rows.length} rows`)}
onStateChange={() => showStateChange && log("onStateChange", "getState() changed")}
/>
<div className="demo-log events-log">
{entries.map((entry) => (
<div key={entry.id}>
<b>{entry.name}</b> {entry.detail}
</div>
))}
</div>
<style>{`
.demo-log.events-log {
height: 110px;
max-height: none;
}
`}</style>
</div>
);
}Example: your own context menu
The grid has a built-in context menu. If you need a completely different menu, handle the cell-contextmenu event (onCellContextMenu) and call event.preventDefault():
Source
vue
<template>
<div class="demo" @click="menu = undefined">
<Datagrid
ref="grid"
:columns="columns"
v-model:rows="rows"
:options="options"
@cell-contextmenu="openMenu"
/>
<ul
v-if="menu"
class="context-menu"
:style="{ left: `${menu.x}px`, top: `${menu.y}px` }"
>
<li @click="copy">Copy "{{ menu.text }}"</li>
<li @click="duplicate">Duplicate row</li>
<li @click="remove">Delete row</li>
<li @click="grid?.exportCsv({ onlySelected: true, fileName: 'selection.csv' })">
Export selected rows
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import {
Datagrid,
getText,
type CellEventPayload,
type ColumnConfig,
type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const grid = ref<InstanceType<typeof Datagrid>>();
const rows = ref(createEmployees(50));
const menu = ref<{ x: number; y: number; text: string; data: Employee }>();
function openMenu({ event, row, column, data }: CellEventPayload<Employee>) {
event.preventDefault(); // hide the browser context menu
const mouse = event as MouseEvent;
menu.value = {
x: mouse.clientX,
y: mouse.clientY,
text: getText(column, row),
data,
};
}
function copy() {
navigator.clipboard?.writeText(menu.value!.text);
}
function duplicate() {
const index = rows.value.indexOf(menu.value!.data);
const copy = { ...menu.value!.data, id: Date.now() };
rows.value = [...rows.value.slice(0, index + 1), copy, ...rows.value.slice(index + 1)];
}
function remove() {
rows.value = rows.value.filter((row) => row !== menu.value!.data);
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 110 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
];
const options: GridOptions<Employee> = {
rowId: (data) => data.id,
selection: "Row",
singleSelect: false,
defaultColumn: { flex: 1 },
};
</script>
<style>
.context-menu {
position: fixed;
z-index: 100;
margin: 0;
padding: 4px;
list-style: none;
background: var(--vp-c-bg, white);
border: 1px solid var(--vp-c-divider, #ddd);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
font-size: 14px;
}
.context-menu li {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.context-menu li:hover {
background: var(--vp-c-bg-soft, #f3f3f3);
}
</style>tsx
import { useRef, useState } from "react";
import {
Datagrid,
getText,
type CellEventPayload,
type ColumnConfig,
type DatagridHandle,
type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 110 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
];
const options: GridOptions<Employee> = {
rowId: (data) => data.id,
selection: "Row",
singleSelect: false,
defaultColumn: { flex: 1 },
};
type Menu = { x: number; y: number; text: string; data: Employee };
export default function ContextMenu() {
const grid = useRef<DatagridHandle>(null);
const [rows, setRows] = useState(() => createEmployees(50));
const [menu, setMenu] = useState<Menu>();
function openMenu({ event, row, column, data }: CellEventPayload<Employee>) {
event.preventDefault(); // hide the browser context menu
const mouse = event as MouseEvent;
setMenu({
x: mouse.clientX,
y: mouse.clientY,
text: getText(column, row),
data,
});
}
function copy() {
navigator.clipboard?.writeText(menu!.text);
}
function duplicate() {
const index = rows.indexOf(menu!.data);
const copy = { ...menu!.data, id: Date.now() };
setRows([...rows.slice(0, index + 1), copy, ...rows.slice(index + 1)]);
}
function remove() {
setRows(rows.filter((row) => row !== menu!.data));
}
return (
<div className="demo" onClick={() => setMenu(undefined)}>
<Datagrid
ref={grid}
columns={columns}
rows={rows}
onRowsChange={setRows}
options={options}
onCellContextMenu={openMenu}
/>
{menu && (
<ul className="context-menu" style={{ left: `${menu.x}px`, top: `${menu.y}px` }}>
<li onClick={copy}>Copy "{menu.text}"</li>
<li onClick={duplicate}>Duplicate row</li>
<li onClick={remove}>Delete row</li>
<li onClick={() => grid.current?.exportCsv({ onlySelected: true, fileName: "selection.csv" })}>
Export selected rows
</li>
</ul>
)}
<style>{`
.context-menu {
position: fixed;
z-index: 100;
margin: 0;
padding: 4px;
list-style: none;
background: var(--vp-c-bg, white);
border: 1px solid var(--vp-c-divider, #ddd);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
font-size: 14px;
}
.context-menu li {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.context-menu li:hover {
background: var(--vp-c-bg-soft, #f3f3f3);
}
`}</style>
</div>
);
}Low level callbacks
The events prop accepts all callbacks of the core (CoreDelegate), e.g. onCellKeyDown, onScrollChanged, onResize, onHoverChanged, onFocusChanged. Use them for special cases like custom keyboard handling.