Appearance
Server side data
For large data sets which cannot be loaded completely, the grid can delegate sorting, filtering and pagination to the server. Pass a dataGetter function instead of rows:
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<input v-model="search" type="search" placeholder="Search on the server…" />
<span>{{ requests }} requests</span>
</div>
<Datagrid :columns="columns" :options="options" v-model:globalFilter="search" />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import {
Datagrid,
type ColumnConfig,
type DataGetterParams,
type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const search = ref("");
const requests = ref(0);
// a fake REST api with 50.000 rows and 300ms latency
const database = createEmployees(50_000);
async function fetchEmployees(params: DataGetterParams) {
requests.value++;
// in a real application:
// const response = await fetch(`/api/employees?${toQueryString(params)}`, { signal: params.signal });
// return response.json(); // { data: [...], total: 1234 }
await new Promise((resolve) => setTimeout(resolve, 300));
let result = database;
const text = String(params.globalFilter ?? "").toLowerCase();
if (text) {
result = result.filter((row) => `${row.firstName} ${row.lastName} ${row.city}`.toLowerCase().includes(text));
}
for (const { field, value } of params.filter) {
result = result.filter((row) => String(row[field as keyof Employee]).toLowerCase().includes(String(value).toLowerCase()));
}
for (const { field, asc } of [...params.sort].reverse()) {
const key = field as keyof Employee;
result = [...result].sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0) * (asc ? 1 : -1));
}
const { pageIndex, pageSize } = params.pagination!;
return {
data: result.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize),
total: result.length,
};
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 90 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "city", text: "City" },
{ field: "salary", text: "Salary", type: "number" },
];
const options: GridOptions<Employee> = {
dataGetter: fetchEmployees,
rowId: (data) => data.id,
pagination: { enabled: true, pageSize: 50 },
defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>tsx
import { useMemo, useState } from "react";
import {
Datagrid,
type ColumnConfig,
type DataGetterParams,
type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
// a fake REST api with 50.000 rows and 300ms latency
const database = createEmployees(50_000);
async function fetchEmployees(params: DataGetterParams) {
// in a real application:
// const response = await fetch(`/api/employees?${toQueryString(params)}`, { signal: params.signal });
// return response.json(); // { data: [...], total: 1234 }
await new Promise((resolve) => setTimeout(resolve, 300));
let result = database;
const text = String(params.globalFilter ?? "").toLowerCase();
if (text) {
result = result.filter((row) => `${row.firstName} ${row.lastName} ${row.city}`.toLowerCase().includes(text));
}
for (const { field, value } of params.filter) {
result = result.filter((row) => String(row[field as keyof Employee]).toLowerCase().includes(String(value).toLowerCase()));
}
for (const { field, asc } of [...params.sort].reverse()) {
const key = field as keyof Employee;
result = [...result].sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0) * (asc ? 1 : -1));
}
const { pageIndex, pageSize } = params.pagination!;
return {
data: result.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize),
total: result.length,
};
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 90 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "city", text: "City" },
{ field: "salary", text: "Salary", type: "number" },
];
export default function ServerSide() {
const [search, setSearch] = useState("");
const [requests, setRequests] = useState(0);
// memoized: new options would reload the data
const options = useMemo<GridOptions<Employee>>(
() => ({
dataGetter: (params) => {
setRequests((count) => count + 1);
return fetchEmployees(params);
},
rowId: (data) => data.id,
pagination: { enabled: true, pageSize: 50 },
defaultColumn: { sortable: true, filterable: true, flex: 1 },
}),
[]
);
return (
<div className="demo">
<div className="demo-toolbar">
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
type="search"
placeholder="Search on the server…"
/>
<span>{requests} requests</span>
</div>
<Datagrid
columns={columns}
options={options}
globalFilter={search}
onGlobalFilterChange={(value) => setSearch(String(value ?? ""))}
/>
</div>
);
}The data getter
ts
type DataGetterFunction<T> = (params: DataGetterParams) => Promise<{ data: T[]; total: number }>;params contains:
| Property | Description |
|---|---|
sort | [{ column, field, asc }] in sort order |
filter | [{ column, field, value }], see filter values |
globalFilter | value of the quick filter |
grouping | ids of the grouped columns |
pagination | { pageIndex, pageSize } or undefined if pagination is disabled |
columns | all columns |
signal | AbortSignal, aborted when a newer request is started (pass it to fetch) |
The function is called initially and whenever sorting, filters, the quick filter or the page change. While a request is running, a loading bar is shown. Responses of outdated requests are ignored.
total is the number of rows matching the filters. It is used for the pagination.
ts
async function dataGetter(params: DataGetterParams) {
const query = new URLSearchParams({
page: String(params.pagination!.pageIndex),
size: String(params.pagination!.pageSize),
sort: params.sort.map((s) => `${s.field},${s.asc ? "asc" : "desc"}`).join(";"),
search: String(params.globalFilter ?? ""),
});
const response = await fetch(`/api/orders?${query}`, { signal: params.signal });
return response.json(); // { data: [...], total: 12345 }
}Requests and parameters
The log below shows what the grid sends to the dataGetter for every change of sorting, filters or the page. Try a slow latency and type quickly into a filter: the grid aborts the outdated requests with params.signal, so only the last response is shown. Pass the signal to fetch to cancel the HTTP request too.
The set filter of Department can't collect its values from the rows of one page, so they are passed in with filterParams.values. The fake server in this demo understands "contains" for text filters and set filters – a real backend has to translate the filter values of the columns it allows to filter.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<label>
Latency
<select v-model.number="latency">
<option :value="100">100 ms</option>
<option :value="500">500 ms</option>
<option :value="1500">1.5 s</option>
</select>
</label>
<span>Sort, filter or change the page – every change is a request.</span>
</div>
<Datagrid :columns="columns" :options="options" />
<div class="demo-log">
<div v-for="entry in log" :key="entry.id">{{ entry.text }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import {
Datagrid,
formatters,
type ColumnConfig,
type DataGetterParams,
type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const departments = ["Engineering", "Sales", "Marketing", "Support", "Finance", "HR"];
const latency = ref(500);
const log = ref<Array<{ id: number; text: string }>>([]);
let requestId = 0;
// ---- fake server with 10.000 rows ----
const database = createEmployees(10_000);
function queryDatabase({ filter, sort, pagination }: DataGetterParams) {
let result = database;
for (const { field, value } of filter) {
const key = field as keyof Employee;
if (typeof value === "object" && value && "values" in value) {
// set filter: { values: [...] }
const values = (value as { values: Array<unknown> }).values;
result = result.filter((row) => values.includes(row[key]));
} else {
// text filter: "contains"
const text = String(value).toLowerCase();
result = result.filter((row) => String(row[key]).toLowerCase().includes(text));
}
}
for (const { field, asc } of [...sort].reverse()) {
const key = field as keyof Employee;
result = [...result].sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0) * (asc ? 1 : -1));
}
const { pageIndex, pageSize } = pagination!;
return { data: result.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize), total: result.length };
}
// waits like a network request, rejects when the grid aborts the request
function delay(ms: number, signal: AbortSignal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal.addEventListener("abort", () => {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
});
});
}
async function dataGetter(params: DataGetterParams) {
const id = ++requestId;
const sort = params.sort.map((s) => `${s.field} ${s.asc ? "asc" : "desc"}`).join(", ") || "–";
const filter = params.filter.map((f) => `${f.field}=${JSON.stringify(f.value)}`).join(", ") || "–";
const text = `#${id} page ${params.pagination!.pageIndex + 1} · sort: ${sort} · filter: ${filter}`;
const entry = { id, text: `${text} …` };
log.value = [entry, ...log.value.slice(0, 9)];
try {
await delay(latency.value, params.signal);
} catch (e) {
// a newer request was started (e.g. while typing in a filter)
log.value = log.value.map((el) => (el.id === id ? { id, text: `${text} → aborted` } : el));
throw e;
}
const result = queryDatabase(params);
log.value = log.value.map((el) => (el.id === id ? { id, text: `${text} → ${result.total} rows` } : el));
return result;
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 80, filterable: false },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
// the set filter can't collect the values from the loaded page, so they are passed in
{ field: "department", text: "Department", filterType: "set", filterParams: { values: departments } },
{ field: "salary", text: "Salary", type: "number", filterable: false, valueFormatter: formatters.currency("EUR") },
];
const options: GridOptions<Employee> = {
dataGetter,
rowId: (data) => data.id,
pagination: { enabled: true, pageSize: 25 },
defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>tsx
import { useMemo, useRef, useState } from "react";
import {
Datagrid,
formatters,
type ColumnConfig,
type DataGetterParams,
type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const departments = ["Engineering", "Sales", "Marketing", "Support", "Finance", "HR"];
// ---- fake server with 10.000 rows ----
const database = createEmployees(10_000);
function queryDatabase({ filter, sort, pagination }: DataGetterParams) {
let result = database;
for (const { field, value } of filter) {
const key = field as keyof Employee;
if (typeof value === "object" && value && "values" in value) {
// set filter: { values: [...] }
const values = (value as { values: Array<unknown> }).values;
result = result.filter((row) => values.includes(row[key]));
} else {
// text filter: "contains"
const text = String(value).toLowerCase();
result = result.filter((row) => String(row[key]).toLowerCase().includes(text));
}
}
for (const { field, asc } of [...sort].reverse()) {
const key = field as keyof Employee;
result = [...result].sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0) * (asc ? 1 : -1));
}
const { pageIndex, pageSize } = pagination!;
return { data: result.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize), total: result.length };
}
// waits like a network request, rejects when the grid aborts the request
function delay(ms: number, signal: AbortSignal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal.addEventListener("abort", () => {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
});
});
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 80, filterable: false },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
// the set filter can't collect the values from the loaded page, so they are passed in
{ field: "department", text: "Department", filterType: "set", filterParams: { values: departments } },
{ field: "salary", text: "Salary", type: "number", filterable: false, valueFormatter: formatters.currency("EUR") },
];
export default function ServerSideRequests() {
const [latency, setLatency] = useState(500);
const [log, setLog] = useState<Array<{ id: number; text: string }>>([]);
const latencyRef = useRef(latency);
latencyRef.current = latency;
const requestId = useRef(0);
// memoized: new options would reload the data
const options = useMemo<GridOptions<Employee>>(() => {
const update = (id: number, text: string) =>
setLog((log) => log.map((el) => (el.id === id ? { id, text } : el)));
async function dataGetter(params: DataGetterParams) {
const id = ++requestId.current;
const sort = params.sort.map((s) => `${s.field} ${s.asc ? "asc" : "desc"}`).join(", ") || "–";
const filter = params.filter.map((f) => `${f.field}=${JSON.stringify(f.value)}`).join(", ") || "–";
const text = `#${id} page ${params.pagination!.pageIndex + 1} · sort: ${sort} · filter: ${filter}`;
setLog((log) => [{ id, text: `${text} …` }, ...log.slice(0, 9)]);
try {
await delay(latencyRef.current, params.signal);
} catch (e) {
// a newer request was started (e.g. while typing in a filter)
update(id, `${text} → aborted`);
throw e;
}
const result = queryDatabase(params);
update(id, `${text} → ${result.total} rows`);
return result;
}
return {
dataGetter,
rowId: (data) => data.id,
pagination: { enabled: true, pageSize: 25 },
defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
}, []);
return (
<div className="demo">
<div className="demo-toolbar">
<label>
Latency{" "}
<select value={latency} onChange={(e) => setLatency(Number(e.target.value))}>
<option value={100}>100 ms</option>
<option value={500}>500 ms</option>
<option value={1500}>1.5 s</option>
</select>
</label>
<span>Sort, filter or change the page – every change is a request.</span>
</div>
<Datagrid columns={columns} options={options} />
<div className="demo-log">
{log.map((entry) => (
<div key={entry.id}>{entry.text}</div>
))}
</div>
</div>
);
}Error handling
If the promise of the dataGetter is rejected, the loading indicator disappears, the previous rows stay visible and the load-error event (onLoadError in React) is emitted with the error. A later successful request emits it again with undefined, and api.getLoadError() returns the current error. Without a listener, the error is logged to the console. The demo shows the error in the overlay layer on top of the grid with a button that calls api.refresh(). Check Server is down and change the page.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<label><input v-model="serverDown" type="checkbox" /> Server is down</label>
<button @click="api?.refresh()">Reload</button>
</div>
<Datagrid :columns="columns" :options="options" @ready="api = $event" @load-error="onLoadError">
<template v-if="error" #overlay="{ api }">
<div class="sserr-overlay">
<div class="sserr-box">
<strong>Could not load the employees</strong>
<div>{{ error }}</div>
<button @click="api.refresh()">Try again</button>
</div>
</div>
</template>
</Datagrid>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import {
Datagrid,
type ColumnConfig,
type Core,
type DataGetterParams,
type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const api = shallowRef<Core>();
const serverDown = ref(false);
const error = ref<string>();
// ---- fake server ----
const database = createEmployees(500);
async function dataGetter({ pagination }: DataGetterParams) {
await new Promise((resolve) => setTimeout(resolve, 400));
if (serverDown.value) {
throw new Error("503 Service Unavailable");
}
const { pageIndex, pageSize } = pagination!;
return { data: database.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize), total: database.length };
}
// called with the error of a failed request, and with undefined when a later request succeeds
function onLoadError(e: unknown) {
error.value = e ? (e as Error).message : undefined;
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 80 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "city", text: "City" },
];
const options: GridOptions<Employee> = {
dataGetter,
rowId: (data) => data.id,
pagination: { enabled: true, pageSize: 25 },
defaultColumn: { flex: 1 },
};
</script>
<style>
.sserr-overlay {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: color-mix(in srgb, var(--ut-background) 70%, transparent);
}
.sserr-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 24px;
border: 1px solid #cf222e;
border-radius: 8px;
background: var(--ut-background);
color: #cf222e;
}
.sserr-box button {
padding: 4px 12px;
border: 1px solid var(--ut-border-color);
border-radius: 6px;
color: var(--ut-color);
cursor: pointer;
}
</style>tsx
import { useMemo, useRef, useState } from "react";
import {
Datagrid,
type ColumnConfig,
type DatagridHandle,
type DataGetterParams,
type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
// ---- fake server ----
const database = createEmployees(500);
async function fetchEmployees({ pagination }: DataGetterParams, serverDown: boolean) {
await new Promise((resolve) => setTimeout(resolve, 400));
if (serverDown) {
throw new Error("503 Service Unavailable");
}
const { pageIndex, pageSize } = pagination!;
return { data: database.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize), total: database.length };
}
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 80 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "city", text: "City" },
];
export default function ServerSideErrors() {
const grid = useRef<DatagridHandle>(null);
const [serverDown, setServerDown] = useState(false);
const [error, setError] = useState<string>();
const serverDownRef = useRef(serverDown);
serverDownRef.current = serverDown;
// memoized: new options would reload the data
const options = useMemo<GridOptions<Employee>>(
() => ({
dataGetter: (params) => fetchEmployees(params, serverDownRef.current),
rowId: (data) => data.id,
pagination: { enabled: true, pageSize: 25 },
defaultColumn: { flex: 1 },
}),
[]
);
return (
<div className="demo">
<div className="demo-toolbar">
<label>
<input type="checkbox" checked={serverDown} onChange={(e) => setServerDown(e.target.checked)} /> Server is
down
</label>
<button onClick={() => grid.current?.refresh()}>Reload</button>
</div>
<Datagrid
ref={grid}
columns={columns}
options={options}
// called with the error of a failed request, and with undefined when a later request succeeds
onLoadError={(e) => setError(e ? (e as Error).message : undefined)}
overlay={
error
? (api) => (
<div className="sserr-overlay">
<div className="sserr-box">
<strong>Could not load the employees</strong>
<div>{error}</div>
<button onClick={() => api.refresh()}>Try again</button>
</div>
</div>
)
: undefined
}
/>
<style>{`
.sserr-overlay {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: color-mix(in srgb, var(--ut-background) 70%, transparent);
}
.sserr-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 24px;
border: 1px solid #cf222e;
border-radius: 8px;
background: var(--ut-background);
color: #cf222e;
}
.sserr-box button {
padding: 4px 12px;
border: 1px solid var(--ut-border-color);
border-radius: 6px;
color: var(--ut-color);
cursor: pointer;
}
`}</style>
</div>
);
}Reloading and saving
Call api.refresh() to load the current page again, e.g. after the data was changed on the server. Edited cells are changed in the row objects of the current page; save them in the cell-value-changed event (onCellValueChanged in React). The buttons change the data "on the server" and reload the page; flashOnChange highlights the changed salaries.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<button @click="addEmployee">Add employee on the server</button>
<button @click="raiseSalaries">Raise all salaries by 5 % on the server</button>
</div>
<Datagrid :columns="columns" :options="options" @ready="api = $event" @cell-value-changed="save" />
<div class="demo-log">{{ status }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import {
Datagrid,
formatters,
type CellValueChangedEvent,
type ColumnConfig,
type Core,
type DataGetterParams,
type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const api = shallowRef<Core>();
const status = ref("Double click a city or salary to edit it.");
// ---- fake server, returns copies like a real api ----
const database = createEmployees(200);
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function dataGetter({ sort, pagination }: DataGetterParams) {
await wait(300);
const result = [...database];
for (const { field, asc } of [...sort].reverse()) {
const key = field as keyof Employee;
result.sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0) * (asc ? 1 : -1));
}
const { pageIndex, pageSize } = pagination!;
const page = result.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
return { data: page.map((row) => ({ ...row })), total: result.length };
}
// saves an edited cell (e.g. PATCH /api/employees/:id)
async function save({ data, column, newValue }: CellValueChangedEvent<Employee>) {
status.value = `Saving ${column.id} of #${data.id}…`;
await wait(500);
Object.assign(database.find((row) => row.id === data.id)!, { [column.field!]: newValue });
status.value = `Saved ${column.id} = ${newValue} for #${data.id}`;
}
// changes on the server are loaded with api.refresh()
function addEmployee() {
const id = Math.max(...database.map((row) => row.id)) + 1;
database.push({ ...createEmployees(1, id)[0], id });
status.value = `Added #${id} on the server, reloading…`;
api.value?.refresh();
}
function raiseSalaries() {
database.forEach((row) => (row.salary = Math.round(row.salary * 1.05)));
status.value = "Salaries changed on the server, reloading…";
api.value?.refresh();
}
const columns: Array<ColumnConfig<Employee>> = [
// newest employees first
{ field: "id", text: "ID", type: "number", width: 80, sort: "desc" },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "city", text: "City", editable: true },
{
field: "salary",
text: "Salary",
type: "number",
editable: true,
flashOnChange: true,
valueFormatter: formatters.currency("EUR", { maximumFractionDigits: 0 }),
},
];
const options: GridOptions<Employee> = {
dataGetter,
rowId: (data) => data.id,
pagination: { enabled: true, pageSize: 25 },
defaultColumn: { sortable: true, flex: 1 },
};
</script>tsx
import { useRef, useState } from "react";
import {
Datagrid,
formatters,
type CellValueChangedEvent,
type ColumnConfig,
type DatagridHandle,
type DataGetterParams,
type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
// ---- fake server, returns copies like a real api ----
const database = createEmployees(200);
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function dataGetter({ sort, pagination }: DataGetterParams) {
await wait(300);
const result = [...database];
for (const { field, asc } of [...sort].reverse()) {
const key = field as keyof Employee;
result.sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0) * (asc ? 1 : -1));
}
const { pageIndex, pageSize } = pagination!;
const page = result.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
return { data: page.map((row) => ({ ...row })), total: result.length };
}
const columns: Array<ColumnConfig<Employee>> = [
// newest employees first
{ field: "id", text: "ID", type: "number", width: 80, sort: "desc" },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "city", text: "City", editable: true },
{
field: "salary",
text: "Salary",
type: "number",
editable: true,
flashOnChange: true,
valueFormatter: formatters.currency("EUR", { maximumFractionDigits: 0 }),
},
];
// static options: the dataGetter doesn't depend on component state
const options: GridOptions<Employee> = {
dataGetter,
rowId: (data) => data.id,
pagination: { enabled: true, pageSize: 25 },
defaultColumn: { sortable: true, flex: 1 },
};
export default function ServerSideSaving() {
const grid = useRef<DatagridHandle>(null);
const [status, setStatus] = useState("Double click a city or salary to edit it.");
// saves an edited cell (e.g. PATCH /api/employees/:id)
async function save({ data, column, newValue }: CellValueChangedEvent<Employee>) {
setStatus(`Saving ${column.id} of #${data.id}…`);
await wait(500);
Object.assign(database.find((row) => row.id === data.id)!, { [column.field!]: newValue });
setStatus(`Saved ${column.id} = ${newValue} for #${data.id}`);
}
// changes on the server are loaded with api.refresh()
function addEmployee() {
const id = Math.max(...database.map((row) => row.id)) + 1;
database.push({ ...createEmployees(1, id)[0], id });
setStatus(`Added #${id} on the server, reloading…`);
grid.current?.refresh();
}
function raiseSalaries() {
database.forEach((row) => (row.salary = Math.round(row.salary * 1.05)));
setStatus("Salaries changed on the server, reloading…");
grid.current?.refresh();
}
return (
<div className="demo">
<div className="demo-toolbar">
<button onClick={addEmployee}>Add employee on the server</button>
<button onClick={raiseSalaries}>Raise all salaries by 5 % on the server</button>
</div>
<Datagrid ref={grid} columns={columns} options={options} onCellValueChanged={save} />
<div className="demo-log">{status}</div>
</div>
);
}Notes
- Set
rowIdso that the selection is kept when pages are loaded again. - Editing works like with client side data: the row objects are changed and
@cell-value-changedis emitted, so you can save the change on the server. - Aggregations of the total row are calculated for the rows of the current page. Use pinned rows to show totals calculated by the server.