Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
- [ ] Advanced lookup: dependent lookups (filter based on other fields)
- [ ] Hierarchical lookups (parent-child relationships)
- [ ] Lookup result caching
- [x] Lookup field dynamic DataSource loading — popup fetches records via `DataSource.find()` with `$search` debounce, loading/error/empty states
- [x] Lookup field context DataSource — reads DataSource from SchemaRendererContext so forms work without explicit prop
- [x] Lookup field UX polish — arrow key navigation, description field display, quick-create entry, ARIA listbox roles
- [ ] Form conditional logic with branching
- [ ] Multi-page forms with progress indicator

Expand Down
74 changes: 42 additions & 32 deletions content/docs/fields/lookup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Reference field for linking to other objects/records"

import { ComponentDemo, DemoGrid } from '@/app/components/ComponentDemo';

The Lookup Field component provides a reference field for creating relationships between objects and records.
The Lookup Field component provides a reference field for creating relationships between objects and records. It supports dynamic data loading from a DataSource with debounced search, loading/error/empty states, keyboard navigation, and optional quick-create entry.

## Basic Usage

Expand Down Expand Up @@ -62,24 +62,51 @@ interface LookupFieldSchema {
// Reference Configuration
reference_to: string; // Referenced object/table name
reference_field?: string; // Field to display (default: 'name')
description_field?: string; // Secondary field shown below label
id_field?: string; // ID field on records (default: '_id')
options?: LookupOption[]; // Available options (if static)

// Data Source (for dynamic lookups)
dataSource?: {
api?: string; // API endpoint
method?: string; // HTTP method
params?: Record<string, any>; // Query parameters
};
// Data Source (automatic via SchemaRendererContext, or explicit)
// When a DataSource is available, the popup dynamically loads
// records from the referenced object on open, with debounced search.
dataSource?: DataSource;

// Quick-create callback (shown when no results found)
onCreateNew?: (searchQuery: string) => void;
}

interface LookupOption {
label: string; // Display label
value: string; // Record ID
description?: string; // Secondary text below label
_id?: string; // Alternative ID field
name?: string; // Alternative label field
}
```

## Dynamic Data Source

When a `DataSource` is available (via `SchemaRendererContext`, explicit prop, or field config), the Lookup popup **automatically** fetches records from the referenced object:

```plaintext
// Automatic — DataSource from SchemaRendererContext
// (works out-of-the-box in ObjectForm, DrawerForm, etc.)
{
type: 'lookup',
name: 'customer',
label: 'Customer',
reference_to: 'customers',
reference_field: 'name', // Display field (default: 'name')
description_field: 'industry', // Optional secondary field
}
Comment on lines +87 to +101
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section states that Lookup can obtain a DataSource via SchemaRendererContext, but the current LookupField implementation relies on a dynamic require('@object-ui/react') fallback that won’t resolve under ESM-only @object-ui/react exports. If the intended mechanism is “form renderer passes dataSource down”, consider updating the docs to describe that flow (or update the widget to use a real context import).

Copilot uses AI. Check for mistakes.
```

The popup will:
1. Fetch records via `dataSource.find(reference_to, { $top: 50 })` on open
2. Send `$search` queries with 300ms debounce as the user types
3. Show loading spinner, error state with retry, and empty state
4. Display "Showing X of Y" when more records exist than the page size

## Lookup vs Master-Detail

- **Lookup**: Standard reference field, can be deleted independently
Expand All @@ -104,31 +131,14 @@ import { LookupCellRenderer } from '@object-ui/fields';
- **Parent Records**: Master-detail relationships
- **Team Members**: Multi-user references

## Dynamic Data Source

For lookups that fetch data from an API:

```plaintext
{
type: 'lookup',
name: 'customer',
label: 'Customer',
reference_to: 'customers',
dataSource: {
api: '/api/customers',
method: 'GET',
params: {
active: true,
limit: 100
}
}
}
```

## Features

- **Search**: Type-ahead search in options
- **Dynamic DataSource Loading**: Automatically fetches records from referenced objects
- **Search**: Debounced type-ahead search with `$search` parameter
- **Multi-Select**: Support for multiple references
- **Lazy Loading**: Load options on demand
- **Relationships**: Create data relationships
- **Cascading**: Support for dependent lookups
- **Keyboard Navigation**: Arrow keys to navigate, Enter to select
- **Loading/Error/Empty States**: Friendly feedback for all states
- **Secondary Field Display**: Show description/subtitle per option
- **Quick-Create Entry**: Optional "Create new" button when no results
- **Pagination Hint**: Shows total count when more results available
- **Backward Compatible**: Falls back to static options when no DataSource
7 changes: 7 additions & 0 deletions packages/components/src/renderers/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Alert, AlertDescription } from '../../ui/alert';
import { AlertCircle, Loader2 } from 'lucide-react';
import { cn } from '../../lib/utils';
import React from 'react';
import { SchemaRendererContext } from '@object-ui/react';

// Form renderer component - Airtable-style feature-complete form
ComponentRegistry.register('form',
Expand Down Expand Up @@ -57,6 +58,11 @@ ComponentRegistry.register('form',
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [submitError, setSubmitError] = React.useState<string | null>(null);

// Read DataSource from SchemaRendererContext and propagate it to field
// widgets as a prop so they can dynamically load related records.
const schemaCtx = React.useContext(SchemaRendererContext);
const contextDataSource = schemaCtx?.dataSource ?? null;

// React to defaultValues changes
React.useEffect(() => {
form.reset(defaultValues);
Expand Down Expand Up @@ -313,6 +319,7 @@ ComponentRegistry.register('form',
options: fieldProps.options,
placeholder: fieldProps.placeholder,
disabled: disabled || fieldDisabled || readonly || isSubmitting,
dataSource: contextDataSource,
})}
Comment on lines 319 to 323
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing dataSource: contextDataSource here will override any per-field dataSource present in fieldProps and also forwards dataSource to built-in inputs/selects (which can end up as an unknown DOM prop). Prefer preserving precedence (fieldProps.dataSource ?? contextDataSource) and only including the prop when defined (e.g., conditional spread) so standard DOM inputs don’t receive it.

Copilot uses AI. Check for mistakes.
</FormControl>
{description && (
Expand Down
Loading