@fogpipe/forma-react 0.9.0 → 0.10.1
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 +11 -1
- package/dist/index.js +65 -89
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/FormRenderer.tsx +38 -84
- package/src/__tests__/FormRenderer.test.tsx +167 -0
- package/src/__tests__/canProceed.test.ts +11 -8
- package/src/__tests__/diabetes-trial-flow.test.ts +41 -24
- package/src/__tests__/null-handling.test.ts +53 -50
- package/src/__tests__/useForma.test.ts +129 -0
- package/src/types.ts +11 -1
- package/src/useForma.ts +28 -1
package/dist/index.d.ts
CHANGED
|
@@ -273,6 +273,11 @@ interface FieldWrapperProps {
|
|
|
273
273
|
errors: FieldError[];
|
|
274
274
|
touched: boolean;
|
|
275
275
|
required: boolean;
|
|
276
|
+
/**
|
|
277
|
+
* Whether to show the required indicator in the UI.
|
|
278
|
+
* False for boolean fields since false is a valid answer.
|
|
279
|
+
*/
|
|
280
|
+
showRequiredIndicator: boolean;
|
|
276
281
|
visible: boolean;
|
|
277
282
|
}
|
|
278
283
|
/**
|
|
@@ -362,8 +367,13 @@ interface GetFieldPropsResult {
|
|
|
362
367
|
visible: boolean;
|
|
363
368
|
/** Whether field is enabled (not disabled) */
|
|
364
369
|
enabled: boolean;
|
|
365
|
-
/** Whether field is required */
|
|
370
|
+
/** Whether field is required (for validation) */
|
|
366
371
|
required: boolean;
|
|
372
|
+
/**
|
|
373
|
+
* Whether to show the required indicator in the UI.
|
|
374
|
+
* False for boolean fields since false is a valid answer.
|
|
375
|
+
*/
|
|
376
|
+
showRequiredIndicator: boolean;
|
|
367
377
|
/** Whether field has been touched */
|
|
368
378
|
touched: boolean;
|
|
369
379
|
/** Validation errors for this field */
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,18 @@ function formReducer(state, action) {
|
|
|
50
50
|
return state;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
function getDefaultBooleanValues(spec) {
|
|
54
|
+
var _a;
|
|
55
|
+
const defaults = {};
|
|
56
|
+
for (const fieldPath of spec.fieldOrder) {
|
|
57
|
+
const schemaProperty = (_a = spec.schema.properties) == null ? void 0 : _a[fieldPath];
|
|
58
|
+
const fieldDef = spec.fields[fieldPath];
|
|
59
|
+
if ((schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean") {
|
|
60
|
+
defaults[fieldPath] = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return defaults;
|
|
64
|
+
}
|
|
53
65
|
function useForma(options) {
|
|
54
66
|
const { spec: inputSpec, initialData = {}, onSubmit, onChange, validateOn = "blur", referenceData, validationDebounceMs = 0 } = options;
|
|
55
67
|
const spec = useMemo(() => {
|
|
@@ -63,7 +75,8 @@ function useForma(options) {
|
|
|
63
75
|
};
|
|
64
76
|
}, [inputSpec, referenceData]);
|
|
65
77
|
const [state, dispatch] = useReducer(formReducer, {
|
|
66
|
-
data: initialData,
|
|
78
|
+
data: { ...getDefaultBooleanValues(spec), ...initialData },
|
|
79
|
+
// Boolean defaults merged UNDER initialData
|
|
67
80
|
touched: {},
|
|
68
81
|
isSubmitting: false,
|
|
69
82
|
isSubmitted: false,
|
|
@@ -305,23 +318,24 @@ function useForma(options) {
|
|
|
305
318
|
return fieldHandlers.current.get(path);
|
|
306
319
|
}, [setValueAtPath, setFieldTouched]);
|
|
307
320
|
const getFieldProps = useCallback((path) => {
|
|
321
|
+
var _a;
|
|
308
322
|
const fieldDef = spec.fields[path];
|
|
309
323
|
const handlers = getFieldHandlers(path);
|
|
310
324
|
let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
|
|
311
325
|
if (!fieldType || fieldType === "computed") {
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
314
|
-
if (
|
|
315
|
-
else if (
|
|
316
|
-
else if (
|
|
317
|
-
else if (
|
|
318
|
-
else if (
|
|
319
|
-
else if ("enum" in
|
|
320
|
-
else if ("format" in
|
|
321
|
-
if (
|
|
322
|
-
else if (
|
|
323
|
-
else if (
|
|
324
|
-
else if (
|
|
326
|
+
const schemaProperty2 = spec.schema.properties[path];
|
|
327
|
+
if (schemaProperty2) {
|
|
328
|
+
if (schemaProperty2.type === "number") fieldType = "number";
|
|
329
|
+
else if (schemaProperty2.type === "integer") fieldType = "integer";
|
|
330
|
+
else if (schemaProperty2.type === "boolean") fieldType = "boolean";
|
|
331
|
+
else if (schemaProperty2.type === "array") fieldType = "array";
|
|
332
|
+
else if (schemaProperty2.type === "object") fieldType = "object";
|
|
333
|
+
else if ("enum" in schemaProperty2 && schemaProperty2.enum) fieldType = "select";
|
|
334
|
+
else if ("format" in schemaProperty2) {
|
|
335
|
+
if (schemaProperty2.format === "date") fieldType = "date";
|
|
336
|
+
else if (schemaProperty2.format === "date-time") fieldType = "datetime";
|
|
337
|
+
else if (schemaProperty2.format === "email") fieldType = "email";
|
|
338
|
+
else if (schemaProperty2.format === "uri") fieldType = "url";
|
|
325
339
|
}
|
|
326
340
|
}
|
|
327
341
|
}
|
|
@@ -331,6 +345,10 @@ function useForma(options) {
|
|
|
331
345
|
const displayedErrors = showErrors ? fieldErrors : [];
|
|
332
346
|
const hasErrors = displayedErrors.length > 0;
|
|
333
347
|
const isRequired = required[path] ?? false;
|
|
348
|
+
const schemaProperty = spec.schema.properties[path];
|
|
349
|
+
const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
|
|
350
|
+
const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
351
|
+
const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
|
|
334
352
|
return {
|
|
335
353
|
name: path,
|
|
336
354
|
value: getValueAtPath(path),
|
|
@@ -341,6 +359,7 @@ function useForma(options) {
|
|
|
341
359
|
visible: visibility[path] !== false,
|
|
342
360
|
enabled: enabled[path] !== false,
|
|
343
361
|
required: isRequired,
|
|
362
|
+
showRequiredIndicator,
|
|
344
363
|
touched: isTouched,
|
|
345
364
|
errors: displayedErrors,
|
|
346
365
|
onChange: handlers.onChange,
|
|
@@ -387,6 +406,8 @@ function useForma(options) {
|
|
|
387
406
|
enabled: enabled[path] !== false,
|
|
388
407
|
required: false,
|
|
389
408
|
// TODO: Evaluate item field required
|
|
409
|
+
showRequiredIndicator: false,
|
|
410
|
+
// Item fields don't show required indicator
|
|
390
411
|
touched: isTouched,
|
|
391
412
|
errors: showErrors ? fieldErrors : [],
|
|
392
413
|
onChange: handlers.onChange,
|
|
@@ -490,7 +511,7 @@ function DefaultLayout({ children, onSubmit, isSubmitting }) {
|
|
|
490
511
|
}
|
|
491
512
|
);
|
|
492
513
|
}
|
|
493
|
-
function DefaultFieldWrapper({ fieldPath, field, children, errors,
|
|
514
|
+
function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }) {
|
|
494
515
|
if (!visible) return null;
|
|
495
516
|
const errorId = `${fieldPath}-error`;
|
|
496
517
|
const descriptionId = field.description ? `${fieldPath}-description` : void 0;
|
|
@@ -498,8 +519,8 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, required, vis
|
|
|
498
519
|
return /* @__PURE__ */ jsxs("div", { className: "field-wrapper", "data-field-path": fieldPath, children: [
|
|
499
520
|
field.label && /* @__PURE__ */ jsxs("label", { htmlFor: fieldPath, children: [
|
|
500
521
|
field.label,
|
|
501
|
-
|
|
502
|
-
|
|
522
|
+
showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "required", "aria-hidden": "true", children: "*" }),
|
|
523
|
+
showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (required)" })
|
|
503
524
|
] }),
|
|
504
525
|
children,
|
|
505
526
|
hasErrors && /* @__PURE__ */ jsx(
|
|
@@ -569,7 +590,6 @@ var FormRenderer = forwardRef(
|
|
|
569
590
|
validateOn
|
|
570
591
|
});
|
|
571
592
|
const fieldRefs = useRef2(/* @__PURE__ */ new Map());
|
|
572
|
-
const arrayHelpersCache = useRef2(/* @__PURE__ */ new Map());
|
|
573
593
|
const focusField = useCallback2((path) => {
|
|
574
594
|
const element = fieldRefs.current.get(path);
|
|
575
595
|
element == null ? void 0 : element.focus();
|
|
@@ -607,6 +627,7 @@ var FormRenderer = forwardRef(
|
|
|
607
627
|
return spec.fieldOrder;
|
|
608
628
|
}, [spec.pages, spec.fieldOrder, forma.wizard]);
|
|
609
629
|
const renderField = useCallback2((fieldPath) => {
|
|
630
|
+
var _a;
|
|
610
631
|
const fieldDef = spec.fields[fieldPath];
|
|
611
632
|
if (!fieldDef) return null;
|
|
612
633
|
const isVisible = forma.visibility[fieldPath] !== false;
|
|
@@ -623,6 +644,9 @@ var FormRenderer = forwardRef(
|
|
|
623
644
|
const required = forma.required[fieldPath] ?? false;
|
|
624
645
|
const disabled = forma.enabled[fieldPath] === false;
|
|
625
646
|
const schemaProperty = spec.schema.properties[fieldPath];
|
|
647
|
+
const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
|
|
648
|
+
const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
649
|
+
const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
|
|
626
650
|
const baseProps = {
|
|
627
651
|
name: fieldPath,
|
|
628
652
|
field: fieldDef,
|
|
@@ -660,82 +684,33 @@ var FormRenderer = forwardRef(
|
|
|
660
684
|
options: fieldDef.options ?? []
|
|
661
685
|
};
|
|
662
686
|
} else if (fieldType === "array" && fieldDef.itemFields) {
|
|
663
|
-
const arrayValue = baseProps.value
|
|
687
|
+
const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
|
|
664
688
|
const minItems = fieldDef.minItems ?? 0;
|
|
665
689
|
const maxItems = fieldDef.maxItems ?? Infinity;
|
|
666
690
|
const itemFieldDefs = fieldDef.itemFields;
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const newArray = [...currentArray];
|
|
683
|
-
newArray.splice(index, 1);
|
|
684
|
-
forma.setFieldValue(fieldPath, newArray);
|
|
685
|
-
},
|
|
686
|
-
move: (from, to) => {
|
|
687
|
-
const currentArray = forma.data[fieldPath] ?? [];
|
|
688
|
-
const newArray = [...currentArray];
|
|
689
|
-
const [item] = newArray.splice(from, 1);
|
|
690
|
-
newArray.splice(to, 0, item);
|
|
691
|
-
forma.setFieldValue(fieldPath, newArray);
|
|
692
|
-
},
|
|
693
|
-
swap: (indexA, indexB) => {
|
|
694
|
-
const currentArray = forma.data[fieldPath] ?? [];
|
|
695
|
-
const newArray = [...currentArray];
|
|
696
|
-
[newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
|
|
697
|
-
forma.setFieldValue(fieldPath, newArray);
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
const cachedHelpers = arrayHelpersCache.current.get(fieldPath);
|
|
691
|
+
const baseHelpers = forma.getArrayHelpers(fieldPath);
|
|
692
|
+
const pushWithDefault = (item) => {
|
|
693
|
+
const newItem = item ?? createDefaultItem(itemFieldDefs);
|
|
694
|
+
baseHelpers.push(newItem);
|
|
695
|
+
};
|
|
696
|
+
const getItemFieldPropsExtended = (index, fieldName) => {
|
|
697
|
+
const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
|
|
698
|
+
const itemFieldDef = itemFieldDefs[fieldName];
|
|
699
|
+
return {
|
|
700
|
+
...baseProps2,
|
|
701
|
+
itemIndex: index,
|
|
702
|
+
fieldName,
|
|
703
|
+
options: itemFieldDef == null ? void 0 : itemFieldDef.options
|
|
704
|
+
};
|
|
705
|
+
};
|
|
702
706
|
const helpers = {
|
|
703
707
|
items: arrayValue,
|
|
704
|
-
push:
|
|
705
|
-
insert:
|
|
706
|
-
remove:
|
|
707
|
-
move:
|
|
708
|
-
swap:
|
|
709
|
-
getItemFieldProps:
|
|
710
|
-
var _a;
|
|
711
|
-
const itemFieldDef = itemFieldDefs[fieldName];
|
|
712
|
-
const itemPath = `${fieldPath}[${index}].${fieldName}`;
|
|
713
|
-
const itemValue = (_a = arrayValue[index]) == null ? void 0 : _a[fieldName];
|
|
714
|
-
return {
|
|
715
|
-
name: itemPath,
|
|
716
|
-
value: itemValue,
|
|
717
|
-
type: (itemFieldDef == null ? void 0 : itemFieldDef.type) ?? "text",
|
|
718
|
-
label: (itemFieldDef == null ? void 0 : itemFieldDef.label) ?? fieldName,
|
|
719
|
-
description: itemFieldDef == null ? void 0 : itemFieldDef.description,
|
|
720
|
-
placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
|
|
721
|
-
visible: true,
|
|
722
|
-
enabled: !disabled,
|
|
723
|
-
required: (itemFieldDef == null ? void 0 : itemFieldDef.requiredWhen) === "true",
|
|
724
|
-
touched: forma.touched[itemPath] ?? false,
|
|
725
|
-
errors: forma.errors.filter((e) => e.field === itemPath),
|
|
726
|
-
onChange: (value) => {
|
|
727
|
-
const currentArray = forma.data[fieldPath] ?? [];
|
|
728
|
-
const newArray = [...currentArray];
|
|
729
|
-
const item = newArray[index] ?? {};
|
|
730
|
-
newArray[index] = { ...item, [fieldName]: value };
|
|
731
|
-
forma.setFieldValue(fieldPath, newArray);
|
|
732
|
-
},
|
|
733
|
-
onBlur: () => forma.setFieldTouched(itemPath),
|
|
734
|
-
itemIndex: index,
|
|
735
|
-
fieldName,
|
|
736
|
-
options: itemFieldDef == null ? void 0 : itemFieldDef.options
|
|
737
|
-
};
|
|
738
|
-
},
|
|
708
|
+
push: pushWithDefault,
|
|
709
|
+
insert: baseHelpers.insert,
|
|
710
|
+
remove: baseHelpers.remove,
|
|
711
|
+
move: baseHelpers.move,
|
|
712
|
+
swap: baseHelpers.swap,
|
|
713
|
+
getItemFieldProps: getItemFieldPropsExtended,
|
|
739
714
|
minItems,
|
|
740
715
|
maxItems,
|
|
741
716
|
canAdd: arrayValue.length < maxItems,
|
|
@@ -768,6 +743,7 @@ var FormRenderer = forwardRef(
|
|
|
768
743
|
errors,
|
|
769
744
|
touched,
|
|
770
745
|
required,
|
|
746
|
+
showRequiredIndicator,
|
|
771
747
|
visible: isVisible,
|
|
772
748
|
children: React.createElement(Component, componentProps)
|
|
773
749
|
},
|