@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/dist/index.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
// src/useForma.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useReducer,
|
|
7
|
+
useRef,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
import { isAdornableField } from "@fogpipe/forma-core";
|
|
3
11
|
import {
|
|
4
12
|
getVisibility,
|
|
5
13
|
getRequired,
|
|
6
14
|
getEnabled,
|
|
15
|
+
getReadonly,
|
|
7
16
|
validate,
|
|
8
17
|
calculate,
|
|
9
18
|
getPageVisibility,
|
|
@@ -64,7 +73,15 @@ function getDefaultBooleanValues(spec) {
|
|
|
64
73
|
return defaults;
|
|
65
74
|
}
|
|
66
75
|
function useForma(options) {
|
|
67
|
-
const {
|
|
76
|
+
const {
|
|
77
|
+
spec: inputSpec,
|
|
78
|
+
initialData = {},
|
|
79
|
+
onSubmit,
|
|
80
|
+
onChange,
|
|
81
|
+
validateOn = "blur",
|
|
82
|
+
referenceData,
|
|
83
|
+
validationDebounceMs = 0
|
|
84
|
+
} = options;
|
|
68
85
|
const spec = useMemo(() => {
|
|
69
86
|
if (!referenceData) return inputSpec;
|
|
70
87
|
return {
|
|
@@ -103,6 +120,10 @@ function useForma(options) {
|
|
|
103
120
|
() => getEnabled(state.data, spec, { computed }),
|
|
104
121
|
[state.data, spec, computed]
|
|
105
122
|
);
|
|
123
|
+
const readonly = useMemo(
|
|
124
|
+
() => getReadonly(state.data, spec, { computed }),
|
|
125
|
+
[state.data, spec, computed]
|
|
126
|
+
);
|
|
106
127
|
const optionsVisibility = useMemo(
|
|
107
128
|
() => getOptionsVisibility(state.data, spec, { computed }),
|
|
108
129
|
[state.data, spec, computed]
|
|
@@ -130,33 +151,39 @@ function useForma(options) {
|
|
|
130
151
|
hasInitialized.current = true;
|
|
131
152
|
}
|
|
132
153
|
}, [state.data, computed, onChange]);
|
|
133
|
-
const setNestedValue = useCallback(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const buildNestedObject = (data, pathParts, val) => {
|
|
140
|
-
const result = { ...data };
|
|
141
|
-
let current = result;
|
|
142
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
143
|
-
const part = pathParts[i];
|
|
144
|
-
const nextPart = pathParts[i + 1];
|
|
145
|
-
const isNextArrayIndex = /^\d+$/.test(nextPart);
|
|
146
|
-
if (current[part] === void 0) {
|
|
147
|
-
current[part] = isNextArrayIndex ? [] : {};
|
|
148
|
-
} else if (Array.isArray(current[part])) {
|
|
149
|
-
current[part] = [...current[part]];
|
|
150
|
-
} else {
|
|
151
|
-
current[part] = { ...current[part] };
|
|
152
|
-
}
|
|
153
|
-
current = current[part];
|
|
154
|
+
const setNestedValue = useCallback(
|
|
155
|
+
(path, value) => {
|
|
156
|
+
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
157
|
+
if (parts.length === 1) {
|
|
158
|
+
dispatch({ type: "SET_FIELD_VALUE", field: path, value });
|
|
159
|
+
return;
|
|
154
160
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
const buildNestedObject = (data, pathParts, val) => {
|
|
162
|
+
const result = { ...data };
|
|
163
|
+
let current = result;
|
|
164
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
165
|
+
const part = pathParts[i];
|
|
166
|
+
const nextPart = pathParts[i + 1];
|
|
167
|
+
const isNextArrayIndex = /^\d+$/.test(nextPart);
|
|
168
|
+
if (current[part] === void 0) {
|
|
169
|
+
current[part] = isNextArrayIndex ? [] : {};
|
|
170
|
+
} else if (Array.isArray(current[part])) {
|
|
171
|
+
current[part] = [...current[part]];
|
|
172
|
+
} else {
|
|
173
|
+
current[part] = { ...current[part] };
|
|
174
|
+
}
|
|
175
|
+
current = current[part];
|
|
176
|
+
}
|
|
177
|
+
current[pathParts[pathParts.length - 1]] = val;
|
|
178
|
+
return result;
|
|
179
|
+
};
|
|
180
|
+
dispatch({
|
|
181
|
+
type: "SET_VALUES",
|
|
182
|
+
values: buildNestedObject(state.data, parts, value)
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
[state.data]
|
|
186
|
+
);
|
|
160
187
|
const setFieldValue = useCallback(
|
|
161
188
|
(path, value) => {
|
|
162
189
|
setNestedValue(path, value);
|
|
@@ -207,7 +234,10 @@ function useForma(options) {
|
|
|
207
234
|
}));
|
|
208
235
|
const visiblePages = pages.filter((p) => p.visible);
|
|
209
236
|
const maxPageIndex = Math.max(0, visiblePages.length - 1);
|
|
210
|
-
const clampedPageIndex = Math.min(
|
|
237
|
+
const clampedPageIndex = Math.min(
|
|
238
|
+
Math.max(0, state.currentPage),
|
|
239
|
+
maxPageIndex
|
|
240
|
+
);
|
|
211
241
|
if (clampedPageIndex !== state.currentPage && visiblePages.length > 0) {
|
|
212
242
|
dispatch({ type: "SET_PAGE", page: clampedPageIndex });
|
|
213
243
|
}
|
|
@@ -300,7 +330,7 @@ function useForma(options) {
|
|
|
300
330
|
const validFields = new Set(spec.fieldOrder);
|
|
301
331
|
for (const fieldId of spec.fieldOrder) {
|
|
302
332
|
const fieldDef = spec.fields[fieldId];
|
|
303
|
-
if (fieldDef == null ? void 0 : fieldDef.itemFields) {
|
|
333
|
+
if ((fieldDef == null ? void 0 : fieldDef.type) === "array" && fieldDef.itemFields) {
|
|
304
334
|
for (const key of fieldHandlers.current.keys()) {
|
|
305
335
|
if (key.startsWith(`${fieldId}[`)) {
|
|
306
336
|
validFields.add(key);
|
|
@@ -315,183 +345,272 @@ function useForma(options) {
|
|
|
315
345
|
}
|
|
316
346
|
}
|
|
317
347
|
}, [spec]);
|
|
318
|
-
const getFieldHandlers = useCallback(
|
|
319
|
-
|
|
320
|
-
fieldHandlers.current.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return fieldHandlers.current.get(path);
|
|
326
|
-
}, [setValueAtPath, setFieldTouched]);
|
|
327
|
-
const getFieldProps = useCallback((path) => {
|
|
328
|
-
var _a;
|
|
329
|
-
const fieldDef = spec.fields[path];
|
|
330
|
-
const handlers = getFieldHandlers(path);
|
|
331
|
-
let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
|
|
332
|
-
if (!fieldType || fieldType === "computed") {
|
|
333
|
-
const schemaProperty2 = spec.schema.properties[path];
|
|
334
|
-
if (schemaProperty2) {
|
|
335
|
-
if (schemaProperty2.type === "number") fieldType = "number";
|
|
336
|
-
else if (schemaProperty2.type === "integer") fieldType = "integer";
|
|
337
|
-
else if (schemaProperty2.type === "boolean") fieldType = "boolean";
|
|
338
|
-
else if (schemaProperty2.type === "array") fieldType = "array";
|
|
339
|
-
else if (schemaProperty2.type === "object") fieldType = "object";
|
|
340
|
-
else if ("enum" in schemaProperty2 && schemaProperty2.enum) fieldType = "select";
|
|
341
|
-
else if ("format" in schemaProperty2) {
|
|
342
|
-
if (schemaProperty2.format === "date") fieldType = "date";
|
|
343
|
-
else if (schemaProperty2.format === "date-time") fieldType = "datetime";
|
|
344
|
-
else if (schemaProperty2.format === "email") fieldType = "email";
|
|
345
|
-
else if (schemaProperty2.format === "uri") fieldType = "url";
|
|
346
|
-
}
|
|
348
|
+
const getFieldHandlers = useCallback(
|
|
349
|
+
(path) => {
|
|
350
|
+
if (!fieldHandlers.current.has(path)) {
|
|
351
|
+
fieldHandlers.current.set(path, {
|
|
352
|
+
onChange: (value) => setValueAtPath(path, value),
|
|
353
|
+
onBlur: () => setFieldTouched(path)
|
|
354
|
+
});
|
|
347
355
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const isRequired = required[path] ?? false;
|
|
355
|
-
const schemaProperty = spec.schema.properties[path];
|
|
356
|
-
const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
|
|
357
|
-
const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
358
|
-
const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
|
|
359
|
-
return {
|
|
360
|
-
name: path,
|
|
361
|
-
value: getValueAtPath(path),
|
|
362
|
-
type: fieldType,
|
|
363
|
-
label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
|
|
364
|
-
description: fieldDef == null ? void 0 : fieldDef.description,
|
|
365
|
-
placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
|
|
366
|
-
visible: visibility[path] !== false,
|
|
367
|
-
enabled: enabled[path] !== false,
|
|
368
|
-
required: isRequired,
|
|
369
|
-
showRequiredIndicator,
|
|
370
|
-
touched: isTouched,
|
|
371
|
-
errors: displayedErrors,
|
|
372
|
-
onChange: handlers.onChange,
|
|
373
|
-
onBlur: handlers.onBlur,
|
|
374
|
-
// ARIA accessibility attributes
|
|
375
|
-
"aria-invalid": hasErrors || void 0,
|
|
376
|
-
"aria-describedby": hasErrors ? `${path}-error` : void 0,
|
|
377
|
-
"aria-required": isRequired || void 0
|
|
378
|
-
};
|
|
379
|
-
}, [spec, state.touched, state.isSubmitted, visibility, enabled, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);
|
|
380
|
-
const getSelectFieldProps = useCallback((path) => {
|
|
381
|
-
const baseProps = getFieldProps(path);
|
|
382
|
-
const visibleOptions = optionsVisibility[path] ?? [];
|
|
383
|
-
return {
|
|
384
|
-
...baseProps,
|
|
385
|
-
options: visibleOptions
|
|
386
|
-
};
|
|
387
|
-
}, [getFieldProps, optionsVisibility]);
|
|
388
|
-
const getArrayHelpers = useCallback((path) => {
|
|
389
|
-
const fieldDef = spec.fields[path];
|
|
390
|
-
const currentValue = getValueAtPath(path) ?? [];
|
|
391
|
-
const minItems = (fieldDef == null ? void 0 : fieldDef.minItems) ?? 0;
|
|
392
|
-
const maxItems = (fieldDef == null ? void 0 : fieldDef.maxItems) ?? Infinity;
|
|
393
|
-
const canAdd = currentValue.length < maxItems;
|
|
394
|
-
const canRemove = currentValue.length > minItems;
|
|
395
|
-
const getItemFieldProps = (index, fieldName) => {
|
|
356
|
+
return fieldHandlers.current.get(path);
|
|
357
|
+
},
|
|
358
|
+
[setValueAtPath, setFieldTouched]
|
|
359
|
+
);
|
|
360
|
+
const getFieldProps = useCallback(
|
|
361
|
+
(path) => {
|
|
396
362
|
var _a;
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
363
|
+
const fieldDef = spec.fields[path];
|
|
364
|
+
const handlers = getFieldHandlers(path);
|
|
365
|
+
let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
|
|
366
|
+
if (!fieldType || fieldType === "computed") {
|
|
367
|
+
const schemaProperty2 = spec.schema.properties[path];
|
|
368
|
+
if (schemaProperty2) {
|
|
369
|
+
if (schemaProperty2.type === "number") fieldType = "number";
|
|
370
|
+
else if (schemaProperty2.type === "integer") fieldType = "integer";
|
|
371
|
+
else if (schemaProperty2.type === "boolean") fieldType = "boolean";
|
|
372
|
+
else if (schemaProperty2.type === "array") fieldType = "array";
|
|
373
|
+
else if (schemaProperty2.type === "object") fieldType = "object";
|
|
374
|
+
else if ("enum" in schemaProperty2 && schemaProperty2.enum)
|
|
375
|
+
fieldType = "select";
|
|
376
|
+
else if ("format" in schemaProperty2) {
|
|
377
|
+
if (schemaProperty2.format === "date") fieldType = "date";
|
|
378
|
+
else if (schemaProperty2.format === "date-time")
|
|
379
|
+
fieldType = "datetime";
|
|
380
|
+
else if (schemaProperty2.format === "email") fieldType = "email";
|
|
381
|
+
else if (schemaProperty2.format === "uri") fieldType = "url";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const fieldErrors = validation.errors.filter((e) => e.field === path);
|
|
386
|
+
const isTouched = state.touched[path] ?? false;
|
|
404
387
|
const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
|
|
405
|
-
const
|
|
388
|
+
const displayedErrors = showErrors ? fieldErrors : [];
|
|
389
|
+
const hasErrors = displayedErrors.length > 0;
|
|
390
|
+
const isRequired = required[path] ?? false;
|
|
391
|
+
const schemaProperty = spec.schema.properties[path];
|
|
392
|
+
const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
|
|
393
|
+
const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
394
|
+
const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
|
|
395
|
+
const adornerProps = fieldDef && isAdornableField(fieldDef) ? { prefix: fieldDef.prefix, suffix: fieldDef.suffix } : {};
|
|
406
396
|
return {
|
|
407
|
-
name:
|
|
408
|
-
value:
|
|
409
|
-
type:
|
|
410
|
-
label: (
|
|
411
|
-
description:
|
|
412
|
-
placeholder:
|
|
413
|
-
visible:
|
|
397
|
+
name: path,
|
|
398
|
+
value: getValueAtPath(path),
|
|
399
|
+
type: fieldType,
|
|
400
|
+
label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
|
|
401
|
+
description: fieldDef == null ? void 0 : fieldDef.description,
|
|
402
|
+
placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
|
|
403
|
+
visible: visibility[path] !== false,
|
|
414
404
|
enabled: enabled[path] !== false,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
showRequiredIndicator
|
|
418
|
-
// Item fields don't show required indicator
|
|
405
|
+
readonly: readonly[path] ?? false,
|
|
406
|
+
required: isRequired,
|
|
407
|
+
showRequiredIndicator,
|
|
419
408
|
touched: isTouched,
|
|
420
|
-
errors:
|
|
409
|
+
errors: displayedErrors,
|
|
421
410
|
onChange: handlers.onChange,
|
|
422
411
|
onBlur: handlers.onBlur,
|
|
412
|
+
// ARIA accessibility attributes
|
|
413
|
+
"aria-invalid": hasErrors || void 0,
|
|
414
|
+
"aria-describedby": hasErrors ? `${path}-error` : void 0,
|
|
415
|
+
"aria-required": isRequired || void 0,
|
|
416
|
+
// Adorner props (only for adornable field types)
|
|
417
|
+
...adornerProps,
|
|
418
|
+
// Presentation variant
|
|
419
|
+
variant: fieldDef == null ? void 0 : fieldDef.variant,
|
|
420
|
+
variantConfig: fieldDef == null ? void 0 : fieldDef.variantConfig
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
[
|
|
424
|
+
spec,
|
|
425
|
+
state.touched,
|
|
426
|
+
state.isSubmitted,
|
|
427
|
+
visibility,
|
|
428
|
+
enabled,
|
|
429
|
+
readonly,
|
|
430
|
+
required,
|
|
431
|
+
validation.errors,
|
|
432
|
+
validateOn,
|
|
433
|
+
getValueAtPath,
|
|
434
|
+
getFieldHandlers
|
|
435
|
+
]
|
|
436
|
+
);
|
|
437
|
+
const getSelectFieldProps = useCallback(
|
|
438
|
+
(path) => {
|
|
439
|
+
const baseProps = getFieldProps(path);
|
|
440
|
+
const visibleOptions = optionsVisibility[path] ?? [];
|
|
441
|
+
return {
|
|
442
|
+
...baseProps,
|
|
423
443
|
options: visibleOptions
|
|
424
444
|
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
445
|
+
},
|
|
446
|
+
[getFieldProps, optionsVisibility]
|
|
447
|
+
);
|
|
448
|
+
const getArrayHelpers = useCallback(
|
|
449
|
+
(path) => {
|
|
450
|
+
const fieldDef = spec.fields[path];
|
|
451
|
+
const currentValue = getValueAtPath(path) ?? [];
|
|
452
|
+
const arrayDef = (fieldDef == null ? void 0 : fieldDef.type) === "array" ? fieldDef : void 0;
|
|
453
|
+
const minItems = (arrayDef == null ? void 0 : arrayDef.minItems) ?? 0;
|
|
454
|
+
const maxItems = (arrayDef == null ? void 0 : arrayDef.maxItems) ?? Infinity;
|
|
455
|
+
const canAdd = currentValue.length < maxItems;
|
|
456
|
+
const canRemove = currentValue.length > minItems;
|
|
457
|
+
const getItemFieldProps = (index, fieldName) => {
|
|
458
|
+
var _a;
|
|
459
|
+
const itemPath = `${path}[${index}].${fieldName}`;
|
|
460
|
+
const itemFieldDef = (_a = arrayDef == null ? void 0 : arrayDef.itemFields) == null ? void 0 : _a[fieldName];
|
|
461
|
+
const handlers = getFieldHandlers(itemPath);
|
|
462
|
+
const item = currentValue[index] ?? {};
|
|
463
|
+
const itemValue = item[fieldName];
|
|
464
|
+
const fieldErrors = validation.errors.filter(
|
|
465
|
+
(e) => e.field === itemPath
|
|
466
|
+
);
|
|
467
|
+
const isTouched = state.touched[itemPath] ?? false;
|
|
468
|
+
const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
|
|
469
|
+
const visibleOptions = optionsVisibility[itemPath];
|
|
470
|
+
return {
|
|
471
|
+
name: itemPath,
|
|
472
|
+
value: itemValue,
|
|
473
|
+
type: (itemFieldDef == null ? void 0 : itemFieldDef.type) || "text",
|
|
474
|
+
label: (itemFieldDef == null ? void 0 : itemFieldDef.label) || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
|
|
475
|
+
description: itemFieldDef == null ? void 0 : itemFieldDef.description,
|
|
476
|
+
placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
|
|
477
|
+
visible: true,
|
|
478
|
+
enabled: enabled[path] !== false,
|
|
479
|
+
readonly: readonly[itemPath] ?? false,
|
|
480
|
+
required: false,
|
|
481
|
+
// TODO: Evaluate item field required
|
|
482
|
+
showRequiredIndicator: false,
|
|
483
|
+
// Item fields don't show required indicator
|
|
484
|
+
touched: isTouched,
|
|
485
|
+
errors: showErrors ? fieldErrors : [],
|
|
486
|
+
onChange: handlers.onChange,
|
|
487
|
+
onBlur: handlers.onBlur,
|
|
488
|
+
options: visibleOptions
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
return {
|
|
492
|
+
items: currentValue,
|
|
493
|
+
push: (item) => {
|
|
494
|
+
if (canAdd) {
|
|
495
|
+
setValueAtPath(path, [...currentValue, item]);
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
remove: (index) => {
|
|
499
|
+
if (canRemove) {
|
|
500
|
+
const newArray = [...currentValue];
|
|
501
|
+
newArray.splice(index, 1);
|
|
502
|
+
setValueAtPath(path, newArray);
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
move: (from, to) => {
|
|
435
506
|
const newArray = [...currentValue];
|
|
436
|
-
newArray.splice(
|
|
507
|
+
const [item] = newArray.splice(from, 1);
|
|
508
|
+
newArray.splice(to, 0, item);
|
|
437
509
|
setValueAtPath(path, newArray);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
move: (from, to) => {
|
|
441
|
-
const newArray = [...currentValue];
|
|
442
|
-
const [item] = newArray.splice(from, 1);
|
|
443
|
-
newArray.splice(to, 0, item);
|
|
444
|
-
setValueAtPath(path, newArray);
|
|
445
|
-
},
|
|
446
|
-
swap: (indexA, indexB) => {
|
|
447
|
-
const newArray = [...currentValue];
|
|
448
|
-
[newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
|
|
449
|
-
setValueAtPath(path, newArray);
|
|
450
|
-
},
|
|
451
|
-
insert: (index, item) => {
|
|
452
|
-
if (canAdd) {
|
|
510
|
+
},
|
|
511
|
+
swap: (indexA, indexB) => {
|
|
453
512
|
const newArray = [...currentValue];
|
|
454
|
-
newArray
|
|
513
|
+
[newArray[indexA], newArray[indexB]] = [
|
|
514
|
+
newArray[indexB],
|
|
515
|
+
newArray[indexA]
|
|
516
|
+
];
|
|
455
517
|
setValueAtPath(path, newArray);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
518
|
+
},
|
|
519
|
+
insert: (index, item) => {
|
|
520
|
+
if (canAdd) {
|
|
521
|
+
const newArray = [...currentValue];
|
|
522
|
+
newArray.splice(index, 0, item);
|
|
523
|
+
setValueAtPath(path, newArray);
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
getItemFieldProps,
|
|
527
|
+
minItems,
|
|
528
|
+
maxItems,
|
|
529
|
+
canAdd,
|
|
530
|
+
canRemove
|
|
531
|
+
};
|
|
532
|
+
},
|
|
533
|
+
[
|
|
534
|
+
spec.fields,
|
|
535
|
+
getValueAtPath,
|
|
536
|
+
setValueAtPath,
|
|
537
|
+
getFieldHandlers,
|
|
538
|
+
enabled,
|
|
539
|
+
readonly,
|
|
540
|
+
state.touched,
|
|
541
|
+
state.isSubmitted,
|
|
542
|
+
validation.errors,
|
|
543
|
+
validateOn,
|
|
544
|
+
optionsVisibility
|
|
545
|
+
]
|
|
546
|
+
);
|
|
547
|
+
return useMemo(
|
|
548
|
+
() => ({
|
|
549
|
+
data: state.data,
|
|
550
|
+
computed,
|
|
551
|
+
visibility,
|
|
552
|
+
required,
|
|
553
|
+
enabled,
|
|
554
|
+
readonly,
|
|
555
|
+
optionsVisibility,
|
|
556
|
+
touched: state.touched,
|
|
557
|
+
errors: validation.errors,
|
|
558
|
+
isValid: validation.valid,
|
|
559
|
+
isSubmitting: state.isSubmitting,
|
|
560
|
+
isSubmitted: state.isSubmitted,
|
|
561
|
+
isDirty: state.isDirty,
|
|
562
|
+
spec,
|
|
563
|
+
wizard,
|
|
564
|
+
setFieldValue,
|
|
565
|
+
setFieldTouched,
|
|
566
|
+
setValues,
|
|
567
|
+
validateField,
|
|
568
|
+
validateForm,
|
|
569
|
+
submitForm,
|
|
570
|
+
resetForm,
|
|
571
|
+
getFieldProps,
|
|
572
|
+
getSelectFieldProps,
|
|
573
|
+
getArrayHelpers
|
|
574
|
+
}),
|
|
575
|
+
[
|
|
576
|
+
state.data,
|
|
577
|
+
state.touched,
|
|
578
|
+
state.isSubmitting,
|
|
579
|
+
state.isSubmitted,
|
|
580
|
+
state.isDirty,
|
|
581
|
+
computed,
|
|
582
|
+
visibility,
|
|
583
|
+
required,
|
|
584
|
+
enabled,
|
|
585
|
+
readonly,
|
|
586
|
+
optionsVisibility,
|
|
587
|
+
validation.errors,
|
|
588
|
+
validation.valid,
|
|
589
|
+
spec,
|
|
590
|
+
wizard,
|
|
591
|
+
setFieldValue,
|
|
592
|
+
setFieldTouched,
|
|
593
|
+
setValues,
|
|
594
|
+
validateField,
|
|
595
|
+
validateForm,
|
|
596
|
+
submitForm,
|
|
597
|
+
resetForm,
|
|
598
|
+
getFieldProps,
|
|
599
|
+
getSelectFieldProps,
|
|
600
|
+
getArrayHelpers
|
|
601
|
+
]
|
|
602
|
+
);
|
|
491
603
|
}
|
|
492
604
|
|
|
493
605
|
// src/FormRenderer.tsx
|
|
494
|
-
import React, {
|
|
606
|
+
import React, {
|
|
607
|
+
forwardRef,
|
|
608
|
+
useImperativeHandle,
|
|
609
|
+
useRef as useRef2,
|
|
610
|
+
useMemo as useMemo2,
|
|
611
|
+
useCallback as useCallback2
|
|
612
|
+
} from "react";
|
|
613
|
+
import { isAdornableField as isAdornableField2, isSelectionField } from "@fogpipe/forma-core";
|
|
495
614
|
|
|
496
615
|
// src/context.ts
|
|
497
616
|
import { createContext, useContext } from "react";
|
|
@@ -521,7 +640,14 @@ function DefaultLayout({ children, onSubmit, isSubmitting }) {
|
|
|
521
640
|
}
|
|
522
641
|
);
|
|
523
642
|
}
|
|
524
|
-
function DefaultFieldWrapper({
|
|
643
|
+
function DefaultFieldWrapper({
|
|
644
|
+
fieldPath,
|
|
645
|
+
field,
|
|
646
|
+
children,
|
|
647
|
+
errors,
|
|
648
|
+
showRequiredIndicator,
|
|
649
|
+
visible
|
|
650
|
+
}) {
|
|
525
651
|
if (!visible) return null;
|
|
526
652
|
const errorId = `${fieldPath}-error`;
|
|
527
653
|
const descriptionId = field.description ? `${fieldPath}-description` : void 0;
|
|
@@ -546,7 +672,11 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredI
|
|
|
546
672
|
field.description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "field-description", children: field.description })
|
|
547
673
|
] });
|
|
548
674
|
}
|
|
549
|
-
function DefaultPageWrapper({
|
|
675
|
+
function DefaultPageWrapper({
|
|
676
|
+
title,
|
|
677
|
+
description,
|
|
678
|
+
children
|
|
679
|
+
}) {
|
|
550
680
|
return /* @__PURE__ */ jsxs("div", { className: "page-wrapper", children: [
|
|
551
681
|
/* @__PURE__ */ jsx("h2", { children: title }),
|
|
552
682
|
description && /* @__PURE__ */ jsx("p", { children: description }),
|
|
@@ -625,6 +755,20 @@ var FormRenderer = forwardRef(
|
|
|
625
755
|
}),
|
|
626
756
|
[forma, focusField, focusFirstError]
|
|
627
757
|
);
|
|
758
|
+
const {
|
|
759
|
+
data: formaData,
|
|
760
|
+
computed: formaComputed,
|
|
761
|
+
visibility: formaVisibility,
|
|
762
|
+
required: formaRequired,
|
|
763
|
+
enabled: formaEnabled,
|
|
764
|
+
readonly: formaReadonly,
|
|
765
|
+
optionsVisibility: formaOptionsVisibility,
|
|
766
|
+
touched: formaTouched,
|
|
767
|
+
errors: formaErrors,
|
|
768
|
+
setFieldValue,
|
|
769
|
+
setFieldTouched,
|
|
770
|
+
getArrayHelpers
|
|
771
|
+
} = forma;
|
|
628
772
|
const fieldsToRender = useMemo2(() => {
|
|
629
773
|
var _a;
|
|
630
774
|
if (spec.pages && spec.pages.length > 0 && forma.wizard) {
|
|
@@ -636,131 +780,179 @@ var FormRenderer = forwardRef(
|
|
|
636
780
|
}
|
|
637
781
|
return spec.fieldOrder;
|
|
638
782
|
}, [spec.pages, spec.fieldOrder, forma.wizard]);
|
|
639
|
-
const renderField = useCallback2(
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
disabled,
|
|
667
|
-
errors,
|
|
668
|
-
onChange: (value) => forma.setFieldValue(fieldPath, value),
|
|
669
|
-
onBlur: () => forma.setFieldTouched(fieldPath),
|
|
670
|
-
// Convenience properties
|
|
671
|
-
visible: true,
|
|
672
|
-
// Always true since we already filtered for visibility
|
|
673
|
-
enabled: !disabled,
|
|
674
|
-
label: fieldDef.label ?? fieldPath,
|
|
675
|
-
description: fieldDef.description,
|
|
676
|
-
placeholder: fieldDef.placeholder
|
|
677
|
-
};
|
|
678
|
-
let fieldProps = baseProps;
|
|
679
|
-
if (fieldType === "number" || fieldType === "integer") {
|
|
680
|
-
const constraints = getNumberConstraints(schemaProperty);
|
|
681
|
-
fieldProps = {
|
|
682
|
-
...baseProps,
|
|
683
|
-
fieldType,
|
|
684
|
-
value: baseProps.value,
|
|
685
|
-
onChange: baseProps.onChange,
|
|
686
|
-
...constraints
|
|
687
|
-
};
|
|
688
|
-
} else if (fieldType === "select" || fieldType === "multiselect") {
|
|
689
|
-
fieldProps = {
|
|
690
|
-
...baseProps,
|
|
691
|
-
fieldType,
|
|
692
|
-
value: baseProps.value,
|
|
693
|
-
onChange: baseProps.onChange,
|
|
694
|
-
options: forma.optionsVisibility[fieldPath] ?? fieldDef.options ?? []
|
|
695
|
-
};
|
|
696
|
-
} else if (fieldType === "array" && fieldDef.itemFields) {
|
|
697
|
-
const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
|
|
698
|
-
const minItems = fieldDef.minItems ?? 0;
|
|
699
|
-
const maxItems = fieldDef.maxItems ?? Infinity;
|
|
700
|
-
const itemFieldDefs = fieldDef.itemFields;
|
|
701
|
-
const baseHelpers = forma.getArrayHelpers(fieldPath);
|
|
702
|
-
const pushWithDefault = (item) => {
|
|
703
|
-
const newItem = item ?? createDefaultItem(itemFieldDefs);
|
|
704
|
-
baseHelpers.push(newItem);
|
|
705
|
-
};
|
|
706
|
-
const getItemFieldPropsExtended = (index, fieldName) => {
|
|
707
|
-
const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
|
|
708
|
-
const itemFieldDef = itemFieldDefs[fieldName];
|
|
709
|
-
const itemPath = `${fieldPath}[${index}].${fieldName}`;
|
|
710
|
-
return {
|
|
711
|
-
...baseProps2,
|
|
712
|
-
itemIndex: index,
|
|
713
|
-
fieldName,
|
|
714
|
-
options: forma.optionsVisibility[itemPath] ?? (itemFieldDef == null ? void 0 : itemFieldDef.options)
|
|
715
|
-
};
|
|
716
|
-
};
|
|
717
|
-
const helpers = {
|
|
718
|
-
items: arrayValue,
|
|
719
|
-
push: pushWithDefault,
|
|
720
|
-
insert: baseHelpers.insert,
|
|
721
|
-
remove: baseHelpers.remove,
|
|
722
|
-
move: baseHelpers.move,
|
|
723
|
-
swap: baseHelpers.swap,
|
|
724
|
-
getItemFieldProps: getItemFieldPropsExtended,
|
|
725
|
-
minItems,
|
|
726
|
-
maxItems,
|
|
727
|
-
canAdd: arrayValue.length < maxItems,
|
|
728
|
-
canRemove: arrayValue.length > minItems
|
|
729
|
-
};
|
|
730
|
-
fieldProps = {
|
|
731
|
-
...baseProps,
|
|
732
|
-
fieldType: "array",
|
|
733
|
-
value: arrayValue,
|
|
734
|
-
onChange: baseProps.onChange,
|
|
735
|
-
helpers,
|
|
736
|
-
itemFields: itemFieldDefs,
|
|
737
|
-
minItems,
|
|
738
|
-
maxItems
|
|
739
|
-
};
|
|
740
|
-
} else {
|
|
741
|
-
fieldProps = {
|
|
742
|
-
...baseProps,
|
|
743
|
-
fieldType,
|
|
744
|
-
value: baseProps.value ?? "",
|
|
745
|
-
onChange: baseProps.onChange
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
const componentProps = { field: fieldProps, spec };
|
|
749
|
-
return /* @__PURE__ */ jsx(
|
|
750
|
-
FieldWrapper,
|
|
751
|
-
{
|
|
752
|
-
fieldPath,
|
|
783
|
+
const renderField = useCallback2(
|
|
784
|
+
(fieldPath) => {
|
|
785
|
+
var _a;
|
|
786
|
+
const fieldDef = spec.fields[fieldPath];
|
|
787
|
+
if (!fieldDef) return null;
|
|
788
|
+
const isVisible = formaVisibility[fieldPath] !== false;
|
|
789
|
+
if (!isVisible) {
|
|
790
|
+
return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, hidden: true }, fieldPath);
|
|
791
|
+
}
|
|
792
|
+
const fieldType = fieldDef.type;
|
|
793
|
+
const componentKey = fieldType;
|
|
794
|
+
const Component = components[componentKey] || components.fallback;
|
|
795
|
+
if (!Component) {
|
|
796
|
+
console.warn(`No component found for field type: ${fieldType}`);
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
const errors = formaErrors.filter((e) => e.field === fieldPath);
|
|
800
|
+
const touched = formaTouched[fieldPath] ?? false;
|
|
801
|
+
const required = formaRequired[fieldPath] ?? false;
|
|
802
|
+
const disabled = formaEnabled[fieldPath] === false;
|
|
803
|
+
const schemaProperty = spec.schema.properties[fieldPath];
|
|
804
|
+
const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
|
|
805
|
+
const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
806
|
+
const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
|
|
807
|
+
const isReadonly = formaReadonly[fieldPath] ?? false;
|
|
808
|
+
const baseProps = {
|
|
809
|
+
name: fieldPath,
|
|
753
810
|
field: fieldDef,
|
|
754
|
-
|
|
811
|
+
value: formaData[fieldPath],
|
|
755
812
|
touched,
|
|
756
813
|
required,
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
814
|
+
disabled,
|
|
815
|
+
errors,
|
|
816
|
+
onChange: (value) => setFieldValue(fieldPath, value),
|
|
817
|
+
onBlur: () => setFieldTouched(fieldPath),
|
|
818
|
+
// Convenience properties
|
|
819
|
+
visible: true,
|
|
820
|
+
// Always true since we already filtered for visibility
|
|
821
|
+
enabled: !disabled,
|
|
822
|
+
readonly: isReadonly,
|
|
823
|
+
label: fieldDef.label ?? fieldPath,
|
|
824
|
+
description: fieldDef.description,
|
|
825
|
+
placeholder: fieldDef.placeholder,
|
|
826
|
+
// Adorner properties (only for adornable field types)
|
|
827
|
+
...isAdornableField2(fieldDef) && {
|
|
828
|
+
prefix: fieldDef.prefix,
|
|
829
|
+
suffix: fieldDef.suffix
|
|
830
|
+
},
|
|
831
|
+
// Presentation variant
|
|
832
|
+
variant: fieldDef.variant,
|
|
833
|
+
variantConfig: fieldDef.variantConfig
|
|
834
|
+
};
|
|
835
|
+
let fieldProps = baseProps;
|
|
836
|
+
if (fieldType === "number" || fieldType === "integer") {
|
|
837
|
+
const constraints = getNumberConstraints(schemaProperty);
|
|
838
|
+
fieldProps = {
|
|
839
|
+
...baseProps,
|
|
840
|
+
fieldType,
|
|
841
|
+
value: baseProps.value,
|
|
842
|
+
onChange: baseProps.onChange,
|
|
843
|
+
...constraints
|
|
844
|
+
};
|
|
845
|
+
} else if (fieldType === "select" || fieldType === "multiselect") {
|
|
846
|
+
const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
|
|
847
|
+
fieldProps = {
|
|
848
|
+
...baseProps,
|
|
849
|
+
fieldType,
|
|
850
|
+
value: baseProps.value,
|
|
851
|
+
onChange: baseProps.onChange,
|
|
852
|
+
options: formaOptionsVisibility[fieldPath] ?? selectOptions ?? []
|
|
853
|
+
};
|
|
854
|
+
} else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
|
|
855
|
+
const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
|
|
856
|
+
const minItems = fieldDef.minItems ?? 0;
|
|
857
|
+
const maxItems = fieldDef.maxItems ?? Infinity;
|
|
858
|
+
const itemFieldDefs = fieldDef.itemFields;
|
|
859
|
+
const baseHelpers = getArrayHelpers(fieldPath);
|
|
860
|
+
const pushWithDefault = (item) => {
|
|
861
|
+
const newItem = item ?? createDefaultItem(itemFieldDefs);
|
|
862
|
+
baseHelpers.push(newItem);
|
|
863
|
+
};
|
|
864
|
+
const getItemFieldPropsExtended = (index, fieldName) => {
|
|
865
|
+
const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
|
|
866
|
+
const itemFieldDef = itemFieldDefs[fieldName];
|
|
867
|
+
const itemPath = `${fieldPath}[${index}].${fieldName}`;
|
|
868
|
+
return {
|
|
869
|
+
...baseProps2,
|
|
870
|
+
itemIndex: index,
|
|
871
|
+
fieldName,
|
|
872
|
+
options: formaOptionsVisibility[itemPath] ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : void 0)
|
|
873
|
+
};
|
|
874
|
+
};
|
|
875
|
+
const helpers = {
|
|
876
|
+
items: arrayValue,
|
|
877
|
+
push: pushWithDefault,
|
|
878
|
+
insert: baseHelpers.insert,
|
|
879
|
+
remove: baseHelpers.remove,
|
|
880
|
+
move: baseHelpers.move,
|
|
881
|
+
swap: baseHelpers.swap,
|
|
882
|
+
getItemFieldProps: getItemFieldPropsExtended,
|
|
883
|
+
minItems,
|
|
884
|
+
maxItems,
|
|
885
|
+
canAdd: arrayValue.length < maxItems,
|
|
886
|
+
canRemove: arrayValue.length > minItems
|
|
887
|
+
};
|
|
888
|
+
fieldProps = {
|
|
889
|
+
...baseProps,
|
|
890
|
+
fieldType: "array",
|
|
891
|
+
value: arrayValue,
|
|
892
|
+
onChange: baseProps.onChange,
|
|
893
|
+
helpers,
|
|
894
|
+
itemFields: itemFieldDefs,
|
|
895
|
+
minItems,
|
|
896
|
+
maxItems
|
|
897
|
+
};
|
|
898
|
+
} else if (fieldType === "display" && fieldDef.type === "display") {
|
|
899
|
+
const sourceValue = fieldDef.source ? formaData[fieldDef.source] ?? formaComputed[fieldDef.source] : void 0;
|
|
900
|
+
const {
|
|
901
|
+
onChange: _onChange,
|
|
902
|
+
value: _value,
|
|
903
|
+
...displayBaseProps
|
|
904
|
+
} = baseProps;
|
|
905
|
+
fieldProps = {
|
|
906
|
+
...displayBaseProps,
|
|
907
|
+
fieldType: "display",
|
|
908
|
+
content: fieldDef.content,
|
|
909
|
+
sourceValue,
|
|
910
|
+
format: fieldDef.format
|
|
911
|
+
};
|
|
912
|
+
} else {
|
|
913
|
+
fieldProps = {
|
|
914
|
+
...baseProps,
|
|
915
|
+
fieldType,
|
|
916
|
+
value: baseProps.value ?? "",
|
|
917
|
+
onChange: baseProps.onChange
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const componentProps = { field: fieldProps, spec };
|
|
921
|
+
return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, children: /* @__PURE__ */ jsx(
|
|
922
|
+
FieldWrapper,
|
|
923
|
+
{
|
|
924
|
+
fieldPath,
|
|
925
|
+
field: fieldDef,
|
|
926
|
+
errors,
|
|
927
|
+
touched,
|
|
928
|
+
required,
|
|
929
|
+
showRequiredIndicator,
|
|
930
|
+
visible: isVisible,
|
|
931
|
+
children: React.createElement(
|
|
932
|
+
Component,
|
|
933
|
+
componentProps
|
|
934
|
+
)
|
|
935
|
+
}
|
|
936
|
+
) }, fieldPath);
|
|
937
|
+
},
|
|
938
|
+
[
|
|
939
|
+
spec,
|
|
940
|
+
components,
|
|
941
|
+
FieldWrapper,
|
|
942
|
+
formaData,
|
|
943
|
+
formaComputed,
|
|
944
|
+
formaVisibility,
|
|
945
|
+
formaRequired,
|
|
946
|
+
formaEnabled,
|
|
947
|
+
formaReadonly,
|
|
948
|
+
formaOptionsVisibility,
|
|
949
|
+
formaTouched,
|
|
950
|
+
formaErrors,
|
|
951
|
+
setFieldValue,
|
|
952
|
+
setFieldTouched,
|
|
953
|
+
getArrayHelpers
|
|
954
|
+
]
|
|
955
|
+
);
|
|
764
956
|
const renderedFields = useMemo2(
|
|
765
957
|
() => fieldsToRender.map(renderField),
|
|
766
958
|
[fieldsToRender, renderField]
|
|
@@ -796,6 +988,7 @@ var FormRenderer = forwardRef(
|
|
|
796
988
|
|
|
797
989
|
// src/FieldRenderer.tsx
|
|
798
990
|
import React2 from "react";
|
|
991
|
+
import { isAdornableField as isAdornableField3 } from "@fogpipe/forma-core";
|
|
799
992
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
800
993
|
function getNumberConstraints2(schema) {
|
|
801
994
|
if (!schema) return {};
|
|
@@ -823,7 +1016,11 @@ function createDefaultItem2(itemFields) {
|
|
|
823
1016
|
}
|
|
824
1017
|
return item;
|
|
825
1018
|
}
|
|
826
|
-
function FieldRenderer({
|
|
1019
|
+
function FieldRenderer({
|
|
1020
|
+
fieldPath,
|
|
1021
|
+
components,
|
|
1022
|
+
className
|
|
1023
|
+
}) {
|
|
827
1024
|
const forma = useFormaContext();
|
|
828
1025
|
const { spec } = forma;
|
|
829
1026
|
const fieldDef = spec.fields[fieldPath];
|
|
@@ -832,8 +1029,10 @@ function FieldRenderer({ fieldPath, components, className }) {
|
|
|
832
1029
|
return null;
|
|
833
1030
|
}
|
|
834
1031
|
const isVisible = forma.visibility[fieldPath] !== false;
|
|
835
|
-
if (!isVisible)
|
|
836
|
-
|
|
1032
|
+
if (!isVisible) {
|
|
1033
|
+
return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, hidden: true });
|
|
1034
|
+
}
|
|
1035
|
+
const fieldType = fieldDef.type;
|
|
837
1036
|
const componentKey = fieldType;
|
|
838
1037
|
const Component = components[componentKey] || components.fallback;
|
|
839
1038
|
if (!Component) {
|
|
@@ -845,6 +1044,7 @@ function FieldRenderer({ fieldPath, components, className }) {
|
|
|
845
1044
|
const required = forma.required[fieldPath] ?? false;
|
|
846
1045
|
const disabled = forma.enabled[fieldPath] === false;
|
|
847
1046
|
const schemaProperty = spec.schema.properties[fieldPath];
|
|
1047
|
+
const isReadonly = forma.readonly[fieldPath] ?? false;
|
|
848
1048
|
const baseProps = {
|
|
849
1049
|
name: fieldPath,
|
|
850
1050
|
field: fieldDef,
|
|
@@ -859,9 +1059,18 @@ function FieldRenderer({ fieldPath, components, className }) {
|
|
|
859
1059
|
visible: true,
|
|
860
1060
|
// Always true since we already filtered for visibility
|
|
861
1061
|
enabled: !disabled,
|
|
1062
|
+
readonly: isReadonly,
|
|
862
1063
|
label: fieldDef.label ?? fieldPath,
|
|
863
1064
|
description: fieldDef.description,
|
|
864
|
-
placeholder: fieldDef.placeholder
|
|
1065
|
+
placeholder: fieldDef.placeholder,
|
|
1066
|
+
// Adorner properties (only for adornable field types)
|
|
1067
|
+
...isAdornableField3(fieldDef) && {
|
|
1068
|
+
prefix: fieldDef.prefix,
|
|
1069
|
+
suffix: fieldDef.suffix
|
|
1070
|
+
},
|
|
1071
|
+
// Presentation variant
|
|
1072
|
+
variant: fieldDef.variant,
|
|
1073
|
+
variantConfig: fieldDef.variantConfig
|
|
865
1074
|
};
|
|
866
1075
|
let fieldProps = baseProps;
|
|
867
1076
|
if (fieldType === "number") {
|
|
@@ -901,7 +1110,7 @@ function FieldRenderer({ fieldPath, components, className }) {
|
|
|
901
1110
|
onChange: baseProps.onChange,
|
|
902
1111
|
options: visibleOptions
|
|
903
1112
|
};
|
|
904
|
-
} else if (fieldType === "array" && fieldDef.itemFields) {
|
|
1113
|
+
} else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
|
|
905
1114
|
const arrayValue = baseProps.value ?? [];
|
|
906
1115
|
const minItems = fieldDef.minItems ?? 0;
|
|
907
1116
|
const maxItems = fieldDef.maxItems ?? Infinity;
|
|
@@ -930,7 +1139,10 @@ function FieldRenderer({ fieldPath, components, className }) {
|
|
|
930
1139
|
},
|
|
931
1140
|
swap: (indexA, indexB) => {
|
|
932
1141
|
const newArray = [...arrayValue];
|
|
933
|
-
[newArray[indexA], newArray[indexB]] = [
|
|
1142
|
+
[newArray[indexA], newArray[indexB]] = [
|
|
1143
|
+
newArray[indexB],
|
|
1144
|
+
newArray[indexA]
|
|
1145
|
+
];
|
|
934
1146
|
forma.setFieldValue(fieldPath, newArray);
|
|
935
1147
|
},
|
|
936
1148
|
getItemFieldProps: (index, fieldName) => {
|
|
@@ -948,6 +1160,7 @@ function FieldRenderer({ fieldPath, components, className }) {
|
|
|
948
1160
|
placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
|
|
949
1161
|
visible: true,
|
|
950
1162
|
enabled: !disabled,
|
|
1163
|
+
readonly: forma.readonly[itemPath] ?? false,
|
|
951
1164
|
required: (itemFieldDef == null ? void 0 : itemFieldDef.requiredWhen) === "true",
|
|
952
1165
|
touched: forma.touched[itemPath] ?? false,
|
|
953
1166
|
errors: forma.errors.filter((e) => e.field === itemPath),
|
|
@@ -978,6 +1191,20 @@ function FieldRenderer({ fieldPath, components, className }) {
|
|
|
978
1191
|
minItems,
|
|
979
1192
|
maxItems
|
|
980
1193
|
};
|
|
1194
|
+
} else if (fieldType === "display" && fieldDef.type === "display") {
|
|
1195
|
+
const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : void 0;
|
|
1196
|
+
const {
|
|
1197
|
+
onChange: _onChange,
|
|
1198
|
+
value: _value,
|
|
1199
|
+
...displayBaseProps
|
|
1200
|
+
} = baseProps;
|
|
1201
|
+
fieldProps = {
|
|
1202
|
+
...displayBaseProps,
|
|
1203
|
+
fieldType: "display",
|
|
1204
|
+
content: fieldDef.content,
|
|
1205
|
+
sourceValue,
|
|
1206
|
+
format: fieldDef.format
|
|
1207
|
+
};
|
|
981
1208
|
} else {
|
|
982
1209
|
fieldProps = {
|
|
983
1210
|
...baseProps,
|
|
@@ -987,11 +1214,14 @@ function FieldRenderer({ fieldPath, components, className }) {
|
|
|
987
1214
|
};
|
|
988
1215
|
}
|
|
989
1216
|
const componentProps = { field: fieldProps, spec };
|
|
990
|
-
const element = React2.createElement(
|
|
1217
|
+
const element = React2.createElement(
|
|
1218
|
+
Component,
|
|
1219
|
+
componentProps
|
|
1220
|
+
);
|
|
991
1221
|
if (className) {
|
|
992
|
-
return /* @__PURE__ */ jsx2("div", { className, children: element });
|
|
1222
|
+
return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, className, children: element });
|
|
993
1223
|
}
|
|
994
|
-
return element;
|
|
1224
|
+
return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, children: element });
|
|
995
1225
|
}
|
|
996
1226
|
|
|
997
1227
|
// src/ErrorBoundary.tsx
|