Skip to content

Commit 65bb513

Browse files
yinlianghuiclaude
andauthored
refactor(fields)!: host docs field examples in a real form, remove registerFields + createFieldRenderer (#3910) (#3953)
Ruling B of #3798 (maintainer-confirmed), the remove side of #3308's "one registration path" intent. PR #3793 corrected the misleading `@deprecated` note first and is the safety net this builds on. The docs field demos rendered a BARE field node as a labelled, editable input, which no application can do: on the live path a bare field node has no host for its label or its value. That only worked because the docs site registered a demo-only adapter over the same `field:<type>` keys the live path owns. The field docs are a first-hand transcription source for AI authors, so they were teaching a shape that does not work. - catalog: 74 bare-node examples under `examples/schema-catalog/src/schemas/ fields-*` become `{ type: 'form', showSubmit: false, showCancel: false, fields: [...] }` (2 were already form-hosted). A node's `value` moves to the form's `defaultValues`: the form spreads react-hook-form's state after the schema props, so a field-level `value` is silently ignored (measured — field-level yields an empty input, form-level yields the value). All 74 round-trip to their original content with `value` relocated and nothing else added or dropped. - fields: delete `registerFields()` and `createFieldRenderer()`, plus the 37 static widget imports that existed only to feed the wrapper. Tombstones record why, and carry forward the `capability-multiselect` tombstone that lived inside the removed function. - site: `ObjectUIProvider` drops the `registerFields()` call for a side-effect import; the module's `registerAllFields()` is the one seam. - three widget comments credited the `field.field` nesting to the deleted wrapper, which never produced it — re-pointed at the form renderer's declared metadata slot (#3090), the real producer. The 26 `content/docs/fields/*.mdx` pages need no edits: each consumes only `<SchemaExample id>` (76 references, one per catalog example). New catalog pins: every `fields-*` example is form-rooted, carries no dead field-level `value`, and renders a real `form` with its label in the field's own chrome; three value-bearing examples assert the seeded value reaches the DOM. Reverse-verified — restoring either limb turns exactly these red. Co-authored-by: Claude <noreply@anthropic.com>
1 parent b14ab3a commit 65bb513

83 files changed

Lines changed: 2924 additions & 2324 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@object-ui/fields": minor
3+
---
4+
5+
fields: remove the docs-demo registration path (`registerFields` + `createFieldRenderer`), and host the docs field examples in a real form
6+
7+
**Breaking (shipped as `minor` per AGENTS.md §版本号策略 — objectui's major tracks `@objectstack`, so the repo's own breaking changes are `minor` with the break spelled out here):** two public exports are removed from `@object-ui/fields`:
8+
9+
- `registerFields()` — registered every widget under the same `field:<type>` keys as `registerAllFields()`, but wrapped in a demo-only renderer. Two paths writing the same registry keys meant whoever ran LAST won it for every consumer sharing the registry.
10+
- `createFieldRenderer()` — that wrapper. It synthesized a `label`, a description, and a local `useState`/`onChange` around a widget.
11+
12+
**Migration:** there is nothing to migrate for an application. `registerAllFields()` runs on import of `@object-ui/fields` and is the one registration seam (`registerField(type)` for a single type); no shipped application ever called the removed pair — the docs site was the only caller. Code that rendered a **bare field node** standalone (`{ type: 'currency', label: 'Amount' }`) and relied on the wrapper for label and value state must host the field in a form instead: `{ type: 'form', fields: [{ name: 'amount', label: 'Amount', type: 'currency' }] }`. Seed values through the form's `defaultValues`, not a field-level `value`.
13+
14+
**Why the removal rather than a relocation** (ruling B of objectui#3798, confirmed by the maintainer; objectui#3308 is the origin, PR #3793 the safety net that first corrected the misleading `@deprecated` note): the wrapper existed only so the documentation could render a bare field node as a labelled, editable input. No application produces that rendering — on the live path a bare field node has no host for its label or its value. So the field docs, which are a first-hand transcription source for AI authors, were teaching a shape that does not work, and an author copying it got a node with neither label nor `onChange`. Relocating the wrapper into the docs site would have preserved that divergence under a new owner. Hosting the examples in a real form removes the reason for the wrapper to exist: the form renderer already owns label and value state, so the docs can only show what an application actually renders.
15+
16+
The 74 bare-node examples under `examples/schema-catalog/src/schemas/fields-*` are now form-hosted (the other 2 already were). A field's `value` moved to the form's `defaultValues`, because the form renderer spreads react-hook-form's state after the schema props and a field-level `value` is therefore ignored — a catalog guard now pins that so a dead `value` cannot come back.

apps/site/app/components/ObjectUIProvider.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22

33
// Import components to trigger registration
44
import { initializeComponents } from '@object-ui/components';
5-
import { registerFields } from '@object-ui/fields';
5+
// Side-effect import: the module runs `registerAllFields()` at load, which is the
6+
// ONE registration path (objectui#3308). The docs site used to also call
7+
// `registerFields()` here — the demo adapter that re-registered every
8+
// `field:<type>` key wrapped in synthesized label/state chrome, so the docs
9+
// rendered bare field nodes in a way no real application does. Ruling B of
10+
// objectui#3798 removed it: the catalog's field examples are form-hosted
11+
// (`{ type: 'form', fields: [...] }`) and the real form renderer owns the label
12+
// and the value state (objectui#3910).
13+
import '@object-ui/fields';
614
import { ComponentRegistry } from '@object-ui/core';
715
import { useEffect } from 'react';
816

917
export function ObjectUIProvider({ children }: { children: React.ReactNode }) {
1018
// Explicitly call init to ensure components are registered
1119
useEffect(() => {
1220
initializeComponents();
13-
registerFields();
14-
21+
1522
// Wait a bit for plugins to register, then log
1623
setTimeout(() => {
1724
const componentTypes = ComponentRegistry.getAllTypes();
Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
{
2-
"type": "auto_number",
3-
"name": "order_number",
4-
"label": "Order Number",
5-
"readonly": true,
6-
"value": "ORD-0001"
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"defaultValues": {
6+
"order_number": "ORD-0001"
7+
},
8+
"fields": [
9+
{
10+
"name": "order_number",
11+
"label": "Order Number",
12+
"type": "auto_number",
13+
"readonly": true
14+
}
15+
]
716
}
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
{
2-
"type": "auto_number",
3-
"name": "ticket_id",
4-
"label": "Ticket ID",
5-
"format": "TKT-{YYYY}{MM}-{0000}",
6-
"readonly": true,
7-
"value": "TKT-202403-0567"
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"defaultValues": {
6+
"ticket_id": "TKT-202403-0567"
7+
},
8+
"fields": [
9+
{
10+
"name": "ticket_id",
11+
"label": "Ticket ID",
12+
"type": "auto_number",
13+
"format": "TKT-{YYYY}{MM}-{0000}",
14+
"readonly": true
15+
}
16+
]
817
}
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
{
2-
"type": "auto_number",
3-
"name": "invoice_id",
4-
"label": "Invoice ID",
5-
"format": "INV-{0000}",
6-
"readonly": true,
7-
"value": "INV-1234"
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"defaultValues": {
6+
"invoice_id": "INV-1234"
7+
},
8+
"fields": [
9+
{
10+
"name": "invoice_id",
11+
"label": "Invoice ID",
12+
"type": "auto_number",
13+
"format": "INV-{0000}",
14+
"readonly": true
15+
}
16+
]
817
}
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
{
2-
"type": "boolean",
3-
"name": "is_active",
4-
"label": "Active"
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"fields": [
6+
{
7+
"name": "is_active",
8+
"label": "Active",
9+
"type": "boolean"
10+
}
11+
]
512
}
Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
{
2-
"type": "boolean",
3-
"name": "terms",
4-
"label": "I agree to the terms and conditions",
5-
"value": true
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"defaultValues": {
6+
"terms": true
7+
},
8+
"fields": [
9+
{
10+
"name": "terms",
11+
"label": "I agree to the terms and conditions",
12+
"type": "boolean"
13+
}
14+
]
615
}
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
{
2-
"type": "boolean",
3-
"name": "notifications",
4-
"label": "Email Notifications",
5-
"description": "Receive email updates about your account"
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"fields": [
6+
{
7+
"name": "notifications",
8+
"label": "Email Notifications",
9+
"type": "boolean",
10+
"description": "Receive email updates about your account"
11+
}
12+
]
613
}
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
{
2-
"type": "currency",
3-
"name": "price_eur",
4-
"label": "Price (EUR)",
5-
"placeholder": "€0.00",
6-
"currency": "EUR"
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"fields": [
6+
{
7+
"name": "price_eur",
8+
"label": "Price (EUR)",
9+
"type": "currency",
10+
"placeholder": "€0.00",
11+
"currency": "EUR"
12+
}
13+
]
714
}
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
{
2-
"type": "currency",
3-
"name": "amount",
4-
"label": "Amount",
5-
"placeholder": "$0.00",
6-
"currency": "USD"
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"fields": [
6+
{
7+
"name": "amount",
8+
"label": "Amount",
9+
"type": "currency",
10+
"placeholder": "$0.00",
11+
"currency": "USD"
12+
}
13+
]
714
}

0 commit comments

Comments
 (0)