@fogpipe/forma-react 0.11.2 → 0.12.0-alpha.2
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.
- package/dist/index.d.ts +45 -3
- package/dist/index.js +553 -323
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/src/FieldRenderer.tsx +107 -20
- package/src/FormRenderer.tsx +321 -157
- package/src/__tests__/FieldRenderer.test.tsx +136 -20
- package/src/__tests__/FormRenderer.test.tsx +264 -85
- package/src/index.ts +2 -0
- package/src/types.ts +44 -1
- package/src/useForma.ts +392 -235
package/src/FormRenderer.tsx
CHANGED
|
@@ -5,11 +5,36 @@
|
|
|
5
5
|
* Supports single-page and multi-page (wizard) forms.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, {
|
|
9
|
-
|
|
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";
|
|
22
|
+
import { isAdornableField, isSelectionField } from "@fogpipe/forma-core";
|
|
10
23
|
import { useForma } from "./useForma.js";
|
|
11
24
|
import { FormaContext } from "./context.js";
|
|
12
|
-
import type {
|
|
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";
|
|
13
38
|
|
|
14
39
|
/**
|
|
15
40
|
* Props for FormRenderer component
|
|
@@ -22,7 +47,10 @@ export interface FormRendererProps {
|
|
|
22
47
|
/** Submit handler */
|
|
23
48
|
onSubmit?: (data: Record<string, unknown>) => void | Promise<void>;
|
|
24
49
|
/** Change handler */
|
|
25
|
-
onChange?: (
|
|
50
|
+
onChange?: (
|
|
51
|
+
data: Record<string, unknown>,
|
|
52
|
+
computed?: Record<string, unknown>,
|
|
53
|
+
) => void;
|
|
26
54
|
/** Component map for rendering fields */
|
|
27
55
|
components: ComponentMap;
|
|
28
56
|
/** Custom layout component */
|
|
@@ -74,11 +102,20 @@ function DefaultLayout({ children, onSubmit, isSubmitting }: LayoutProps) {
|
|
|
74
102
|
/**
|
|
75
103
|
* Default field wrapper component with accessibility support
|
|
76
104
|
*/
|
|
77
|
-
function DefaultFieldWrapper({
|
|
105
|
+
function DefaultFieldWrapper({
|
|
106
|
+
fieldPath,
|
|
107
|
+
field,
|
|
108
|
+
children,
|
|
109
|
+
errors,
|
|
110
|
+
showRequiredIndicator,
|
|
111
|
+
visible,
|
|
112
|
+
}: FieldWrapperProps) {
|
|
78
113
|
if (!visible) return null;
|
|
79
114
|
|
|
80
115
|
const errorId = `${fieldPath}-error`;
|
|
81
|
-
const descriptionId = field.description
|
|
116
|
+
const descriptionId = field.description
|
|
117
|
+
? `${fieldPath}-description`
|
|
118
|
+
: undefined;
|
|
82
119
|
const hasErrors = errors.length > 0;
|
|
83
120
|
|
|
84
121
|
return (
|
|
@@ -86,8 +123,14 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredI
|
|
|
86
123
|
{field.label && (
|
|
87
124
|
<label htmlFor={fieldPath}>
|
|
88
125
|
{field.label}
|
|
89
|
-
{showRequiredIndicator &&
|
|
90
|
-
|
|
126
|
+
{showRequiredIndicator && (
|
|
127
|
+
<span className="required" aria-hidden="true">
|
|
128
|
+
*
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
{showRequiredIndicator && (
|
|
132
|
+
<span className="sr-only"> (required)</span>
|
|
133
|
+
)}
|
|
91
134
|
</label>
|
|
92
135
|
)}
|
|
93
136
|
{children}
|
|
@@ -117,7 +160,11 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredI
|
|
|
117
160
|
/**
|
|
118
161
|
* Default page wrapper component
|
|
119
162
|
*/
|
|
120
|
-
function DefaultPageWrapper({
|
|
163
|
+
function DefaultPageWrapper({
|
|
164
|
+
title,
|
|
165
|
+
description,
|
|
166
|
+
children,
|
|
167
|
+
}: PageWrapperProps) {
|
|
121
168
|
return (
|
|
122
169
|
<div className="page-wrapper">
|
|
123
170
|
<h2>{title}</h2>
|
|
@@ -130,13 +177,23 @@ function DefaultPageWrapper({ title, description, children }: PageWrapperProps)
|
|
|
130
177
|
/**
|
|
131
178
|
* Extract numeric constraints from JSON Schema property
|
|
132
179
|
*/
|
|
133
|
-
function getNumberConstraints(schema?: JSONSchemaProperty): {
|
|
180
|
+
function getNumberConstraints(schema?: JSONSchemaProperty): {
|
|
181
|
+
min?: number;
|
|
182
|
+
max?: number;
|
|
183
|
+
step?: number;
|
|
184
|
+
} {
|
|
134
185
|
if (!schema) return {};
|
|
135
186
|
if (schema.type !== "number" && schema.type !== "integer") return {};
|
|
136
187
|
|
|
137
188
|
// Extract min/max from schema
|
|
138
|
-
const min =
|
|
139
|
-
|
|
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;
|
|
140
197
|
|
|
141
198
|
// Use multipleOf for step if defined, otherwise default to 1 for integers
|
|
142
199
|
let step: number | undefined;
|
|
@@ -152,7 +209,9 @@ function getNumberConstraints(schema?: JSONSchemaProperty): { min?: number; max?
|
|
|
152
209
|
/**
|
|
153
210
|
* Create a default item for an array field based on item field definitions
|
|
154
211
|
*/
|
|
155
|
-
function createDefaultItem(
|
|
212
|
+
function createDefaultItem(
|
|
213
|
+
itemFields: Record<string, FieldDefinition>,
|
|
214
|
+
): Record<string, unknown> {
|
|
156
215
|
const item: Record<string, unknown> = {};
|
|
157
216
|
for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
|
|
158
217
|
if (fieldDef.type === "boolean") {
|
|
@@ -221,9 +280,27 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
221
280
|
isValid: forma.isValid,
|
|
222
281
|
isDirty: forma.isDirty,
|
|
223
282
|
}),
|
|
224
|
-
[forma, focusField, focusFirstError]
|
|
283
|
+
[forma, focusField, focusFirstError],
|
|
225
284
|
);
|
|
226
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
|
+
|
|
227
304
|
// Determine which fields to render based on pages or fieldOrder
|
|
228
305
|
const fieldsToRender = useMemo(() => {
|
|
229
306
|
if (spec.pages && spec.pages.length > 0 && forma.wizard) {
|
|
@@ -240,161 +317,248 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
240
317
|
}, [spec.pages, spec.fieldOrder, forma.wizard]);
|
|
241
318
|
|
|
242
319
|
// Render a single field (memoized)
|
|
243
|
-
const renderField = useCallback(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
}
|
|
249
329
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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;
|
|
254
334
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
335
|
+
if (!Component) {
|
|
336
|
+
console.warn(`No component found for field type: ${fieldType}`);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
259
339
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
...constraints,
|
|
305
|
-
} as NumberFieldProps;
|
|
306
|
-
} else if (fieldType === "select" || fieldType === "multiselect") {
|
|
307
|
-
fieldProps = {
|
|
308
|
-
...baseProps,
|
|
309
|
-
fieldType,
|
|
310
|
-
value: baseProps.value as string | string[] | null,
|
|
311
|
-
onChange: baseProps.onChange as (value: string | string[] | null) => void,
|
|
312
|
-
options: forma.optionsVisibility[fieldPath] ?? fieldDef.options ?? [],
|
|
313
|
-
} as SelectFieldProps;
|
|
314
|
-
} else if (fieldType === "array" && fieldDef.itemFields) {
|
|
315
|
-
const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
|
|
316
|
-
const minItems = fieldDef.minItems ?? 0;
|
|
317
|
-
const maxItems = fieldDef.maxItems ?? Infinity;
|
|
318
|
-
const itemFieldDefs = fieldDef.itemFields;
|
|
319
|
-
|
|
320
|
-
// Get helpers from useForma - these are fresh on each render, avoiding stale closures
|
|
321
|
-
const baseHelpers = forma.getArrayHelpers(fieldPath);
|
|
322
|
-
|
|
323
|
-
// Wrap push to add default item creation when called without arguments
|
|
324
|
-
const pushWithDefault = (item?: unknown): void => {
|
|
325
|
-
const newItem = item ?? createDefaultItem(itemFieldDefs);
|
|
326
|
-
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,
|
|
327
384
|
};
|
|
328
385
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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 = {
|
|
335
398
|
...baseProps,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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);
|
|
339
436
|
};
|
|
340
|
-
};
|
|
341
437
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
maxItems,
|
|
364
|
-
} as ArrayFieldProps;
|
|
365
|
-
} else {
|
|
366
|
-
// Text-based fields
|
|
367
|
-
fieldProps = {
|
|
368
|
-
...baseProps,
|
|
369
|
-
fieldType: fieldType as "text" | "email" | "password" | "url" | "textarea",
|
|
370
|
-
value: (baseProps.value as string) ?? "",
|
|
371
|
-
onChange: baseProps.onChange as (value: string) => void,
|
|
372
|
-
};
|
|
373
|
-
}
|
|
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
|
+
};
|
|
374
459
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
);
|
|
393
557
|
|
|
394
558
|
// Render fields (memoized)
|
|
395
559
|
const renderedFields = useMemo(
|
|
396
560
|
() => fieldsToRender.map(renderField),
|
|
397
|
-
[fieldsToRender, renderField]
|
|
561
|
+
[fieldsToRender, renderField],
|
|
398
562
|
);
|
|
399
563
|
|
|
400
564
|
// Render with page wrapper if using pages
|
|
@@ -429,5 +593,5 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
429
593
|
</Layout>
|
|
430
594
|
</FormaContext.Provider>
|
|
431
595
|
);
|
|
432
|
-
}
|
|
596
|
+
},
|
|
433
597
|
);
|