| title | Field Registry |
|---|
Object UI uses a Field Registry system to decouple the core engine from specific UI implementations of fields. This allows for rich extensibility and plugin support.
The @object-ui/fields package serves as the "Universal Language" for rendering values.
When a component like <ObjectGrid> needs to render a date field, it doesn't import a DatePicker directly. Instead, it asks the registry:
"Hey, give me the component responsible for rendering type 'date'."
This architecture allows you to:
- Override standard fields (e.g. replace the native date picker with a fancy one).
- Add new field types (e.g. add a
ratingorsignaturefield). - Keep bundles small (heavy components like Code Editors are loaded only if their plugin is registered).
You can register a custom renderer globally, typically at your app's entry point.
// src/setup.tsx
import { registerFieldRenderer, type CellRendererProps } from '@object-ui/fields';
const MyRatingField = ({ value, onChange }: CellRendererProps) => {
return (
<div className="rating">
{[1, 2, 3, 4, 5].map(star => (
<span
key={star}
onClick={() => onChange?.(star)}
style={{ color: star <= value ? 'gold' : 'grey' }}
>
★
</span>
))}
</div>
);
};
// Register it
registerFieldRenderer('rating', MyRatingField);Once registered, you can simply use the new type in your JSON schema.
{
"type": "form",
"fields": [
{
"name": "customer_satisfaction",
"type": "rating",
"label": "Satisfaction"
}
]
}Object UI comes with built-in support for the standard ObjectStack Protocol types:
| Type | Description |
|---|---|
text |
Single line text |
textarea |
Multi-line text |
number |
Numeric input |
currency |
Currency formatting |
percent |
Percentage values |
date |
Date picker |
datetime |
Date & Time picker |
boolean |
Checkbox / Switch |
select |
Dropdown |
lookup |
Reference to another object |
master_detail |
Parent-child relationship |
user |
Person picker — searches the sys_user object (a lookup specialized to users) |
owner |
Record owner — a user field, typically read-only and stamped with the current user |
number, currency, percent and geolocation render a native
type="number" input. The browser — not ObjectUI — decides what that box will
accept, and it rewrites some entries before any widget code runs. Two
different things can happen, and only one of them is announced.
If the box is left holding something that is not a complete number, the browser
reports validity.badInput and these widgets now say so: the control is marked
aria-invalid="true" and a message is drawn under it —
Not saved: the text in this box is not a number. Enter a plain decimal (example: 1234.56).
Measured in Chromium 141, typing any of 1e, 1e-, 1e+, 5e, -, .,
+, -. or e leaves the box visibly displaying what was typed while its
value reads empty. Before this was announced, the field simply stored nothing
and said nothing.
This is the important limitation, and it is deliberate rather than an oversight.
| you paste / type | the field stores |
|---|---|
1.2.3 |
1.23 |
0x10 |
10 |
12abc |
12 |
No warning is shown for these, and no widget-side check can add one. The
browser filters the keystrokes or the pasted text as it arrives, so by the time
ObjectUI sees the field the discarded characters are already gone — there is
nothing left to detect. This is native type="number" behaviour; recovering it
would mean giving up the numeric keyboard on mobile and the min/max/step
spinner on every numeric field in the product.
⛔ So do not read "no warning" as "the value is correct." A warning means the
browser could not read the box at all. Silence means the browser read
something — which may be less than you typed. When exact input matters
(reference codes, serial numbers, anything where 1.2.3 is meaningful), declare
a text field, not a numeric one.
If you are building your own custom component (like a Kanban board card), you can leverage the registry to render fields without reinventing the wheel.
import { getCellRenderer } from '@object-ui/fields';
export const KanbanCard = ({ task }: { task: { name: string; assignee: string } }) => {
// Get the standard renderer for a 'user' type field
const UserRenderer = getCellRenderer('user');
return (
<div className="card">
<h3>{task.name}</h3>
<div className="assignee">
<UserRenderer
value={task.assignee}
field={{ type: 'user', name: 'assignee' }}
/>
</div>
</div>
);
};