Appearance
Row data
Rows are passed as an array of objects with the rows prop:
vue
<Datagrid :columns="columns" :rows="rows" />tsx
<Datagrid columns={columns} rows={rows} />Row ids
Every row gets a unique id (uid). Without configuration the index of the row is used (0, 1, 2, … and "0-1" for child rows of tree data).
When rows are added, removed or reordered, index based ids change. Selection, focus, expanded state and row heights are bound to the id, so set rowId whenever the data can change:
ts
const options: GridOptions<Person> = {
rowId: (data) => data.id,
};The id is used by the selection models (v-model:selectedItems), v-model:expandedItems, the state and API methods like api.getRow(id).
Updating data
The grid watches the rows prop (not deeply). Assign a new array to update the grid. Thanks to rowId the selection survives adding, removing, replacing and reordering rows – select some rows and try the buttons:
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<button @click="add">Add employee</button>
<button @click="removeSelected">Remove selected</button>
<button @click="raiseSalary">Raise salary of selected by 5 %</button>
<button @click="rows = rows.toReversed()">Reverse order</button>
<span>{{ rows.length }} rows, {{ selected.length }} selected</span>
</div>
<Datagrid :columns="columns" :rows="rows" :options="options" v-model:selectedItems="selected" />
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions, type ItemUid } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const rows = shallowRef(createEmployees(8));
const selected = ref<Array<ItemUid>>([2, 5]);
let nextId = 9;
function add() {
const employee = { ...createEmployees(1, nextId)[0], id: nextId++ };
rows.value = [employee, ...rows.value];
}
function removeSelected() {
rows.value = rows.value.filter((row) => !selected.value.includes(row.id));
}
function raiseSalary() {
// replace the changed objects, the grid finds the rows by their id
rows.value = rows.value.map((row) =>
selected.value.includes(row.id) ? { ...row, salary: Math.round(row.salary * 1.05) } : row
);
}
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", valueFormatter: formatters.currency("EUR", { maximumFractionDigits: 0 }) },
];
const options: GridOptions<Employee> = {
// selection, focus and expanded state are bound to the id
rowId: (data) => data.id,
selection: "Row",
singleSelect: false,
checkboxSelection: true,
defaultColumn: { flex: 1 },
};
</script>tsx
import { useRef, useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions, type ItemUid } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
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", valueFormatter: formatters.currency("EUR", { maximumFractionDigits: 0 }) },
];
const options: GridOptions<Employee> = {
// selection, focus and expanded state are bound to the id
rowId: (data) => data.id,
selection: "Row",
singleSelect: false,
checkboxSelection: true,
defaultColumn: { flex: 1 },
};
export default function DataUpdate() {
const [rows, setRows] = useState(() => createEmployees(8));
const [selected, setSelected] = useState<Array<ItemUid>>([2, 5]);
const nextId = useRef(9);
function add() {
const employee = { ...createEmployees(1, nextId.current)[0], id: nextId.current++ };
setRows([employee, ...rows]);
}
function removeSelected() {
setRows(rows.filter((row) => !selected.includes(row.id)));
}
function raiseSalary() {
// replace the changed objects, the grid finds the rows by their id
setRows(rows.map((row) => (selected.includes(row.id) ? { ...row, salary: Math.round(row.salary * 1.05) } : row)));
}
return (
<div className="demo">
<div className="demo-toolbar">
<button onClick={add}>Add employee</button>
<button onClick={removeSelected}>Remove selected</button>
<button onClick={raiseSalary}>Raise salary of selected by 5 %</button>
<button onClick={() => setRows(rows.toReversed())}>Reverse order</button>
<span>
{rows.length} rows, {selected.length} selected
</span>
</div>
<Datagrid columns={columns} rows={rows} options={options} selectedItems={selected} onSelectedItemsChange={setSelected} />
</div>
);
}ts
const rows = ref<Array<Person>>([]);
rows.value = await loadPersons(); // replace all rows
rows.value = [...rows.value, newPerson]; // add a row
rows.value = rows.value.filter((p) => p.id !== id); // remove a rowtsx
const [rows, setRows] = useState<Array<Person>>([]);
setRows(await loadPersons()); // replace all rows
setRows([...rows, newPerson]); // add a row
setRows(rows.filter((p) => p.id !== id)); // remove a rowExisting row objects are reused (by rowId), so updates are fast even for large data sets. Sorting, filtering and grouping are applied again after each update.
Mutating row objects
If you change properties of row objects directly (without assigning a new array), call refresh() to apply sorting and filtering again and to rerender the cells. This avoids copying objects, e.g. for frequent updates of single values. Here the list is sorted by stock: selling units of the selected product moves it up, cells with flashOnChange are highlighted.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<button @click="sell">Sell 10 units of the selected product</button>
<button @click="restock">Restock products below 20 units</button>
</div>
<Datagrid
:columns="columns"
:rows="products"
:options="options"
v-model:selectedItems="selected"
@ready="api = $event"
/>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import { Datagrid, formatters, type ColumnConfig, type Core, type GridOptions, type ItemUid } from "@datagrid/vue-ui";
import { createProducts, type Product } from "./data";
// a plain (not reactive) array, the objects are changed directly
const products = createProducts(40);
const api = shallowRef<Core>();
const selected = ref<Array<ItemUid>>([25]);
function sell() {
const product = products.find((p) => p.id === selected.value[0]);
if (!product) return;
product.stock = Math.max(0, product.stock - 10);
// sorting and filters are applied again, the cells are rendered again
api.value?.refresh();
}
function restock() {
products.filter((p) => p.stock < 20).forEach((p) => (p.stock += 50));
api.value?.refresh();
}
const columns: Array<ColumnConfig<Product>> = [
{ field: "name", text: "Product", flex: 1 },
{ field: "category", text: "Category" },
{ field: "price", text: "Price", type: "number", valueFormatter: formatters.currency("EUR") },
{
field: "stock",
text: "Stock",
type: "number",
sort: "asc",
flashOnChange: true,
cellClass: ({ value }) => (value < 20 ? "stock-low" : undefined),
},
];
const options: GridOptions<Product> = {
rowId: (data) => data.id,
selection: "Row",
defaultColumn: { sortable: true, width: 130 },
};
</script>
<style>
.stock-low {
color: #d03030;
font-weight: 600;
}
</style>tsx
import { useRef, useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type DatagridHandle, type GridOptions, type ItemUid } from "@datagrid/react-ui";
import { createProducts, type Product } from "../data";
const columns: Array<ColumnConfig<Product>> = [
{ field: "name", text: "Product", flex: 1 },
{ field: "category", text: "Category" },
{ field: "price", text: "Price", type: "number", valueFormatter: formatters.currency("EUR") },
{
field: "stock",
text: "Stock",
type: "number",
sort: "asc",
flashOnChange: true,
cellClass: ({ value }) => (value < 20 ? "stock-low" : undefined),
},
];
const options: GridOptions<Product> = {
rowId: (data) => data.id,
selection: "Row",
defaultColumn: { sortable: true, width: 130 },
};
export default function DataMutate() {
// the array is never replaced, the objects are changed directly
const [products] = useState(() => createProducts(40));
const grid = useRef<DatagridHandle>(null);
const [selected, setSelected] = useState<Array<ItemUid>>([25]);
function sell() {
const product = products.find((p) => p.id === selected[0]);
if (!product) return;
product.stock = Math.max(0, product.stock - 10);
// sorting and filters are applied again, the cells are rendered again
grid.current?.refresh();
}
function restock() {
products.filter((p) => p.stock < 20).forEach((p) => (p.stock += 50));
grid.current?.refresh();
}
return (
<div className="demo">
<div className="demo-toolbar">
<button onClick={sell}>Sell 10 units of the selected product</button>
<button onClick={restock}>Restock products below 20 units</button>
</div>
<Datagrid
ref={grid}
columns={columns}
rows={products}
options={options}
selectedItems={selected}
onSelectedItemsChange={setSelected}
/>
<style>{`
.stock-low {
color: #d03030;
font-weight: 600;
}
`}</style>
</div>
);
}ts
product.stock -= 10;
api.value.refresh(); // api from the ready eventtsx
product.stock -= 10;
grid.current.refresh();Changes made by the grid
When the user edits a cell or pastes values, the grid changes the row objects directly and reports a new array, so you can keep your data in sync:
vue
<Datagrid v-model:rows="rows" :columns="columns" />tsx
const [rows, setRows] = useState(initialRows);
<Datagrid rows={rows} onRowsChange={setRows} columns={columns} />Loading state
While rows is undefined or null, the grid shows the loading indicator. An empty array shows the "no rows" message. To keep the current rows visible while new data is loaded (e.g. a reload), set the option loading: true: the grid shows a progress bar above the rows.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<button @click="load(50)">Load</button>
<button @click="load(0)">Load empty result</button>
<button @click="reload">Reload (keep rows)</button>
</div>
<Datagrid :columns="columns" :rows="rows" :options="options" />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, shallowRef } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
// simulates a request to the server
const fetchEmployees = (count: number) =>
new Promise<Array<Employee>>((resolve) => setTimeout(() => resolve(createEmployees(count, Date.now())), 1500));
const rows = shallowRef<Array<Employee>>();
const reloading = ref(false);
async function load(count: number) {
// undefined shows the loading overlay instead of the rows
rows.value = undefined;
rows.value = await fetchEmployees(count);
}
async function reload() {
// the loading option shows a progress bar and keeps the current rows
reloading.value = true;
rows.value = await fetchEmployees(50);
reloading.value = false;
}
onMounted(() => load(50));
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
{ field: "city", text: "City" },
];
const options = computed<GridOptions<Employee>>(() => ({
rowId: (data) => data.id,
loading: reloading.value,
defaultColumn: { flex: 1 },
}));
</script>tsx
import { useEffect, useMemo, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
// simulates a request to the server
const fetchEmployees = (count: number) =>
new Promise<Array<Employee>>((resolve) => setTimeout(() => resolve(createEmployees(count, Date.now())), 1500));
const columns: Array<ColumnConfig<Employee>> = [
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
{ field: "city", text: "City" },
];
export default function DataLoading() {
const [rows, setRows] = useState<Array<Employee>>();
const [reloading, setReloading] = useState(false);
async function load(count: number) {
// undefined shows the loading overlay instead of the rows
setRows(undefined);
setRows(await fetchEmployees(count));
}
async function reload() {
// the loading option shows a progress bar and keeps the current rows
setReloading(true);
setRows(await fetchEmployees(50));
setReloading(false);
}
useEffect(() => {
load(50);
}, []);
const options = useMemo<GridOptions<Employee>>(
() => ({
rowId: (data) => data.id,
loading: reloading,
defaultColumn: { flex: 1 },
}),
[reloading]
);
return (
<div className="demo">
<div className="demo-toolbar">
<button onClick={() => load(50)}>Load</button>
<button onClick={() => load(0)}>Load empty result</button>
<button onClick={reload}>Reload (keep rows)</button>
</div>
<Datagrid columns={columns} rows={rows} options={options} />
</div>
);
}The texts can be replaced (Vue: loading and empty slots, React: renderLoading and renderEmpty), see Custom rendering.
Large data sets
The grid renders only the visible rows and columns, so the number of rows mainly affects sorting, filtering and the creation of the data. Tips for large data sets:
- set
rowId, updates of the data reuse existing row objects - use a fixed
rowHeight, rows don't have to be measured - avoid expensive
valueGetterfunctions for sorted or filtered columns
Vue makes every object inside of a ref deeply reactive. For large arrays this costs memory and time. The grid does not need reactive rows, so use shallowRef:
ts
const rows = shallowRef(hugeArray);Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<label>
Rows
<select v-model.number="count">
<option :value="1000">1,000</option>
<option :value="100000">100,000</option>
<option :value="500000">500,000</option>
</select>
</label>
<button @click="generate">Generate</button>
<span>{{ info }}</span>
</div>
<Datagrid :columns="columns" :rows="rows" :options="options" />
</div>
</template>
<script setup lang="ts">
import { nextTick, ref, shallowRef } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";
const count = ref(100000);
const info = ref("");
// shallowRef: Vue does not make the objects reactive (much faster, less memory)
const rows = shallowRef<Array<Employee>>([]);
async function generate() {
const start = performance.now();
const data = createEmployees(count.value, Math.round(Math.random() * 1000));
const created = performance.now();
rows.value = data;
await nextTick();
const end = performance.now();
info.value = `created in ${Math.round(created - start)} ms, displayed in ${Math.round(end - created)} ms`;
}
generate();
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 90 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
{ field: "city", text: "City" },
{ field: "salary", text: "Salary", type: "number" },
{ field: "startDate", text: "Start date", type: "date" },
];
const options: GridOptions<Employee> = {
rowId: (data) => data.id,
// a fixed row height avoids measuring rows
rowHeight: 32,
defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>tsx
import { useEffect, useRef, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const columns: Array<ColumnConfig<Employee>> = [
{ field: "id", text: "ID", type: "number", width: 90 },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "department", text: "Department" },
{ field: "city", text: "City" },
{ field: "salary", text: "Salary", type: "number" },
{ field: "startDate", text: "Start date", type: "date" },
];
const options: GridOptions<Employee> = {
rowId: (data) => data.id,
// a fixed row height avoids measuring rows
rowHeight: 32,
defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
export default function DataLarge() {
const [count, setCount] = useState(100000);
const [rows, setRows] = useState<Array<Employee>>([]);
const [info, setInfo] = useState("");
const created = useRef({ start: 0, end: 0 });
function generate() {
const start = performance.now();
const data = createEmployees(count, Math.round(Math.random() * 1000));
created.current = { start, end: performance.now() };
setRows(data);
}
useEffect(generate, []);
// runs after the grid has received the new rows
useEffect(() => {
const { start, end } = created.current;
if (start) setInfo(`created in ${Math.round(end - start)} ms, displayed in ${Math.round(performance.now() - end)} ms`);
}, [rows]);
return (
<div className="demo">
<div className="demo-toolbar">
<label>
Rows{" "}
<select value={count} onChange={(e) => setCount(Number(e.target.value))}>
<option value={1000}>1,000</option>
<option value={100000}>100,000</option>
<option value={500000}>500,000</option>
</select>
</label>
<button onClick={generate}>Generate</button>
<span>{info}</span>
</div>
<Datagrid columns={columns} rows={rows} options={options} />
</div>
);
}See Performance for more details.
Reading data
ts
api.getData(); // the rows array passed to the grid
api.getAllRows(); // all row objects (including child rows)
api.getAllVisibleRows(); // rows in the view (sorted, filtered, current page, without collapsed children)
api.getFilteredSortedItems(); // all rows after filtering and sorting (all pages)
api.getRow(id); // row by id
api.getSelectedData(); // data objects of the selected rows
api.getTotalItemCount(); // number of rows
api.getFilteredItemCount(); // number of rows after filteringA row object (Row<T>) contains the data object (row.data), the id (row.uid), the position in the view (row.index) and its state (expanded, showDetails, lvl).