@metaobjectsdev/codegen-ts-react 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"form-file.d.ts","sourceRoot":"","sources":["../../src/templates/form-file.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAa,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAMjE,OAAO,EAAE,KAAK,aAAa,EAAgE,MAAM,4BAA4B,CAAC;AAwC9H,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CAgG7E"}
1
+ {"version":3,"file":"form-file.d.ts","sourceRoot":"","sources":["../../src/templates/form-file.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAa,UAAU,EAAgB,MAAM,0BAA0B,CAAC;AAkB/E,OAAO,EAAE,KAAK,aAAa,EAAgE,MAAM,4BAA4B,CAAC;AA0N9H,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CAmH7E"}
@@ -16,8 +16,8 @@
16
16
  // Form generation is OPT-IN per entity via `@emitForm: true` on the
17
17
  // object metadata. Default off. Most projects don't need stock forms.
18
18
  import { code, imp } from "ts-poet";
19
- import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
20
- import { IDENTITY_SUBTYPE_PRIMARY, IDENTITY_ATTR_FIELDS, FIELD_ATTR_DEFAULT, } from "@metaobjectsdev/metadata";
19
+ import { MetaField, MetaObject, stripPackage } from "@metaobjectsdev/metadata";
20
+ import { IDENTITY_SUBTYPE_PRIMARY, IDENTITY_ATTR_FIELDS, FIELD_ATTR_DEFAULT, FIELD_SUBTYPE_OBJECT, FIELD_ATTR_OBJECT_REF, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_DATE, FIELD_SUBTYPE_TIME, FIELD_SUBTYPE_TIMESTAMP, } from "@metaobjectsdev/metadata";
21
21
  import { entityModuleSpecifier, GENERATED_HEADER, tphDiscriminatorPin } from "@metaobjectsdev/codegen-ts";
22
22
  function primaryFieldNames(entity) {
23
23
  const set = new Set();
@@ -47,7 +47,7 @@ function isAutoManaged(field) {
47
47
  * and injected server-side, never rendered. */
48
48
  function visibleFields(entity, discField) {
49
49
  const pkNames = primaryFieldNames(entity);
50
- const names = [];
50
+ const out = [];
51
51
  // fields() returns effective fields, so inherited fields (from extends:/super:) are included in forms.
52
52
  for (const child of entity.fields()) {
53
53
  if (child.ownAttr("formExclude") === true)
@@ -58,9 +58,164 @@ function visibleFields(entity, discField) {
58
58
  continue;
59
59
  if (discField !== undefined && child.name === discField)
60
60
  continue;
61
- names.push(child.name);
61
+ out.push(child);
62
62
  }
63
- return names;
63
+ return out;
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Nested value-object sub-forms (issue #95)
67
+ // ---------------------------------------------------------------------------
68
+ /** Max recursion depth for nested value objects — a backstop alongside the
69
+ * per-branch visited-set cycle guard. */
70
+ const MAX_VO_DEPTH = 5;
71
+ /** Resolve a `field.object`'s `@objectRef` to the referenced value object from
72
+ * the loaded root. Mirrors the resolution the Zod / inferred-type templates use
73
+ * (read `@objectRef`, strip the package, look the bare name up on the root). */
74
+ function resolveValueObject(field, ctx) {
75
+ if (field.subType !== FIELD_SUBTYPE_OBJECT)
76
+ return undefined;
77
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
78
+ if (typeof ref !== "string" || ref.length === 0)
79
+ return undefined;
80
+ const base = stripPackage(ref);
81
+ return ctx.loadedRoot.objects().find((o) => o.name === base);
82
+ }
83
+ /** camelCase / PascalCase → human label ("llmConfig" → "Llm Config"). */
84
+ function humanize(s) {
85
+ return s
86
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
87
+ .replace(/_/g, " ")
88
+ .replace(/\b\w/g, (c) => c.toUpperCase())
89
+ .trim();
90
+ }
91
+ /** A real HTML <input type=...> for a scalar field subtype, or undefined (text). */
92
+ function htmlTypeForSubType(subType) {
93
+ switch (subType) {
94
+ case FIELD_SUBTYPE_INT:
95
+ case FIELD_SUBTYPE_LONG:
96
+ case FIELD_SUBTYPE_DOUBLE:
97
+ case FIELD_SUBTYPE_FLOAT:
98
+ case FIELD_SUBTYPE_DECIMAL:
99
+ case FIELD_SUBTYPE_CURRENCY:
100
+ return "number";
101
+ case FIELD_SUBTYPE_BOOLEAN:
102
+ return "checkbox";
103
+ case FIELD_SUBTYPE_DATE:
104
+ return "date";
105
+ case FIELD_SUBTYPE_TIME:
106
+ return "time";
107
+ case FIELD_SUBTYPE_TIMESTAMP:
108
+ return "datetime-local";
109
+ default:
110
+ return undefined;
111
+ }
112
+ }
113
+ /** The react-hook-form `register(...)` argument for a nested field path. A
114
+ * static path is a plain string literal; a path threaded through an array
115
+ * element carries a runtime `${index}` and must be a template literal. */
116
+ function registerArg(path, dynamic) {
117
+ return dynamic ? `\`${path}\`` : JSON.stringify(path);
118
+ }
119
+ /** A stable, identifier-safe useFieldArray hook variable for a static path. */
120
+ function arrayHookVar(path) {
121
+ return `${path.replace(/[^A-Za-z0-9]+/g, "_")}Array`;
122
+ }
123
+ /**
124
+ * Render one NESTED field inside a value-object sub-form. Dispatches on subtype:
125
+ * - a resolvable `field.object` (single) → a nested <fieldset> recursing into
126
+ * the value object's fields;
127
+ * - a resolvable `field.object` array → a useFieldArray repeatable group (when
128
+ * the path is static) or a degraded element-shape fieldset + TODO (when the
129
+ * path is already inside an array element, i.e. dynamic);
130
+ * - anything else → a single <input> bound via a nested react-hook-form path.
131
+ */
132
+ function renderNestedField(field, path, dynamic, ctx, visited, depth) {
133
+ const vo = resolveValueObject(field, ctx);
134
+ if (vo === undefined) {
135
+ // Scalar / enum / unresolved object → a single bound input.
136
+ const htmlType = htmlTypeForSubType(field.subType);
137
+ const typeAttr = htmlType !== undefined ? ` type=${JSON.stringify(htmlType)}` : "";
138
+ return {
139
+ jsx: `<div className="metaobjects-field" key=${JSON.stringify(path)}>
140
+ <label className="metaobjects-field-label">${humanize(field.name)}</label>
141
+ <input className="metaobjects-field-input"${typeAttr} {...form.register(${registerArg(path, dynamic)})} />
142
+ </div>`,
143
+ hooks: [],
144
+ };
145
+ }
146
+ const voKey = vo.fqn();
147
+ if (visited.has(voKey) || depth >= MAX_VO_DEPTH) {
148
+ // Cycle or too deep — emit a labeled shell + TODO instead of a flat input.
149
+ return {
150
+ jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
151
+ <legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
152
+ {/* TODO(#95): nested value object ${vo.name} not expanded (recursion guard). Author this sub-form by hand. */}
153
+ </fieldset>`,
154
+ hooks: [],
155
+ };
156
+ }
157
+ const nextVisited = new Set(visited);
158
+ nextVisited.add(voKey);
159
+ if (!field.isArray) {
160
+ // Single nested value object → a fieldset recursing into its fields.
161
+ const inner = [];
162
+ const hooks = [];
163
+ for (const sub of vo.fields()) {
164
+ const r = renderNestedField(sub, `${path}.${sub.name}`, dynamic, ctx, nextVisited, depth + 1);
165
+ inner.push(r.jsx);
166
+ hooks.push(...r.hooks);
167
+ }
168
+ return {
169
+ jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
170
+ <legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
171
+ ${inner.join("\n")}
172
+ </fieldset>`,
173
+ hooks,
174
+ };
175
+ }
176
+ // Array of value objects.
177
+ if (dynamic) {
178
+ // Already inside an array element — a static useFieldArray name is impossible.
179
+ // Degrade to the element shape + a single clear TODO (never a flat input).
180
+ return {
181
+ jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
182
+ <legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
183
+ {/* TODO(#95): arrays of value objects nested inside another array are not auto-wired
184
+ (the react-hook-form path is dynamic). Render this repeatable group by hand. */}
185
+ </fieldset>`,
186
+ hooks: [],
187
+ };
188
+ }
189
+ // Static path → a useFieldArray-based repeatable group.
190
+ const hookVar = arrayHookVar(path);
191
+ const elementPath = `${path}.\${index}`;
192
+ const inner = [];
193
+ const hooks = [`const ${hookVar} = useFieldArray({ control: form.control, name: ${JSON.stringify(path)} as never });`];
194
+ for (const sub of vo.fields()) {
195
+ const r = renderNestedField(sub, `${elementPath}.${sub.name}`, true, ctx, nextVisited, depth + 1);
196
+ inner.push(r.jsx);
197
+ hooks.push(...r.hooks);
198
+ }
199
+ const label = humanize(field.name);
200
+ const itemLabel = humanize(vo.name);
201
+ return {
202
+ jsx: `<div className="metaobjects-field-array" key=${JSON.stringify(path)}>
203
+ <span className="metaobjects-field-array-label">${label}</span>
204
+ {${hookVar}.fields.map((item, index) => (
205
+ <fieldset className="metaobjects-fieldset" key={item.id}>
206
+ <legend className="metaobjects-fieldset-legend">${itemLabel} {index + 1}</legend>
207
+ ${inner.join("\n")}
208
+ <button type="button" className="metaobjects-field-array-remove" onClick={() => ${hookVar}.remove(index)}>
209
+ Remove
210
+ </button>
211
+ </fieldset>
212
+ ))}
213
+ <button type="button" className="metaobjects-field-array-add" onClick={() => ${hookVar}.append({} as never)}>
214
+ Add ${itemLabel}
215
+ </button>
216
+ </div>`,
217
+ hooks,
218
+ };
64
219
  }
65
220
  export function renderFormFile(entity, ctx) {
66
221
  const entityName = entity.name;
@@ -79,10 +234,9 @@ export function renderFormFile(entity, ctx) {
79
234
  const ReactElementSym = imp("t:ReactElement@react");
80
235
  const SubmitHandlerSym = imp("t:SubmitHandler@react-hook-form");
81
236
  const useEntityFormSym = imp("useEntityForm@@metaobjectsdev/react");
82
- // For each visible field, emit a label + input + error block.
83
- // The input gets every metadata-derived attr via {...form.input.<field>}.
84
- const fieldBlocks = fields
85
- .map((f) => ` <div className="metaobjects-field" key=${JSON.stringify(f)}>
237
+ // The flat scalar block: a label + pre-bound input + error span, driven
238
+ // entirely by the entity constants via `form.input.<field>`. Unchanged.
239
+ const scalarBlock = (f) => ` <div className="metaobjects-field" key=${JSON.stringify(f)}>
86
240
  <label className="metaobjects-field-label" htmlFor={${entityName}.${f}.name}>
87
241
  {${entityName}.${f}.label}
88
242
  </label>
@@ -92,15 +246,33 @@ export function renderFormFile(entity, ctx) {
92
246
  {String(form.formState.errors.${f}?.message ?? '')}
93
247
  </span>
94
248
  )}
95
- </div>`)
96
- .join("\n");
249
+ </div>`;
250
+ // For each visible field: scalars keep the flat `form.input.<field>` block; a
251
+ // `field.object` with a resolvable `@objectRef` recurses into the referenced
252
+ // value object as a nested <fieldset> sub-form (issue #95). useFieldArray
253
+ // declarations for array-of-value-object fields hoist to the component top.
254
+ const blocks = [];
255
+ const fieldArrayHooks = [];
256
+ for (const f of fields) {
257
+ if (resolveValueObject(f, ctx) === undefined) {
258
+ blocks.push(scalarBlock(f.name));
259
+ continue;
260
+ }
261
+ const r = renderNestedField(f, f.name, false, ctx, new Set(), 0);
262
+ blocks.push(r.jsx);
263
+ fieldArrayHooks.push(...r.hooks);
264
+ }
265
+ const fieldBlocks = blocks.join("\n");
266
+ const hookSection = fieldArrayHooks.length > 0 ? `\n ${fieldArrayHooks.join("\n ")}` : "";
267
+ // Only pull in useFieldArray when an array-of-value-object field needs it.
268
+ const useFieldArrayImport = fieldArrayHooks.length > 0 ? `import { useFieldArray } from "react-hook-form";\n` : "";
97
269
  const literalImports = code `
98
270
  import {
99
271
  ${entityName},
100
272
  ${entityName}InsertSchema,
101
273
  } from ${JSON.stringify(entityFileSpec)};
102
274
  import type { ${entityName} as ${entityName}Row } from ${JSON.stringify(entityFileSpec)};
103
- `;
275
+ ${useFieldArrayImport}`;
104
276
  const body = code `
105
277
  export interface ${entityName}FormProps {
106
278
  onSubmit: ${SubmitHandlerSym}<Partial<${entityName}Row>>;
@@ -124,7 +296,7 @@ export function ${entityName}Form(props: ${entityName}FormProps): ${ReactElement
124
296
  ${entityName},
125
297
  ${formSchema},
126
298
  props.defaultValues !== undefined ? { defaultValues: props.defaultValues } : {},
127
- );
299
+ );${hookSection}
128
300
  return (
129
301
  <form
130
302
  className={props.className ?? 'metaobjects-form'}
@@ -1 +1 @@
1
- {"version":3,"file":"form-file.js","sourceRoot":"","sources":["../../src/templates/form-file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"form-file.js","sourceRoot":"","sources":["../../src/templates/form-file.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/codegen-ts-react",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "React codegen for metaobjects — emits <Entity>.form.tsx files using react-hook-form and @metaobjectsdev/react helpers.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,8 +45,8 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
- "@metaobjectsdev/metadata": "0.13.1",
49
- "@metaobjectsdev/codegen-ts": "0.13.1",
48
+ "@metaobjectsdev/metadata": "0.14.0",
49
+ "@metaobjectsdev/codegen-ts": "0.14.0",
50
50
  "ts-poet": "^6.10.0"
51
51
  },
52
52
  "peerDependencies": {
@@ -17,11 +17,23 @@
17
17
  // object metadata. Default off. Most projects don't need stock forms.
18
18
 
19
19
  import { code, imp } from "ts-poet";
20
- import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
20
+ import { MetaField, MetaObject, stripPackage } from "@metaobjectsdev/metadata";
21
21
  import {
22
22
  IDENTITY_SUBTYPE_PRIMARY,
23
23
  IDENTITY_ATTR_FIELDS,
24
24
  FIELD_ATTR_DEFAULT,
25
+ FIELD_SUBTYPE_OBJECT,
26
+ FIELD_ATTR_OBJECT_REF,
27
+ FIELD_SUBTYPE_INT,
28
+ FIELD_SUBTYPE_LONG,
29
+ FIELD_SUBTYPE_DOUBLE,
30
+ FIELD_SUBTYPE_FLOAT,
31
+ FIELD_SUBTYPE_DECIMAL,
32
+ FIELD_SUBTYPE_CURRENCY,
33
+ FIELD_SUBTYPE_BOOLEAN,
34
+ FIELD_SUBTYPE_DATE,
35
+ FIELD_SUBTYPE_TIME,
36
+ FIELD_SUBTYPE_TIMESTAMP,
25
37
  } from "@metaobjectsdev/metadata";
26
38
  import { type RenderContext, entityModuleSpecifier, GENERATED_HEADER, tphDiscriminatorPin } from "@metaobjectsdev/codegen-ts";
27
39
 
@@ -49,18 +61,196 @@ function isAutoManaged(field: MetaField): boolean {
49
61
  /** Visible form fields = all fields minus PK, DB-auto-defaulted, and (for a TPH
50
62
  * subtype) the discriminator — which is implicit (the form is subtype-specific)
51
63
  * and injected server-side, never rendered. */
52
- function visibleFields(entity: MetaObject, discField?: string): string[] {
64
+ function visibleFields(entity: MetaObject, discField?: string): MetaField[] {
53
65
  const pkNames = primaryFieldNames(entity);
54
- const names: string[] = [];
66
+ const out: MetaField[] = [];
55
67
  // fields() returns effective fields, so inherited fields (from extends:/super:) are included in forms.
56
68
  for (const child of entity.fields()) {
57
69
  if (child.ownAttr("formExclude") === true) continue;
58
70
  if (pkNames.has(child.name)) continue;
59
71
  if (isAutoManaged(child)) continue;
60
72
  if (discField !== undefined && child.name === discField) continue;
61
- names.push(child.name);
73
+ out.push(child);
62
74
  }
63
- return names;
75
+ return out;
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Nested value-object sub-forms (issue #95)
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /** Max recursion depth for nested value objects — a backstop alongside the
83
+ * per-branch visited-set cycle guard. */
84
+ const MAX_VO_DEPTH = 5;
85
+
86
+ /** Resolve a `field.object`'s `@objectRef` to the referenced value object from
87
+ * the loaded root. Mirrors the resolution the Zod / inferred-type templates use
88
+ * (read `@objectRef`, strip the package, look the bare name up on the root). */
89
+ function resolveValueObject(field: MetaField, ctx: RenderContext): MetaObject | undefined {
90
+ if (field.subType !== FIELD_SUBTYPE_OBJECT) return undefined;
91
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
92
+ if (typeof ref !== "string" || ref.length === 0) return undefined;
93
+ const base = stripPackage(ref);
94
+ return ctx.loadedRoot.objects().find((o) => o.name === base) as MetaObject | undefined;
95
+ }
96
+
97
+ /** camelCase / PascalCase → human label ("llmConfig" → "Llm Config"). */
98
+ function humanize(s: string): string {
99
+ return s
100
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
101
+ .replace(/_/g, " ")
102
+ .replace(/\b\w/g, (c) => c.toUpperCase())
103
+ .trim();
104
+ }
105
+
106
+ /** A real HTML <input type=...> for a scalar field subtype, or undefined (text). */
107
+ function htmlTypeForSubType(subType: string): string | undefined {
108
+ switch (subType) {
109
+ case FIELD_SUBTYPE_INT:
110
+ case FIELD_SUBTYPE_LONG:
111
+ case FIELD_SUBTYPE_DOUBLE:
112
+ case FIELD_SUBTYPE_FLOAT:
113
+ case FIELD_SUBTYPE_DECIMAL:
114
+ case FIELD_SUBTYPE_CURRENCY:
115
+ return "number";
116
+ case FIELD_SUBTYPE_BOOLEAN:
117
+ return "checkbox";
118
+ case FIELD_SUBTYPE_DATE:
119
+ return "date";
120
+ case FIELD_SUBTYPE_TIME:
121
+ return "time";
122
+ case FIELD_SUBTYPE_TIMESTAMP:
123
+ return "datetime-local";
124
+ default:
125
+ return undefined;
126
+ }
127
+ }
128
+
129
+ /** The react-hook-form `register(...)` argument for a nested field path. A
130
+ * static path is a plain string literal; a path threaded through an array
131
+ * element carries a runtime `${index}` and must be a template literal. */
132
+ function registerArg(path: string, dynamic: boolean): string {
133
+ return dynamic ? `\`${path}\`` : JSON.stringify(path);
134
+ }
135
+
136
+ /** A stable, identifier-safe useFieldArray hook variable for a static path. */
137
+ function arrayHookVar(path: string): string {
138
+ return `${path.replace(/[^A-Za-z0-9]+/g, "_")}Array`;
139
+ }
140
+
141
+ interface FieldRender {
142
+ jsx: string;
143
+ /** useFieldArray declarations to hoist to the top of the component. */
144
+ hooks: string[];
145
+ }
146
+
147
+ /**
148
+ * Render one NESTED field inside a value-object sub-form. Dispatches on subtype:
149
+ * - a resolvable `field.object` (single) → a nested <fieldset> recursing into
150
+ * the value object's fields;
151
+ * - a resolvable `field.object` array → a useFieldArray repeatable group (when
152
+ * the path is static) or a degraded element-shape fieldset + TODO (when the
153
+ * path is already inside an array element, i.e. dynamic);
154
+ * - anything else → a single <input> bound via a nested react-hook-form path.
155
+ */
156
+ function renderNestedField(
157
+ field: MetaField,
158
+ path: string,
159
+ dynamic: boolean,
160
+ ctx: RenderContext,
161
+ visited: Set<string>,
162
+ depth: number,
163
+ ): FieldRender {
164
+ const vo = resolveValueObject(field, ctx);
165
+ if (vo === undefined) {
166
+ // Scalar / enum / unresolved object → a single bound input.
167
+ const htmlType = htmlTypeForSubType(field.subType);
168
+ const typeAttr = htmlType !== undefined ? ` type=${JSON.stringify(htmlType)}` : "";
169
+ return {
170
+ jsx: `<div className="metaobjects-field" key=${JSON.stringify(path)}>
171
+ <label className="metaobjects-field-label">${humanize(field.name)}</label>
172
+ <input className="metaobjects-field-input"${typeAttr} {...form.register(${registerArg(path, dynamic)})} />
173
+ </div>`,
174
+ hooks: [],
175
+ };
176
+ }
177
+
178
+ const voKey = vo.fqn();
179
+ if (visited.has(voKey) || depth >= MAX_VO_DEPTH) {
180
+ // Cycle or too deep — emit a labeled shell + TODO instead of a flat input.
181
+ return {
182
+ jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
183
+ <legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
184
+ {/* TODO(#95): nested value object ${vo.name} not expanded (recursion guard). Author this sub-form by hand. */}
185
+ </fieldset>`,
186
+ hooks: [],
187
+ };
188
+ }
189
+ const nextVisited = new Set(visited);
190
+ nextVisited.add(voKey);
191
+
192
+ if (!field.isArray) {
193
+ // Single nested value object → a fieldset recursing into its fields.
194
+ const inner: string[] = [];
195
+ const hooks: string[] = [];
196
+ for (const sub of vo.fields()) {
197
+ const r = renderNestedField(sub, `${path}.${sub.name}`, dynamic, ctx, nextVisited, depth + 1);
198
+ inner.push(r.jsx);
199
+ hooks.push(...r.hooks);
200
+ }
201
+ return {
202
+ jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
203
+ <legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
204
+ ${inner.join("\n")}
205
+ </fieldset>`,
206
+ hooks,
207
+ };
208
+ }
209
+
210
+ // Array of value objects.
211
+ if (dynamic) {
212
+ // Already inside an array element — a static useFieldArray name is impossible.
213
+ // Degrade to the element shape + a single clear TODO (never a flat input).
214
+ return {
215
+ jsx: `<fieldset className="metaobjects-fieldset" key=${JSON.stringify(path)}>
216
+ <legend className="metaobjects-fieldset-legend">${humanize(field.name)}</legend>
217
+ {/* TODO(#95): arrays of value objects nested inside another array are not auto-wired
218
+ (the react-hook-form path is dynamic). Render this repeatable group by hand. */}
219
+ </fieldset>`,
220
+ hooks: [],
221
+ };
222
+ }
223
+
224
+ // Static path → a useFieldArray-based repeatable group.
225
+ const hookVar = arrayHookVar(path);
226
+ const elementPath = `${path}.\${index}`;
227
+ const inner: string[] = [];
228
+ const hooks: string[] = [`const ${hookVar} = useFieldArray({ control: form.control, name: ${JSON.stringify(path)} as never });`];
229
+ for (const sub of vo.fields()) {
230
+ const r = renderNestedField(sub, `${elementPath}.${sub.name}`, true, ctx, nextVisited, depth + 1);
231
+ inner.push(r.jsx);
232
+ hooks.push(...r.hooks);
233
+ }
234
+ const label = humanize(field.name);
235
+ const itemLabel = humanize(vo.name);
236
+ return {
237
+ jsx: `<div className="metaobjects-field-array" key=${JSON.stringify(path)}>
238
+ <span className="metaobjects-field-array-label">${label}</span>
239
+ {${hookVar}.fields.map((item, index) => (
240
+ <fieldset className="metaobjects-fieldset" key={item.id}>
241
+ <legend className="metaobjects-fieldset-legend">${itemLabel} {index + 1}</legend>
242
+ ${inner.join("\n")}
243
+ <button type="button" className="metaobjects-field-array-remove" onClick={() => ${hookVar}.remove(index)}>
244
+ Remove
245
+ </button>
246
+ </fieldset>
247
+ ))}
248
+ <button type="button" className="metaobjects-field-array-add" onClick={() => ${hookVar}.append({} as never)}>
249
+ Add ${itemLabel}
250
+ </button>
251
+ </div>`,
252
+ hooks,
253
+ };
64
254
  }
65
255
 
66
256
  export function renderFormFile(entity: MetaObject, ctx: RenderContext): string {
@@ -88,11 +278,9 @@ export function renderFormFile(entity: MetaObject, ctx: RenderContext): string {
88
278
  const SubmitHandlerSym = imp("t:SubmitHandler@react-hook-form");
89
279
  const useEntityFormSym = imp("useEntityForm@@metaobjectsdev/react");
90
280
 
91
- // For each visible field, emit a label + input + error block.
92
- // The input gets every metadata-derived attr via {...form.input.<field>}.
93
- const fieldBlocks = fields
94
- .map(
95
- (f) => ` <div className="metaobjects-field" key=${JSON.stringify(f)}>
281
+ // The flat scalar block: a label + pre-bound input + error span, driven
282
+ // entirely by the entity constants via `form.input.<field>`. Unchanged.
283
+ const scalarBlock = (f: string) => ` <div className="metaobjects-field" key=${JSON.stringify(f)}>
96
284
  <label className="metaobjects-field-label" htmlFor={${entityName}.${f}.name}>
97
285
  {${entityName}.${f}.label}
98
286
  </label>
@@ -102,9 +290,30 @@ export function renderFormFile(entity: MetaObject, ctx: RenderContext): string {
102
290
  {String(form.formState.errors.${f}?.message ?? '')}
103
291
  </span>
104
292
  )}
105
- </div>`,
106
- )
107
- .join("\n");
293
+ </div>`;
294
+
295
+ // For each visible field: scalars keep the flat `form.input.<field>` block; a
296
+ // `field.object` with a resolvable `@objectRef` recurses into the referenced
297
+ // value object as a nested <fieldset> sub-form (issue #95). useFieldArray
298
+ // declarations for array-of-value-object fields hoist to the component top.
299
+ const blocks: string[] = [];
300
+ const fieldArrayHooks: string[] = [];
301
+ for (const f of fields) {
302
+ if (resolveValueObject(f, ctx) === undefined) {
303
+ blocks.push(scalarBlock(f.name));
304
+ continue;
305
+ }
306
+ const r = renderNestedField(f, f.name, false, ctx, new Set<string>(), 0);
307
+ blocks.push(r.jsx);
308
+ fieldArrayHooks.push(...r.hooks);
309
+ }
310
+ const fieldBlocks = blocks.join("\n");
311
+ const hookSection =
312
+ fieldArrayHooks.length > 0 ? `\n ${fieldArrayHooks.join("\n ")}` : "";
313
+
314
+ // Only pull in useFieldArray when an array-of-value-object field needs it.
315
+ const useFieldArrayImport =
316
+ fieldArrayHooks.length > 0 ? `import { useFieldArray } from "react-hook-form";\n` : "";
108
317
 
109
318
  const literalImports = code`
110
319
  import {
@@ -112,7 +321,7 @@ import {
112
321
  ${entityName}InsertSchema,
113
322
  } from ${JSON.stringify(entityFileSpec)};
114
323
  import type { ${entityName} as ${entityName}Row } from ${JSON.stringify(entityFileSpec)};
115
- `;
324
+ ${useFieldArrayImport}`;
116
325
 
117
326
  const body = code`
118
327
  export interface ${entityName}FormProps {
@@ -137,7 +346,7 @@ export function ${entityName}Form(props: ${entityName}FormProps): ${ReactElement
137
346
  ${entityName},
138
347
  ${formSchema},
139
348
  props.defaultValues !== undefined ? { defaultValues: props.defaultValues } : {},
140
- );
349
+ );${hookSection}
141
350
  return (
142
351
  <form
143
352
  className={props.className ?? 'metaobjects-form'}