@fogpipe/forma-react 0.12.0-alpha.1 → 0.12.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.
@@ -5,12 +5,36 @@
5
5
  * Supports single-page and multi-page (wizard) forms.
6
6
  */
7
7
 
8
- import React, { forwardRef, useImperativeHandle, useRef, useMemo, useCallback } from "react";
9
- import type { Forma, FieldDefinition, ValidationResult, JSONSchemaProperty, SelectOption } from "@fogpipe/forma-core";
8
+ import React, {
9
+ forwardRef,
10
+ useImperativeHandle,
11
+ useRef,
12
+ useMemo,
13
+ useCallback,
14
+ } from "react";
15
+ import type {
16
+ Forma,
17
+ FieldDefinition,
18
+ ValidationResult,
19
+ JSONSchemaProperty,
20
+ SelectOption,
21
+ } from "@fogpipe/forma-core";
10
22
  import { isAdornableField, isSelectionField } from "@fogpipe/forma-core";
11
23
  import { useForma } from "./useForma.js";
12
24
  import { FormaContext } from "./context.js";
13
- import type { ComponentMap, LayoutProps, FieldWrapperProps, PageWrapperProps, BaseFieldProps, TextFieldProps, NumberFieldProps, SelectFieldProps, ArrayFieldProps, ArrayHelpers, DisplayFieldProps } from "./types.js";
25
+ import type {
26
+ ComponentMap,
27
+ LayoutProps,
28
+ FieldWrapperProps,
29
+ PageWrapperProps,
30
+ BaseFieldProps,
31
+ TextFieldProps,
32
+ NumberFieldProps,
33
+ SelectFieldProps,
34
+ ArrayFieldProps,
35
+ ArrayHelpers,
36
+ DisplayFieldProps,
37
+ } from "./types.js";
14
38
 
15
39
  /**
16
40
  * Props for FormRenderer component
@@ -23,7 +47,10 @@ export interface FormRendererProps {
23
47
  /** Submit handler */
24
48
  onSubmit?: (data: Record<string, unknown>) => void | Promise<void>;
25
49
  /** Change handler */
26
- onChange?: (data: Record<string, unknown>, computed?: Record<string, unknown>) => void;
50
+ onChange?: (
51
+ data: Record<string, unknown>,
52
+ computed?: Record<string, unknown>,
53
+ ) => void;
27
54
  /** Component map for rendering fields */
28
55
  components: ComponentMap;
29
56
  /** Custom layout component */
@@ -75,11 +102,20 @@ function DefaultLayout({ children, onSubmit, isSubmitting }: LayoutProps) {
75
102
  /**
76
103
  * Default field wrapper component with accessibility support
77
104
  */
78
- function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }: FieldWrapperProps) {
105
+ function DefaultFieldWrapper({
106
+ fieldPath,
107
+ field,
108
+ children,
109
+ errors,
110
+ showRequiredIndicator,
111
+ visible,
112
+ }: FieldWrapperProps) {
79
113
  if (!visible) return null;
80
114
 
81
115
  const errorId = `${fieldPath}-error`;
82
- const descriptionId = field.description ? `${fieldPath}-description` : undefined;
116
+ const descriptionId = field.description
117
+ ? `${fieldPath}-description`
118
+ : undefined;
83
119
  const hasErrors = errors.length > 0;
84
120
 
85
121
  return (
@@ -87,8 +123,14 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredI
87
123
  {field.label && (
88
124
  <label htmlFor={fieldPath}>
89
125
  {field.label}
90
- {showRequiredIndicator && <span className="required" aria-hidden="true">*</span>}
91
- {showRequiredIndicator && <span className="sr-only"> (required)</span>}
126
+ {showRequiredIndicator && (
127
+ <span className="required" aria-hidden="true">
128
+ *
129
+ </span>
130
+ )}
131
+ {showRequiredIndicator && (
132
+ <span className="sr-only"> (required)</span>
133
+ )}
92
134
  </label>
93
135
  )}
94
136
  {children}
@@ -118,7 +160,11 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredI
118
160
  /**
119
161
  * Default page wrapper component
120
162
  */
121
- function DefaultPageWrapper({ title, description, children }: PageWrapperProps) {
163
+ function DefaultPageWrapper({
164
+ title,
165
+ description,
166
+ children,
167
+ }: PageWrapperProps) {
122
168
  return (
123
169
  <div className="page-wrapper">
124
170
  <h2>{title}</h2>
@@ -131,13 +177,23 @@ function DefaultPageWrapper({ title, description, children }: PageWrapperProps)
131
177
  /**
132
178
  * Extract numeric constraints from JSON Schema property
133
179
  */
134
- function getNumberConstraints(schema?: JSONSchemaProperty): { min?: number; max?: number; step?: number } {
180
+ function getNumberConstraints(schema?: JSONSchemaProperty): {
181
+ min?: number;
182
+ max?: number;
183
+ step?: number;
184
+ } {
135
185
  if (!schema) return {};
136
186
  if (schema.type !== "number" && schema.type !== "integer") return {};
137
187
 
138
188
  // Extract min/max from schema
139
- const min = "minimum" in schema && typeof schema.minimum === "number" ? schema.minimum : undefined;
140
- const max = "maximum" in schema && typeof schema.maximum === "number" ? schema.maximum : undefined;
189
+ const min =
190
+ "minimum" in schema && typeof schema.minimum === "number"
191
+ ? schema.minimum
192
+ : undefined;
193
+ const max =
194
+ "maximum" in schema && typeof schema.maximum === "number"
195
+ ? schema.maximum
196
+ : undefined;
141
197
 
142
198
  // Use multipleOf for step if defined, otherwise default to 1 for integers
143
199
  let step: number | undefined;
@@ -153,7 +209,9 @@ function getNumberConstraints(schema?: JSONSchemaProperty): { min?: number; max?
153
209
  /**
154
210
  * Create a default item for an array field based on item field definitions
155
211
  */
156
- function createDefaultItem(itemFields: Record<string, FieldDefinition>): Record<string, unknown> {
212
+ function createDefaultItem(
213
+ itemFields: Record<string, FieldDefinition>,
214
+ ): Record<string, unknown> {
157
215
  const item: Record<string, unknown> = {};
158
216
  for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
159
217
  if (fieldDef.type === "boolean") {
@@ -222,9 +280,27 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
222
280
  isValid: forma.isValid,
223
281
  isDirty: forma.isDirty,
224
282
  }),
225
- [forma, focusField, focusFirstError]
283
+ [forma, focusField, focusFirstError],
226
284
  );
227
285
 
286
+ // Destructure only the values renderField needs from forma.
287
+ // This prevents renderField from recreating when unrelated state changes
288
+ // (isSubmitting, isDirty, wizard page navigation, etc.)
289
+ const {
290
+ data: formaData,
291
+ computed: formaComputed,
292
+ visibility: formaVisibility,
293
+ required: formaRequired,
294
+ enabled: formaEnabled,
295
+ readonly: formaReadonly,
296
+ optionsVisibility: formaOptionsVisibility,
297
+ touched: formaTouched,
298
+ errors: formaErrors,
299
+ setFieldValue,
300
+ setFieldTouched,
301
+ getArrayHelpers,
302
+ } = forma;
303
+
228
304
  // Determine which fields to render based on pages or fieldOrder
229
305
  const fieldsToRender = useMemo(() => {
230
306
  if (spec.pages && spec.pages.length > 0 && forma.wizard) {
@@ -241,185 +317,248 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
241
317
  }, [spec.pages, spec.fieldOrder, forma.wizard]);
242
318
 
243
319
  // Render a single field (memoized)
244
- const renderField = useCallback((fieldPath: string) => {
245
- const fieldDef = spec.fields[fieldPath];
246
- if (!fieldDef) return null;
247
-
248
- const isVisible = forma.visibility[fieldPath] !== false;
249
- if (!isVisible) return null;
320
+ const renderField = useCallback(
321
+ (fieldPath: string) => {
322
+ const fieldDef = spec.fields[fieldPath];
323
+ if (!fieldDef) return null;
324
+
325
+ const isVisible = formaVisibility[fieldPath] !== false;
326
+ if (!isVisible) {
327
+ return <div key={fieldPath} data-field-path={fieldPath} hidden />;
328
+ }
250
329
 
251
- // Get field type (type is required on all field definitions)
252
- const fieldType = fieldDef.type;
253
- const componentKey = fieldType as keyof ComponentMap;
254
- const Component = components[componentKey] || components.fallback;
330
+ // Get field type (type is required on all field definitions)
331
+ const fieldType = fieldDef.type;
332
+ const componentKey = fieldType as keyof ComponentMap;
333
+ const Component = components[componentKey] || components.fallback;
255
334
 
256
- if (!Component) {
257
- console.warn(`No component found for field type: ${fieldType}`);
258
- return null;
259
- }
335
+ if (!Component) {
336
+ console.warn(`No component found for field type: ${fieldType}`);
337
+ return null;
338
+ }
260
339
 
261
- const errors = forma.errors.filter((e) => e.field === fieldPath);
262
- const touched = forma.touched[fieldPath] ?? false;
263
- const required = forma.required[fieldPath] ?? false;
264
- const disabled = forma.enabled[fieldPath] === false;
265
-
266
- // Get schema property for additional constraints
267
- const schemaProperty = spec.schema.properties[fieldPath];
268
-
269
- // Boolean fields: hide asterisk unless they have validation rules (consent pattern)
270
- // - Binary question ("Do you smoke?"): no validation → false is valid → hide asterisk
271
- // - Consent checkbox ("I accept terms"): has validation rule → show asterisk
272
- const isBooleanField = schemaProperty?.type === "boolean" || fieldDef?.type === "boolean";
273
- const hasValidationRules = (fieldDef?.validations?.length ?? 0) > 0;
274
- const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
275
-
276
- // Base field props
277
- const isReadonly = forma.readonly[fieldPath] ?? false;
278
- const baseProps: BaseFieldProps = {
279
- name: fieldPath,
280
- field: fieldDef,
281
- value: forma.data[fieldPath],
282
- touched,
283
- required,
284
- disabled,
285
- errors,
286
- onChange: (value: unknown) => forma.setFieldValue(fieldPath, value),
287
- onBlur: () => forma.setFieldTouched(fieldPath),
288
- // Convenience properties
289
- visible: true, // Always true since we already filtered for visibility
290
- enabled: !disabled,
291
- readonly: isReadonly,
292
- label: fieldDef.label ?? fieldPath,
293
- description: fieldDef.description,
294
- placeholder: fieldDef.placeholder,
295
- // Adorner properties (only for adornable field types)
296
- ...(isAdornableField(fieldDef) && {
297
- prefix: fieldDef.prefix,
298
- suffix: fieldDef.suffix,
299
- }),
300
- // Presentation variant
301
- variant: fieldDef.variant,
302
- variantConfig: fieldDef.variantConfig,
303
- };
304
-
305
- // Build type-specific props
306
- let fieldProps: BaseFieldProps | TextFieldProps | NumberFieldProps | SelectFieldProps | ArrayFieldProps | DisplayFieldProps = baseProps;
307
-
308
- if (fieldType === "number" || fieldType === "integer") {
309
- const constraints = getNumberConstraints(schemaProperty);
310
- fieldProps = {
311
- ...baseProps,
312
- fieldType,
313
- value: baseProps.value as number | null,
314
- onChange: baseProps.onChange as (value: number | null) => void,
315
- ...constraints,
316
- } as NumberFieldProps;
317
- } else if (fieldType === "select" || fieldType === "multiselect") {
318
- const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
319
- fieldProps = {
320
- ...baseProps,
321
- fieldType,
322
- value: baseProps.value as string | string[] | null,
323
- onChange: baseProps.onChange as (value: string | string[] | null) => void,
324
- options: forma.optionsVisibility[fieldPath] ?? selectOptions ?? [],
325
- } as SelectFieldProps;
326
- } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
327
- const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
328
- const minItems = fieldDef.minItems ?? 0;
329
- const maxItems = fieldDef.maxItems ?? Infinity;
330
- const itemFieldDefs = fieldDef.itemFields;
331
-
332
- // Get helpers from useForma - these are fresh on each render, avoiding stale closures
333
- const baseHelpers = forma.getArrayHelpers(fieldPath);
334
-
335
- // Wrap push to add default item creation when called without arguments
336
- const pushWithDefault = (item?: unknown): void => {
337
- const newItem = item ?? createDefaultItem(itemFieldDefs);
338
- baseHelpers.push(newItem);
340
+ const errors = formaErrors.filter((e) => e.field === fieldPath);
341
+ const touched = formaTouched[fieldPath] ?? false;
342
+ const required = formaRequired[fieldPath] ?? false;
343
+ const disabled = formaEnabled[fieldPath] === false;
344
+
345
+ // Get schema property for additional constraints
346
+ const schemaProperty = spec.schema.properties[fieldPath];
347
+
348
+ // Boolean fields: hide asterisk unless they have validation rules (consent pattern)
349
+ // - Binary question ("Do you smoke?"): no validation → false is valid → hide asterisk
350
+ // - Consent checkbox ("I accept terms"): has validation rule → show asterisk
351
+ const isBooleanField =
352
+ schemaProperty?.type === "boolean" || fieldDef?.type === "boolean";
353
+ const hasValidationRules = (fieldDef?.validations?.length ?? 0) > 0;
354
+ const showRequiredIndicator =
355
+ required && (!isBooleanField || hasValidationRules);
356
+
357
+ // Base field props
358
+ const isReadonly = formaReadonly[fieldPath] ?? false;
359
+ const baseProps: BaseFieldProps = {
360
+ name: fieldPath,
361
+ field: fieldDef,
362
+ value: formaData[fieldPath],
363
+ touched,
364
+ required,
365
+ disabled,
366
+ errors,
367
+ onChange: (value: unknown) => setFieldValue(fieldPath, value),
368
+ onBlur: () => setFieldTouched(fieldPath),
369
+ // Convenience properties
370
+ visible: true, // Always true since we already filtered for visibility
371
+ enabled: !disabled,
372
+ readonly: isReadonly,
373
+ label: fieldDef.label ?? fieldPath,
374
+ description: fieldDef.description,
375
+ placeholder: fieldDef.placeholder,
376
+ // Adorner properties (only for adornable field types)
377
+ ...(isAdornableField(fieldDef) && {
378
+ prefix: fieldDef.prefix,
379
+ suffix: fieldDef.suffix,
380
+ }),
381
+ // Presentation variant
382
+ variant: fieldDef.variant,
383
+ variantConfig: fieldDef.variantConfig,
339
384
  };
340
385
 
341
- // Extend getItemFieldProps to include additional metadata (itemIndex, fieldName, options)
342
- const getItemFieldPropsExtended = (index: number, fieldName: string) => {
343
- const baseProps = baseHelpers.getItemFieldProps(index, fieldName);
344
- const itemFieldDef = itemFieldDefs[fieldName];
345
- const itemPath = `${fieldPath}[${index}].${fieldName}`;
346
- return {
386
+ // Build type-specific props
387
+ let fieldProps:
388
+ | BaseFieldProps
389
+ | TextFieldProps
390
+ | NumberFieldProps
391
+ | SelectFieldProps
392
+ | ArrayFieldProps
393
+ | DisplayFieldProps = baseProps;
394
+
395
+ if (fieldType === "number" || fieldType === "integer") {
396
+ const constraints = getNumberConstraints(schemaProperty);
397
+ fieldProps = {
347
398
  ...baseProps,
348
- itemIndex: index,
349
- fieldName,
350
- options: (forma.optionsVisibility[itemPath] as SelectOption[] | undefined) ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : undefined),
399
+ fieldType,
400
+ value: baseProps.value as number | null,
401
+ onChange: baseProps.onChange as (value: number | null) => void,
402
+ ...constraints,
403
+ } as NumberFieldProps;
404
+ } else if (fieldType === "select" || fieldType === "multiselect") {
405
+ const selectOptions = isSelectionField(fieldDef)
406
+ ? fieldDef.options
407
+ : [];
408
+ fieldProps = {
409
+ ...baseProps,
410
+ fieldType,
411
+ value: baseProps.value as string | string[] | null,
412
+ onChange: baseProps.onChange as (
413
+ value: string | string[] | null,
414
+ ) => void,
415
+ options: formaOptionsVisibility[fieldPath] ?? selectOptions ?? [],
416
+ } as SelectFieldProps;
417
+ } else if (
418
+ fieldType === "array" &&
419
+ fieldDef.type === "array" &&
420
+ fieldDef.itemFields
421
+ ) {
422
+ const arrayValue = Array.isArray(baseProps.value)
423
+ ? baseProps.value
424
+ : [];
425
+ const minItems = fieldDef.minItems ?? 0;
426
+ const maxItems = fieldDef.maxItems ?? Infinity;
427
+ const itemFieldDefs = fieldDef.itemFields;
428
+
429
+ // Get helpers from useForma - these are fresh on each render, avoiding stale closures
430
+ const baseHelpers = getArrayHelpers(fieldPath);
431
+
432
+ // Wrap push to add default item creation when called without arguments
433
+ const pushWithDefault = (item?: unknown): void => {
434
+ const newItem = item ?? createDefaultItem(itemFieldDefs);
435
+ baseHelpers.push(newItem);
351
436
  };
352
- };
353
437
 
354
- const helpers: ArrayHelpers = {
355
- items: arrayValue,
356
- push: pushWithDefault,
357
- insert: baseHelpers.insert,
358
- remove: baseHelpers.remove,
359
- move: baseHelpers.move,
360
- swap: baseHelpers.swap,
361
- getItemFieldProps: getItemFieldPropsExtended,
362
- minItems,
363
- maxItems,
364
- canAdd: arrayValue.length < maxItems,
365
- canRemove: arrayValue.length > minItems,
366
- };
367
- fieldProps = {
368
- ...baseProps,
369
- fieldType: "array",
370
- value: arrayValue,
371
- onChange: baseProps.onChange as (value: unknown[]) => void,
372
- helpers,
373
- itemFields: itemFieldDefs,
374
- minItems,
375
- maxItems,
376
- } as ArrayFieldProps;
377
- } else if (fieldType === "display" && fieldDef.type === "display") {
378
- // Display fields (read-only presentation content)
379
- // Resolve source value if the display field has a source property
380
- const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : undefined;
381
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
382
- const { onChange: _onChange, value: _value, ...displayBaseProps } = baseProps;
383
- fieldProps = {
384
- ...displayBaseProps,
385
- fieldType: "display",
386
- content: fieldDef.content,
387
- sourceValue,
388
- format: fieldDef.format,
389
- } as DisplayFieldProps;
390
- } else {
391
- // Text-based fields
392
- fieldProps = {
393
- ...baseProps,
394
- fieldType: fieldType as "text" | "email" | "password" | "url" | "textarea",
395
- value: (baseProps.value as string) ?? "",
396
- onChange: baseProps.onChange as (value: string) => void,
397
- };
398
- }
438
+ // Extend getItemFieldProps to include additional metadata (itemIndex, fieldName, options)
439
+ const getItemFieldPropsExtended = (
440
+ index: number,
441
+ fieldName: string,
442
+ ) => {
443
+ const baseProps = baseHelpers.getItemFieldProps(index, fieldName);
444
+ const itemFieldDef = itemFieldDefs[fieldName];
445
+ const itemPath = `${fieldPath}[${index}].${fieldName}`;
446
+ return {
447
+ ...baseProps,
448
+ itemIndex: index,
449
+ fieldName,
450
+ options:
451
+ (formaOptionsVisibility[itemPath] as
452
+ | SelectOption[]
453
+ | undefined) ??
454
+ (itemFieldDef && isSelectionField(itemFieldDef)
455
+ ? itemFieldDef.options
456
+ : undefined),
457
+ };
458
+ };
399
459
 
400
- // Wrap props in { field, spec } structure for components
401
- const componentProps = { field: fieldProps, spec };
402
-
403
- return (
404
- <FieldWrapper
405
- key={fieldPath}
406
- fieldPath={fieldPath}
407
- field={fieldDef}
408
- errors={errors}
409
- touched={touched}
410
- required={required}
411
- showRequiredIndicator={showRequiredIndicator}
412
- visible={isVisible}
413
- >
414
- {React.createElement(Component as React.ComponentType<typeof componentProps>, componentProps)}
415
- </FieldWrapper>
416
- );
417
- }, [spec, forma, components, FieldWrapper]);
460
+ const helpers: ArrayHelpers = {
461
+ items: arrayValue,
462
+ push: pushWithDefault,
463
+ insert: baseHelpers.insert,
464
+ remove: baseHelpers.remove,
465
+ move: baseHelpers.move,
466
+ swap: baseHelpers.swap,
467
+ getItemFieldProps: getItemFieldPropsExtended,
468
+ minItems,
469
+ maxItems,
470
+ canAdd: arrayValue.length < maxItems,
471
+ canRemove: arrayValue.length > minItems,
472
+ };
473
+ fieldProps = {
474
+ ...baseProps,
475
+ fieldType: "array",
476
+ value: arrayValue,
477
+ onChange: baseProps.onChange as (value: unknown[]) => void,
478
+ helpers,
479
+ itemFields: itemFieldDefs,
480
+ minItems,
481
+ maxItems,
482
+ } as ArrayFieldProps;
483
+ } else if (fieldType === "display" && fieldDef.type === "display") {
484
+ // Display fields (read-only presentation content)
485
+ // Resolve source value if the display field has a source property
486
+ const sourceValue = fieldDef.source
487
+ ? (formaData[fieldDef.source] ?? formaComputed[fieldDef.source])
488
+ : undefined;
489
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
490
+ const {
491
+ onChange: _onChange,
492
+ value: _value,
493
+ ...displayBaseProps
494
+ } = baseProps;
495
+ fieldProps = {
496
+ ...displayBaseProps,
497
+ fieldType: "display",
498
+ content: fieldDef.content,
499
+ sourceValue,
500
+ format: fieldDef.format,
501
+ } as DisplayFieldProps;
502
+ } else {
503
+ // Text-based fields
504
+ fieldProps = {
505
+ ...baseProps,
506
+ fieldType: fieldType as
507
+ | "text"
508
+ | "email"
509
+ | "password"
510
+ | "url"
511
+ | "textarea",
512
+ value: (baseProps.value as string) ?? "",
513
+ onChange: baseProps.onChange as (value: string) => void,
514
+ };
515
+ }
516
+
517
+ // Wrap props in { field, spec } structure for components
518
+ const componentProps = { field: fieldProps, spec };
519
+
520
+ return (
521
+ <div key={fieldPath} data-field-path={fieldPath}>
522
+ <FieldWrapper
523
+ fieldPath={fieldPath}
524
+ field={fieldDef}
525
+ errors={errors}
526
+ touched={touched}
527
+ required={required}
528
+ showRequiredIndicator={showRequiredIndicator}
529
+ visible={isVisible}
530
+ >
531
+ {React.createElement(
532
+ Component as React.ComponentType<typeof componentProps>,
533
+ componentProps,
534
+ )}
535
+ </FieldWrapper>
536
+ </div>
537
+ );
538
+ },
539
+ [
540
+ spec,
541
+ components,
542
+ FieldWrapper,
543
+ formaData,
544
+ formaComputed,
545
+ formaVisibility,
546
+ formaRequired,
547
+ formaEnabled,
548
+ formaReadonly,
549
+ formaOptionsVisibility,
550
+ formaTouched,
551
+ formaErrors,
552
+ setFieldValue,
553
+ setFieldTouched,
554
+ getArrayHelpers,
555
+ ],
556
+ );
418
557
 
419
558
  // Render fields (memoized)
420
559
  const renderedFields = useMemo(
421
560
  () => fieldsToRender.map(renderField),
422
- [fieldsToRender, renderField]
561
+ [fieldsToRender, renderField],
423
562
  );
424
563
 
425
564
  // Render with page wrapper if using pages
@@ -454,5 +593,5 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
454
593
  </Layout>
455
594
  </FormaContext.Provider>
456
595
  );
457
- }
596
+ },
458
597
  );