Appearance
Editing
Cells of columns with editable: true can be edited.
Source
vue
<template>
<div class="demo">
<Datagrid
:columns="columns"
v-model:rows="rows"
:options="options"
@cell-value-changed="onChanged"
/>
<div class="demo-log">
<div v-for="(entry, index) in log" :key="index">{{ entry }}</div>
<div v-if="!log.length">Double click a cell or start typing…</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import {
Datagrid,
type CellValueChangedEvent,
type ColumnConfig,
type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = ref(createEmployees(100));
const log = ref<Array<string>>([]);
function onChanged(e: CellValueChangedEvent<Employee>) {
log.value.unshift(
`${e.column.id}: ${JSON.stringify(e.oldValue)} → ${JSON.stringify(e.newValue)} (${e.source})`
);
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "Text" },
{
field: "department",
text: "Select",
editor: "select",
editorParams: {
options: ["Engineering", "Sales", "Marketing", "Support", "Finance", "HR"],
},
},
{ field: "salary", text: "Number", type: "number" },
{ field: "startDate", text: "Date", type: "date" },
{ field: "remote", text: "Checkbox", type: "boolean" },
{
field: "rating",
text: "Validated (0-5)",
type: "number",
// invalid values are rejected by returning the old value
valueParser: ({ value, oldValue }) => {
const number = Number(String(value).replace(",", "."));
return isNaN(number) || number < 0 || number > 5 ? oldValue : number;
},
},
{
field: "email",
text: "Only editable for remote",
width: 250,
editable: ({ data }) => data.remote,
},
];
const options: GridOptions<Employee> = {
selection: "Cell",
singleSelect: false,
enterMovesDown: true,
defaultColumn: { editable: true, resizeable: true, width: 140 },
};
</script>tsx
import { useState } from "react";
import {
Datagrid,
type CellValueChangedEvent,
type ColumnConfig,
type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "Text" },
{
field: "department",
text: "Select",
editor: "select",
editorParams: {
options: ["Engineering", "Sales", "Marketing", "Support", "Finance", "HR"],
},
},
{ field: "salary", text: "Number", type: "number" },
{ field: "startDate", text: "Date", type: "date" },
{ field: "remote", text: "Checkbox", type: "boolean" },
{
field: "rating",
text: "Validated (0-5)",
type: "number",
// invalid values are rejected by returning the old value
valueParser: ({ value, oldValue }) => {
const number = Number(String(value).replace(",", "."));
return isNaN(number) || number < 0 || number > 5 ? oldValue : number;
},
},
{
field: "email",
text: "Only editable for remote",
width: 250,
editable: ({ data }) => data.remote,
},
];
const options: GridOptions<Employee> = {
selection: "Cell",
singleSelect: false,
enterMovesDown: true,
defaultColumn: { editable: true, resizeable: true, width: 140 },
};
export default function Editing() {
const [rows, setRows] = useState(() => createEmployees(100));
const [log, setLog] = useState<Array<string>>([]);
function onChanged(e: CellValueChangedEvent<Employee>) {
const entry = `${e.column.id}: ${JSON.stringify(e.oldValue)} → ${JSON.stringify(e.newValue)} (${e.source})`;
setLog((log) => [entry, ...log]);
}
return (
<div className="demo">
<Datagrid
columns={columns}
rows={rows}
onRowsChange={setRows}
options={options}
onCellValueChanged={onChanged}
/>
<div className="demo-log">
{log.map((entry, index) => (
<div key={index}>{entry}</div>
))}
{!log.length && <div>Double click a cell or start typing…</div>}
</div>
</div>
);
}Start and stop editing
| Action | Result |
|---|---|
| double click | starts editing (configurable with editTrigger) |
| typing a character | starts editing and replaces the value with the typed character |
| F2 / Enter | starts editing with the current value |
| Enter (while editing) | saves the value; moves down if enterMovesDown: true |
| Tab / Shift + Tab | saves and edits the next / previous cell |
| Escape | cancels editing |
| click outside of the cell | saves the value |
| Delete / Backspace | clears the value of the focused cell |
editTrigger: "click" starts editing with a single click, "none" disables starting with the mouse.
Use "click" for grids which are mainly used for data entry (like a form with many rows), keep "dblclick" when the user mostly reads, selects and copies. "none" still allows editing with the keyboard (typing, F2, Enter). Combine it with enterMovesDown: true to enter a column of values like in a spreadsheet: type, Enter, type, Enter, …
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<label>
editTrigger
<select v-model="editTrigger">
<option value="click">click</option>
<option value="dblclick">dblclick</option>
<option value="none">none (keyboard only)</option>
</select>
</label>
<label><input v-model="enterMovesDown" type="checkbox" /> enterMovesDown</label>
</div>
<Datagrid :columns="columns" :rows="rows" :options="options" />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = createEmployees(100);
const editTrigger = ref<"click" | "dblclick" | "none">("click");
const enterMovesDown = ref(true);
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "salary", text: "Salary", type: "number" },
{ field: "bonus", text: "Bonus", type: "number" },
{ field: "remote", text: "Remote", type: "boolean" },
];
const options = computed<GridOptions<Employee>>(() => ({
// "click": a single click starts editing, like in a form
editTrigger: editTrigger.value,
// Enter saves and moves to the cell below, like in a spreadsheet
enterMovesDown: enterMovesDown.value,
defaultColumn: { editable: true, flex: 1 },
}));
</script>tsx
import { useMemo, 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: "salary", text: "Salary", type: "number" },
{ field: "bonus", text: "Bonus", type: "number" },
{ field: "remote", text: "Remote", type: "boolean" },
];
export default function EditingTrigger() {
const [editTrigger, setEditTrigger] = useState<"click" | "dblclick" | "none">("click");
const [enterMovesDown, setEnterMovesDown] = useState(true);
const options = useMemo<GridOptions<Employee>>(
() => ({
// "click": a single click starts editing, like in a form
editTrigger,
// Enter saves and moves to the cell below, like in a spreadsheet
enterMovesDown,
defaultColumn: { editable: true, flex: 1 },
}),
[editTrigger, enterMovesDown]
);
return (
<div className="demo">
<div className="demo-toolbar">
<label>
editTrigger{" "}
<select value={editTrigger} onChange={(e) => setEditTrigger(e.target.value as typeof editTrigger)}>
<option value="click">click</option>
<option value="dblclick">dblclick</option>
<option value="none">none (keyboard only)</option>
</select>
</label>
<label>
<input type="checkbox" checked={enterMovesDown} onChange={(e) => setEnterMovesDown(e.target.checked)} />{" "}
enterMovesDown
</label>
</div>
<Datagrid columns={columns} rows={rows} options={options} />
</div>
);
}Editors
The editor depends on the column type or on editor:
editor | Default for | Input |
|---|---|---|
"text" | text | single line input |
"textarea" | columns with autoHeight | multi line input (Shift + Enter for new lines) |
"number" | type: "number" | input with decimal keyboard |
"date" | type: "date" | date picker |
"checkbox" | type: "boolean" | checkbox |
"select" | – | dropdown with editorParams.options |
ts
{
field: "status",
editable: true,
editor: "select",
editorParams: {
options: [
{ value: "open", label: "Open" },
{ value: "done", label: "Done" },
],
},
}editorParams also supports placeholder, and min, max, step for your own editors.
Select and date editors
Select options are either plain values ("Anna Müller") or { value, label } objects when the stored value differs from the displayed text. The editor shows the labels, the data gets the value – add a valueFormatter so that the cell shows the label as well. Date columns open the browser's date picker and keep the format of the data (ISO strings stay ISO strings, Date objects stay Date objects). The title editor shows a placeholder when it is empty.
Source
vue
<template>
<div class="demo">
<Datagrid :columns="columns" v-model:rows="rows" :options="options" @cell-value-changed="onChanged" />
<div class="demo-log">{{ log }}</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import {
Datagrid,
type CellValueChangedEvent,
type ColumnConfig,
type GridOptions,
type SelectOption,
} from "@datagrid/vue-ui";
import { createTasks, type Task } from "./data";
const rows = ref(createTasks(50));
const log = ref("Double click a cell to open the editor.");
// value = stored in the data, label = shown in the dropdown
const priorities: Array<SelectOption> = [
{ value: "high", label: "↑ High" },
{ value: "medium", label: "→ Medium" },
{ value: "low", label: "↓ Low" },
];
const statuses: Array<SelectOption> = [
{ value: "todo", label: "To do" },
{ value: "in progress", label: "In progress" },
{ value: "review", label: "Review" },
{ value: "done", label: "Done" },
];
// shows the label of an option in the cell
const optionLabel =
(options: Array<SelectOption>) =>
({ value }: { value: unknown }) =>
options.find((option) => option.value === value)?.label ?? String(value ?? "");
const columns: Array<ColumnConfig<Task>> = [
{ field: "title", text: "Title", flex: 2, editorParams: { placeholder: "What needs to be done?" } },
{
field: "assignee",
text: "Assignee",
editor: "select",
// plain strings are value and label
editorParams: { options: ["Anna Müller", "Ben Schmidt", "Clara Weber", "David Koch"] },
},
{
field: "status",
text: "Status",
editor: "select",
editorParams: { options: statuses },
valueFormatter: optionLabel(statuses),
},
{
field: "priority",
text: "Priority",
editor: "select",
editorParams: { options: priorities },
valueFormatter: optionLabel(priorities),
},
// date columns use the date picker, the ISO string format of the data is kept
{ field: "due", text: "Due", type: "date" },
];
const options: GridOptions<Task> = {
rowId: (data) => data.id,
defaultColumn: { editable: true, flex: 1, minWidth: 100 },
};
function onChanged({ data, column, newValue }: CellValueChangedEvent<Task>) {
log.value = `#${data.id} ${column.id} = ${JSON.stringify(newValue)}`;
}
</script>tsx
import { useState } from "react";
import {
Datagrid,
type CellValueChangedEvent,
type ColumnConfig,
type GridOptions,
type SelectOption,
} from "@datagrid/react-ui";
import { createTasks, type Task } from "../data";
// value = stored in the data, label = shown in the dropdown
const priorities: Array<SelectOption> = [
{ value: "high", label: "↑ High" },
{ value: "medium", label: "→ Medium" },
{ value: "low", label: "↓ Low" },
];
const statuses: Array<SelectOption> = [
{ value: "todo", label: "To do" },
{ value: "in progress", label: "In progress" },
{ value: "review", label: "Review" },
{ value: "done", label: "Done" },
];
// shows the label of an option in the cell
const optionLabel =
(options: Array<SelectOption>) =>
({ value }: { value: unknown }) =>
options.find((option) => option.value === value)?.label ?? String(value ?? "");
const columns: Array<ColumnConfig<Task>> = [
{ field: "title", text: "Title", flex: 2, editorParams: { placeholder: "What needs to be done?" } },
{
field: "assignee",
text: "Assignee",
editor: "select",
// plain strings are value and label
editorParams: { options: ["Anna Müller", "Ben Schmidt", "Clara Weber", "David Koch"] },
},
{
field: "status",
text: "Status",
editor: "select",
editorParams: { options: statuses },
valueFormatter: optionLabel(statuses),
},
{
field: "priority",
text: "Priority",
editor: "select",
editorParams: { options: priorities },
valueFormatter: optionLabel(priorities),
},
// date columns use the date picker, the ISO string format of the data is kept
{ field: "due", text: "Due", type: "date" },
];
const options: GridOptions<Task> = {
rowId: (data) => data.id,
defaultColumn: { editable: true, flex: 1, minWidth: 100 },
};
export default function EditingSelect() {
const [rows, setRows] = useState(() => createTasks(50));
const [log, setLog] = useState("Double click a cell to open the editor.");
function onChanged({ data, column, newValue }: CellValueChangedEvent<Task>) {
setLog(`#${data.id} ${column.id} = ${JSON.stringify(newValue)}`);
}
return (
<div className="demo">
<Datagrid
columns={columns}
rows={rows}
onRowsChange={setRows}
options={options}
onCellValueChanged={onChanged}
/>
<div className="demo-log">{log}</div>
</div>
);
}Editable depending on the row
ts
{ field: "price", editable: ({ data }) => !data.locked }Parsing and validating values
The value of an editor is converted with valueParser before it is written. The built-in types provide parsers (numbers, dates, booleans). A parser can reject a value by returning the old value:
ts
{
field: "age",
type: "number",
editable: true,
valueParser: ({ value, oldValue }) => {
const age = Number(value);
return Number.isInteger(age) && age >= 0 ? age : oldValue;
},
}The demo validates salaries and emails with a valueParser and makes only active employees editable:
Source
vue
<template>
<div class="demo">
<Datagrid :columns="columns" :rows="rows" :options="options" @cell-value-changed="onChanged" />
<div class="demo-log">{{ log }}</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, type CellValueChangedEvent, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = createEmployees(100);
const log = ref("Edit a salary (30,000 – 200,000) or an email. Inactive employees are read-only.");
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{
field: "salary",
text: "Salary",
type: "number",
// returning the old value rejects the input
valueParser: ({ value, oldValue }) => {
const salary = Number(value);
if (Number.isFinite(salary) && salary >= 30000 && salary <= 200000) {
return salary;
}
log.value = `rejected salary "${value}"`;
return oldValue;
},
},
{
field: "email",
text: "Email",
valueParser: ({ value, oldValue }) => {
if (/^\S+@\S+\.\S+$/.test(String(value))) {
return String(value).toLowerCase();
}
log.value = `rejected email "${value}"`;
return oldValue;
},
},
{ field: "active", text: "Active", type: "boolean", width: 90, editable: true },
];
const options: GridOptions<Employee> = {
// editable depending on the row
defaultColumn: { flex: 1, editable: ({ data }) => data.active },
rowClass: (row) => (row.data.active ? undefined : "row-inactive"),
enterMovesDown: true,
};
function onChanged({ data, column, oldValue, newValue }: CellValueChangedEvent<Employee>) {
log.value = `${data.firstName} ${data.lastName}: ${column.id} ${oldValue} → ${newValue}`;
}
</script>
<style>
.row-inactive {
color: var(--ut-muted-color);
}
</style>tsx
import { useMemo, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const rows = createEmployees(100);
const options: GridOptions<Employee> = {
// editable depending on the row
defaultColumn: { flex: 1, editable: ({ data }) => data.active },
rowClass: (row) => (row.data.active ? undefined : "row-inactive"),
enterMovesDown: true,
};
export default function EditingValidation() {
const [log, setLog] = useState("Edit a salary (30,000 – 200,000) or an email. Inactive employees are read-only.");
const columns = useMemo<Array<ColumnConfig<Employee>>>(
() => [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{
field: "salary",
text: "Salary",
type: "number",
// returning the old value rejects the input
valueParser: ({ value, oldValue }) => {
const salary = Number(value);
if (Number.isFinite(salary) && salary >= 30000 && salary <= 200000) {
return salary;
}
setLog(`rejected salary "${value}"`);
return oldValue;
},
},
{
field: "email",
text: "Email",
valueParser: ({ value, oldValue }) => {
if (/^\S+@\S+\.\S+$/.test(String(value))) {
return String(value).toLowerCase();
}
setLog(`rejected email "${value}"`);
return oldValue;
},
},
{ field: "active", text: "Active", type: "boolean", width: 90, editable: true },
],
[]
);
return (
<div className="demo">
<style>{`.row-inactive { color: var(--ut-muted-color); }`}</style>
<Datagrid
columns={columns}
rows={rows}
options={options}
onCellValueChanged={({ data, column, oldValue, newValue }) =>
setLog(`${data.firstName} ${data.lastName}: ${column.id} ${oldValue} → ${newValue}`)
}
/>
<div className="demo-log">{log}</div>
</div>
);
}Writing values
By default the value is written to data[field] (also for nested fields like address.city). Computed columns need a valueSetter:
ts
{
id: "fullName",
valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`,
valueSetter: ({ data, value }) => {
[data.firstName, data.lastName] = String(value).split(" ");
},
editable: true,
}Events
After a value was changed, @cell-value-changed is emitted and the rows are sorted and filtered again:
ts
function onChanged({ data, column, oldValue, newValue, source }: CellValueChangedEvent<Person>) {
// source: "edit" | "paste" | "clear" | "cut" | "fill" | "undo" | "redo" | "api"
api.save(data);
}Additionally update:rows is emitted, so v-model:rows keeps your data in sync.
The event prop is onCellValueChanged. Additionally onRowsChange receives the changed rows array, so you can keep your state in sync: <Datagrid rows={rows} onRowsChange={setRows} />.
Saving changes on the server
The event is the place to save a change. The grid doesn't wait for your request, the user can continue editing. If the server rejects a change, restore the old value with api.setValue(column, row, oldValue) – its event has the source "api", so it is not saved again, and it can't be undone by the user. The demo sends each change with a fake latency of 800 ms; salaries above 150,000 are rejected and restored.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<span>{{ pending ? `Saving ${pending} change(s)…` : "All changes saved" }}</span>
</div>
<Datagrid
:columns="columns"
:rows="rows"
:options="options"
@ready="api = $event"
@cell-value-changed="save"
/>
<div class="demo-log">
<div v-for="(entry, index) in log" :key="index">{{ entry }}</div>
<div v-if="!log.length">Change a salary – the server rejects salaries above 150,000.</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import {
Datagrid,
type CellValueChangedEvent,
type ColumnConfig,
type Core,
type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = createEmployees(100);
const api = shallowRef<Core>();
const pending = ref(0);
const log = ref<Array<string>>([]);
// fake PATCH /api/employees/:id with 800ms latency
async function patchEmployee(id: number, changes: Partial<Employee>) {
await new Promise((resolve) => setTimeout(resolve, 800));
if (changes.salary !== undefined && changes.salary > 150000) {
throw new Error("salaries above 150,000 need an approval");
}
}
async function save({ item, data, column, oldValue, newValue, source }: CellValueChangedEvent<Employee>) {
// changes made with api.setValue (the restored value below) are not saved
if (source === "api") return;
const name = `${data.firstName} ${data.lastName}`;
pending.value++;
try {
await patchEmployee(data.id, { [column.field!]: newValue });
log.value.unshift(`✓ ${name}: ${column.id} saved`);
} catch (error) {
// restore the old value
api.value!.setValue(column, item, oldValue);
log.value.unshift(`✗ ${name}: ${(error as Error).message}, value restored`);
} finally {
pending.value--;
}
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name", editable: true },
{ field: "lastName", text: "Last name", editable: true },
{ field: "department", text: "Department" },
{ field: "salary", text: "Salary", type: "number", editable: true },
];
const options: GridOptions<Employee> = {
rowId: (data) => data.id,
defaultColumn: { flex: 1 },
};
</script>tsx
import { useRef, useState } from "react";
import {
Datagrid,
type CellValueChangedEvent,
type ColumnConfig,
type DatagridHandle,
type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const rows = createEmployees(100);
// fake PATCH /api/employees/:id with 800ms latency
async function patchEmployee(id: number, changes: Partial<Employee>) {
await new Promise((resolve) => setTimeout(resolve, 800));
if (changes.salary !== undefined && changes.salary > 150000) {
throw new Error("salaries above 150,000 need an approval");
}
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name", editable: true },
{ field: "lastName", text: "Last name", editable: true },
{ field: "department", text: "Department" },
{ field: "salary", text: "Salary", type: "number", editable: true },
];
const options: GridOptions<Employee> = {
rowId: (data) => data.id,
defaultColumn: { flex: 1 },
};
export default function EditingSave() {
const grid = useRef<DatagridHandle>(null);
const [pending, setPending] = useState(0);
const [log, setLog] = useState<Array<string>>([]);
async function save({ item, data, column, oldValue, newValue, source }: CellValueChangedEvent<Employee>) {
// changes made with api.setValue (the restored value below) are not saved
if (source === "api") return;
const name = `${data.firstName} ${data.lastName}`;
setPending((count) => count + 1);
try {
await patchEmployee(data.id, { [column.field!]: newValue });
setLog((entries) => [`✓ ${name}: ${column.id} saved`, ...entries]);
} catch (error) {
// restore the old value
grid.current!.api.setValue(column, item, oldValue);
setLog((entries) => [`✗ ${name}: ${(error as Error).message}, value restored`, ...entries]);
} finally {
setPending((count) => count - 1);
}
}
return (
<div className="demo">
<div className="demo-toolbar">
<span>{pending ? `Saving ${pending} change(s)…` : "All changes saved"}</span>
</div>
<Datagrid ref={grid} columns={columns} rows={rows} options={options} onCellValueChanged={save} />
<div className="demo-log">
{log.map((entry, index) => (
<div key={index}>{entry}</div>
))}
{!log.length && <div>Change a salary – the server rejects salaries above 150,000.</div>}
</div>
</div>
);
}Custom editors
Source
vue
<template>
<Datagrid :columns="columns" :rows="rows" :options="options">
<!-- display of the cell -->
<template #cell-rating="{ value }">
<span class="stars">{{ stars(value) }}</span>
</template>
<!-- editor, clicking a star saves the value -->
<template #editor-rating="{ value, setValue, submit }">
<span class="star-editor">
<button
v-for="n in 5"
:key="n"
type="button"
:class="{ active: n <= Math.round(value) }"
@mouseenter="setValue(n)"
@click="submit(n)"
>
★
</button>
</span>
</template>
</Datagrid>
</template>
<script setup lang="ts">
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = createEmployees(100);
const stars = (value: number) => "★".repeat(Math.round(value)) + "☆".repeat(5 - Math.round(value));
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "role", text: "Role" },
// double click or Enter opens the custom editor
{ field: "rating", text: "Rating", editable: true, width: 160 },
];
const options: GridOptions<Employee> = {
defaultColumn: { flex: 1 },
};
</script>
<style>
.stars {
color: #f5a524;
letter-spacing: 2px;
}
.star-editor {
display: flex;
align-items: center;
height: 100%;
padding: 0 6px;
}
.star-editor button {
font-size: 18px;
line-height: 1;
color: var(--ut-muted-color);
}
.star-editor button.active {
color: #f5a524;
}
</style>tsx
import {
Datagrid,
type ColumnConfig,
type DatagridProps,
type EditorProps,
type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const rows = createEmployees(100);
const stars = (value: number) => "★".repeat(Math.round(value)) + "☆".repeat(5 - Math.round(value));
// editor component: receives the current value, onChange and submitChanges
function StarEditor(props: EditorProps<Employee, number>) {
return (
<span className="star-editor">
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
type="button"
className={n <= Math.round(props.value) ? "active" : undefined}
onMouseEnter={() => props.onChange(n)}
onClick={() => props.submitChanges(n)}
>
★
</button>
))}
</span>
);
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "role", text: "Role" },
// double click or Enter opens the custom editor
{ field: "rating", text: "Rating", editable: true, width: 160, editComponent: StarEditor },
];
const options: GridOptions<Employee> = {
defaultColumn: { flex: 1 },
};
// display of the cell
const cellRenderers: DatagridProps<Employee>["cellRenderers"] = {
rating: ({ value }) => <span className="stars">{stars(value)}</span>,
};
export default function EditingCustomEditor() {
return (
<>
<style>{`
.stars { color: #f5a524; letter-spacing: 2px; }
.star-editor { display: flex; align-items: center; height: 100%; padding: 0 6px; }
.star-editor button { font-size: 18px; line-height: 1; color: var(--ut-muted-color); }
.star-editor button.active { color: #f5a524; }
`}</style>
<Datagrid columns={columns} rows={rows} options={options} cellRenderers={cellRenderers} />
</>
);
}Use the editor-<columnId> slot (or editor for all columns):
vue
<Datagrid ...>
<template #editor-color="{ value, setValue, submit, cancel }">
<input
type="color"
:value="value"
@input="setValue($event.target.value)"
@change="submit()"
/>
</template>
</Datagrid>The slot receives value, setValue(value), submit(value?), cancel(), row, data, column and api. Enter and Escape are handled by the grid.
Alternatively use editComponent in the column definition. The component gets modelValue, submitChanges(value?), cancelEdit(), item and column as props and should emit update:modelValue.
Set a component as editComponent of the column. It receives these props:
| Prop | Description |
|---|---|
value | current value of the editor |
onChange(value) | updates the value (saved when editing ends) |
submitChanges(value?) | saves the value and ends editing |
cancelEdit() | discards the value |
item, column | row and column |
tsx
function ColorEditor({ value, onChange, submitChanges }: EditorProps<Product, string>) {
return <input type="color" value={value} onChange={(e) => onChange(e.target.value)} onBlur={() => submitChanges()} />;
}
const columns = [{ field: "color", editable: true, editComponent: ColorEditor }];The first input of the editor is focused. Enter and Escape are handled by the grid.
Undo and redo
Every change by editing, pasting, clearing (Delete) or the fill handle can be undone with Ctrl + Z and redone with Ctrl + Y (or Ctrl + Shift + Z). A paste or a cleared range is one step. Undo is enabled by default (undoRedo: false disables it), undoLimit sets the number of steps (default 50).
api.undo(), api.redo(), api.canUndo() and api.canRedo() connect your own buttons. Undo and redo emit cell-value-changed with the source "undo" / "redo", so a backend is kept in sync as well.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<button :disabled="!canUndo" @click="api?.undo()">↶ Undo</button>
<button :disabled="!canRedo" @click="api?.redo()">↷ Redo</button>
<span>or <kbd>Ctrl</kbd> + <kbd>Z</kbd> / <kbd>Ctrl</kbd> + <kbd>Y</kbd></span>
</div>
<Datagrid
:columns="columns"
:rows="rows"
:options="options"
@ready="api = $event"
@cell-value-changed="onChanged"
/>
<div class="demo-log">
<div v-for="(entry, index) in log" :key="index">{{ entry }}</div>
<div v-if="!log.length">Edit some cells, select a range and press Delete, or paste values…</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import {
Datagrid,
type CellValueChangedEvent,
type ColumnConfig,
type Core,
type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = createEmployees(100);
const api = shallowRef<Core>();
const canUndo = ref(false);
const canRedo = ref(false);
const log = ref<Array<string>>([]);
function onChanged({ data, column, oldValue, newValue, source }: CellValueChangedEvent<Employee>) {
// undo and redo emit the event as well (source "undo" / "redo")
log.value.unshift(`${source}: ${data.firstName} ${column.id} ${oldValue} → ${newValue}`);
canUndo.value = api.value!.canUndo();
canRedo.value = api.value!.canRedo();
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
{ field: "salary", text: "Salary", type: "number" },
{ field: "rating", text: "Rating", type: "number" },
];
const options: GridOptions<Employee> = {
// undo / redo is enabled by default, one step per edit, paste, clear or fill
undoRedo: true,
undoLimit: 20,
selection: "Cell",
singleSelect: false,
defaultColumn: { editable: true, flex: 1 },
};
</script>tsx
import { useRef, useState } from "react";
import {
Datagrid,
type CellValueChangedEvent,
type ColumnConfig,
type DatagridHandle,
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" },
{ field: "salary", text: "Salary", type: "number" },
{ field: "rating", text: "Rating", type: "number" },
];
const options: GridOptions<Employee> = {
// undo / redo is enabled by default, one step per edit, paste, clear or fill
undoRedo: true,
undoLimit: 20,
selection: "Cell",
singleSelect: false,
defaultColumn: { editable: true, flex: 1 },
};
export default function EditingUndo() {
const grid = useRef<DatagridHandle>(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [log, setLog] = useState<Array<string>>([]);
function onChanged({ data, column, oldValue, newValue, source }: CellValueChangedEvent<Employee>) {
// undo and redo emit the event as well (source "undo" / "redo")
setLog((entries) => [`${source}: ${data.firstName} ${column.id} ${oldValue} → ${newValue}`, ...entries]);
setCanUndo(grid.current!.api.canUndo());
setCanRedo(grid.current!.api.canRedo());
}
return (
<div className="demo">
<div className="demo-toolbar">
<button disabled={!canUndo} onClick={() => grid.current?.undo()}>
↶ Undo
</button>
<button disabled={!canRedo} onClick={() => grid.current?.redo()}>
↷ Redo
</button>
<span>
or <kbd>Ctrl</kbd> + <kbd>Z</kbd> / <kbd>Ctrl</kbd> + <kbd>Y</kbd>
</span>
</div>
<Datagrid ref={grid} columns={columns} rows={rows} options={options} onCellValueChanged={onChanged} />
<div className="demo-log">
{log.map((entry, index) => (
<div key={index}>{entry}</div>
))}
{!log.length && <div>Edit some cells, select a range and press Delete, or paste values…</div>}
</div>
</div>
);
}API
ts
api.startEdit({ item: row, column });
api.endEdit(); // saves the value of the editor
api.cancelEdit();
api.getEditState(); // { active, item, column }
api.setValue(column, row, value);