Data grid input
An editable spreadsheet-like input for tabular data entry. Use it when users need to enter or modify multiple rows of structured data.
ctx.ui.inputs.dataGrid("name", config);Config
stringThe name of the input. This will be the key in the result object.
stringHelp text displayed below the data grid.
T[]An array of objects representing the initial rows. Each object's keys become columns.
ColumnConfig[]Explicit column configuration. If not provided, columns are inferred from the data with all fields editable.
booleanWhether users can add new rows. Default is false.
booleanWhether users can delete rows. Default is false.
(data: T[]) => true | stringA validation function that receives the current data and returns true if valid, or an error message string if invalid.
Column configuration
Each column in the columns array accepts these properties:
| Property | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The property name in your data objects |
label | string | No | Display label for the column header. Defaults to the key |
type | "text" | "number" | "boolean" | "id" | No | Data type for the column. Inferred from data if not specified |
editable | boolean | No | Whether cells in this column can be edited. Defaults to true (except id type which defaults to false) |
Column types
| Type | Description |
|---|---|
text | String values |
number | Numeric values |
boolean | Checkbox (true/false) |
id | Non-editable identifier field |
Usage
The data grid returns an array of objects matching the configured columns. Add it to a page like any other input element.
const result = await ctx.ui.page({
title: "Enter order lines",
content: [
ctx.ui.inputs.dataGrid("orderLines", {
data: [
{ sku: "SKU-001", description: "Widget A", quantity: 10 },
{ sku: "SKU-002", description: "Widget B", quantity: 5 },
],
columns: [
{ key: "sku", label: "SKU", type: "text" },
{ key: "description", label: "Description", type: "text" },
{ key: "quantity", label: "Qty", type: "number" },
],
allowAddRows: true,
allowDeleteRows: true,
}),
],
});
// result.orderLines is an array of { sku: string, description: string, quantity: number }
for (const line of result.orderLines) {
console.log(`${line.sku}: ${line.quantity}`);
}Inferred columns
If you don't provide a columns configuration, the data grid infers columns from your data. Column types are determined by the JavaScript type of each value, and labels are generated from keys using sentence case.
const result = await ctx.ui.page({
title: "Products",
content: [
ctx.ui.inputs.dataGrid("products", {
data: [
{ name: "Widget A", quantity: 10, inStock: true },
{ name: "Widget B", quantity: 5, inStock: false },
],
// Columns inferred as: Name (text), Quantity (number), In stock (boolean)
}),
],
});Validation
Use the validate function to enforce business rules on the data. The function receives the current array of rows and should return true if valid, or an error message string.
ctx.ui.inputs.dataGrid("inventory", {
data: [
{ sku: "SKU001", quantity: 10, price: 99.99 },
{ sku: "SKU002", quantity: 5, price: 149.99 },
],
columns: [
{ key: "sku", label: "SKU", type: "text" },
{ key: "quantity", label: "Qty", type: "number" },
{ key: "price", label: "Price", type: "number" },
],
allowAddRows: true,
allowDeleteRows: true,
validate: (data) => {
if (data.length === 0) {
return "At least one item is required";
}
const hasInvalidQty = data.some((item) => item.quantity < 0);
if (hasInvalidQty) {
return "Quantities must be non-negative";
}
const hasInvalidPrice = data.some((item) => item.price <= 0);
if (hasInvalidPrice) {
return "All prices must be greater than zero";
}
return true;
},
});Read-only columns
Set editable: false on a column to prevent users from modifying its values. Columns with type: "id" are non-editable by default.
ctx.ui.inputs.dataGrid("orders", {
data: [
{ id: "ORD-001", customer: "Acme Corp", status: "Pending" },
{ id: "ORD-002", customer: "GlobalTech", status: "Complete" },
],
columns: [
{ key: "id", label: "Order ID", type: "id" }, // Non-editable by default
{ key: "customer", label: "Customer", type: "text" },
{ key: "status", label: "Status", type: "text", editable: false },
],
allowAddRows: false,
allowDeleteRows: false,
});Example: Stock count entry
A flow for entering stock count results during inventory checks:
import { StockCount, FlowConfig } from "@teamkeel/sdk";
const config: FlowConfig = {
name: "Stock Count",
};
export default StockCount(config, async (ctx) => {
// Fetch current inventory
const items = await ctx.models.stockItem.findMany({
where: { location: { id: ctx.params.locationId } },
});
// Display data grid for count entry
const result = await ctx.ui.page({
title: "Enter stock counts",
content: [
ctx.ui.display.header({ title: "Count all items at this location" }),
ctx.ui.inputs.dataGrid("counts", {
data: items.map((item) => ({
id: item.id,
sku: item.sku,
name: item.name,
expectedQty: item.quantity,
countedQty: item.quantity,
})),
columns: [
{ key: "sku", label: "SKU", type: "text", editable: false },
{ key: "name", label: "Name", type: "text", editable: false },
{ key: "expectedQty", label: "Expected", type: "number", editable: false },
{ key: "countedQty", label: "Counted", type: "number", editable: true },
],
validate: (data) => {
const hasNegative = data.some((row) => row.countedQty < 0);
if (hasNegative) {
return "Counted quantities cannot be negative";
}
return true;
},
}),
],
});
// Process variances
for (const count of result.counts) {
if (count.countedQty !== count.expectedQty) {
await ctx.models.stockVariance.create({
stockItemId: count.id,
expected: count.expectedQty,
counted: count.countedQty,
variance: count.countedQty - count.expectedQty,
});
}
}
return { message: "Stock count recorded" };
});