ObjectUIObjectUI
Fields

Grid Field

Sub-table field for inline tabular data

The Grid Field component provides an inline table for managing related records or tabular data within a parent record. It displays data in rows and columns with optional editing capabilities.

Basic Usage

Basic Grid

#ProductQuantityPrice
1
¥

With Data

Grid With Data

#ItemQtyPrice
1
¥
2
¥
3
¥
4
¥

Read-Only

Read Only Grid

#DateDescriptionAmount
13/15/2024Payment received100
23/14/2024Service charge-5

Field Schema

A grid field is authored as GridFieldMetadata (@object-ui/types), which is the source of truth for the key set: it extends BaseFieldMetadata with the column list and the row-count and row-action limits. Each column is a GridColumnDefinition, so the columns are checked by the same compiler that checks the field.

import type { GridFieldMetadata } from '@object-ui/types';

const lineItems: GridFieldMetadata = {
  type: 'grid',
  name: 'line_items',
  label: 'Line Items',
  columns: [
    { name: 'product', label: 'Product', type: 'lookup', required: true, width: 240 },
    { name: 'quantity', label: 'Qty', type: 'number', defaultValue: 1, width: 80 },
    { name: 'unit_price', label: 'Unit Price', type: 'currency', width: 120 },
  ],
  min_rows: 1,
  max_rows: 50,
  allow_add: true,
  allow_delete: true,
  allow_reorder: false,
};

A column's width is a number of pixels, and there is no per-column editable key: whether cells can be edited follows the field's own read-only state.

The value being edited, and the className / disabled a host supplies, are not metadata keys — they are runtime widget props. See Field Widget Props.

Column Types

Columns can use any field type:

columns: [
  { name: 'name', label: 'Name', type: 'text', required: true },
  { name: 'quantity', label: 'Qty', type: 'number', min: 1 },
  { name: 'price', label: 'Price', type: 'currency', currency: 'USD' },
  { name: 'date', label: 'Date', type: 'date' },
  { name: 'status', label: 'Status', type: 'select', options: [...] },
  { name: 'active', label: 'Active', type: 'boolean' },
  { name: 'receipt', label: 'Receipt', type: 'file', accept: ['image/*', '.pdf'] }
]

File / image columns

A file column renders a real upload control inside the cell — a compact "Upload" button that opens the native file picker, with uploaded files shown as removable chips (image files show a thumbnail). This covers the common "attach a receipt per expense line" pattern without opening the per-row form.

  • accept?: string[] — restrict the picker (e.g. ['image/*', '.pdf']).
  • multiple?: boolean — allow several files per cell.
  • Uploads go through the configured UploadProvider adapter, exactly like the full-size file field.

When grid columns are auto-derived from a child object's schema (master-detail subforms), file / image / avatar fields map to file columns automatically — image-flavoured fields default to accept: ['image/*'].

Because file/image/avatar now render in-grid, a child object with a single such field keeps the inline grid form factor by default (the smart inlineEdit heuristic no longer forces a per-row form for one attachment column). Only a truly form-only field (textarea / rich text / JSON / location) or several rich fields tips the default to the per-row form; an explicit inlineEdit: 'grid' | 'form' always wins.

Data Format

Grid data is stored as an array of objects:

const gridValue = [
  { product: 'Item 1', quantity: 2, price: 29.99 },
  { product: 'Item 2', quantity: 1, price: 49.99 },
  { product: 'Item 3', quantity: 5, price: 9.99 }
];

Common Patterns

Invoice Line Items

{
  type: 'grid',
  name: 'line_items',
  label: 'Line Items',
  columns: [
    { name: 'description', label: 'Description', type: 'text', required: true },
    { name: 'quantity', label: 'Quantity', type: 'number', min: 1, required: true },
    { name: 'unit_price', label: 'Unit Price', type: 'currency', required: true },
    { name: 'amount', label: 'Amount', type: 'currency', readonly: true }
  ]
}

Order Details

{
  type: 'grid',
  name: 'order_details',
  label: 'Order Details',
  columns: [
    { name: 'sku', label: 'SKU', type: 'text' },
    { name: 'product', label: 'Product', type: 'lookup', reference_to: 'products' },
    { name: 'quantity', label: 'Qty', type: 'number' },
    { name: 'price', label: 'Price', type: 'currency' },
    { name: 'discount', label: 'Discount', type: 'percent' },
    { name: 'total', label: 'Total', type: 'currency', readonly: true }
  ]
}

Task Checklist

{
  type: 'grid',
  name: 'tasks',
  label: 'Tasks',
  columns: [
    { name: 'task', label: 'Task', type: 'text', required: true },
    { name: 'assigned_to', label: 'Assigned To', type: 'user' },
    { name: 'due_date', label: 'Due Date', type: 'date' },
    { name: 'completed', label: 'Done', type: 'boolean' }
  ]
}

Features

  • Table Display: Clean tabular layout
  • Pagination Preview: Shows first 5 rows with "Showing X of Y" indicator
  • Type-Specific Rendering: Each column renders according to its type
  • Read-Only Mode: Full table view without editing
  • Responsive: Scrollable for many columns

Cell Renderer

In tables/grids, a grid value is shown as a compact placeholder, not as a nested table. It has no named renderer export of its own — the component is resolved by field type, which is the supported path for every type:

import { getCellRenderer } from '@object-ui/fields';

const GridCell = getCellRenderer('grid');

// <GridCell value={rows} field={field} /> renders: [Grid]

Full Grid Functionality

For advanced grid features, use the grid plugin (@object-ui/plugin-grid):

// Basic inline grid (simple display)
{ type: 'grid', name: 'items', columns: [...] }

// Advanced grid with full features (requires plugin)
// Keys sit on the node — a `props` envelope is never read by the renderer
{
  type: 'object-grid',   // the registered type name — there is no `plugin:grid`
  bind: 'items',         // resolves the array from the surrounding data scope
  columns: [             // spec `ListColumn[]` — each entry is keyed by `field`
    { field: 'product', label: 'Product' },
    { field: 'quantity', label: 'Quantity', sortable: false }
  ],
  editable: true,               // Plugin feature: inline cell editing
  pagination: { pageSize: 20 }  // Plugin feature: pagination
}

Note: editable and pagination are read off the node by @object-ui/plugin-grid, not by the basic grid field. Sorting and search are not node keys: column sorting is on by default and is turned off per column ({ field, sortable: false }), and there is no node-level filterable — a grid that fetches its own rows declares its query with objectName plus filter / sort.

Use Cases

  • Invoice/Order Line Items: Product lines, services
  • Expense Reports: Expense entries, receipts
  • Time Tracking: Time entries, work logs
  • Inventory: Stock items, materials
  • Checklists: Task lists, requirements
  • Schedules: Appointments, bookings
  • Configurations: Settings lists, parameters

Backend Storage

Grid data is typically stored as JSON:

// Database column type: JSONB (PostgreSQL)
interface OrderRecord {
  id: string;
  customer_id: string;
  line_items: Array<{
    product: string;
    quantity: number;
    price: number;
  }>;
}

// Store in database
const order = {
  customer_id: 'CUST-123',
  line_items: [
    { product: 'Widget A', quantity: 2, price: 29.99 },
    { product: 'Widget B', quantity: 1, price: 49.99 }
  ]
};

await db.insert('orders', order);

Validation

Example validation for grid data:

const validateGridData = (data: any[], columns: ColumnDefinition[]) => {
  const errors: string[] = [];
  
  data.forEach((row, index) => {
    columns.forEach(col => {
      // Check required columns
      if (col.required && !row[col.name]) {
        errors.push(`Row ${index + 1}: ${col.label} is required`);
      }
      
      // Validate by type
      if (col.type === 'number' && isNaN(row[col.name])) {
        errors.push(`Row ${index + 1}: ${col.label} must be a number`);
      }
      
      // Check min/max
      if (col.min !== undefined && row[col.name] < col.min) {
        errors.push(`Row ${index + 1}: ${col.label} must be >= ${col.min}`);
      }
    });
  });
  
  return errors;
};

Integration with Advanced Grid

For full-featured grids, use the @object-ui/plugin-grid package which provides:

  • Inline editing
  • Add/remove rows
  • Sorting and filtering
  • Drag-and-drop reordering
  • Export functionality
  • Formula columns
  • Aggregation rows

On this page