@fogpipe/forma-react 0.17.0 → 0.18.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.
- package/README.md +111 -26
- package/dist/FormRenderer-D_ZVK44t.d.ts +558 -0
- package/dist/chunk-5K4QITFH.js +1276 -0
- package/dist/chunk-5K4QITFH.js.map +1 -0
- package/dist/defaults/index.d.ts +56 -0
- package/dist/defaults/index.js +895 -0
- package/dist/defaults/index.js.map +1 -0
- package/dist/defaults/styles/forma-defaults.css +696 -0
- package/dist/index.d.ts +13 -549
- package/dist/index.js +34 -1273
- package/dist/index.js.map +1 -1
- package/package.json +17 -3
- package/src/FieldRenderer.tsx +12 -4
- package/src/FormRenderer.tsx +26 -9
- package/src/__tests__/FieldRenderer.test.tsx +5 -1
- package/src/__tests__/FormRenderer.test.tsx +146 -0
- package/src/__tests__/canProceed.test.ts +243 -0
- package/src/__tests__/defaults/components.test.tsx +818 -0
- package/src/__tests__/defaults/integration.test.tsx +494 -0
- package/src/__tests__/defaults/layout.test.tsx +298 -0
- package/src/__tests__/events.test.ts +15 -5
- package/src/__tests__/useForma.test.ts +108 -5
- package/src/defaults/DefaultFormRenderer.tsx +43 -0
- package/src/defaults/componentMap.ts +45 -0
- package/src/defaults/components/ArrayField.tsx +183 -0
- package/src/defaults/components/BooleanInput.tsx +32 -0
- package/src/defaults/components/ComputedDisplay.tsx +26 -0
- package/src/defaults/components/DateInput.tsx +59 -0
- package/src/defaults/components/DisplayField.tsx +15 -0
- package/src/defaults/components/FallbackField.tsx +35 -0
- package/src/defaults/components/MatrixField.tsx +98 -0
- package/src/defaults/components/MultiSelectInput.tsx +51 -0
- package/src/defaults/components/NumberInput.tsx +73 -0
- package/src/defaults/components/ObjectField.tsx +22 -0
- package/src/defaults/components/SelectInput.tsx +44 -0
- package/src/defaults/components/TextInput.tsx +48 -0
- package/src/defaults/components/TextareaInput.tsx +46 -0
- package/src/defaults/index.ts +33 -0
- package/src/defaults/layout/FieldWrapper.tsx +83 -0
- package/src/defaults/layout/FormLayout.tsx +34 -0
- package/src/defaults/layout/PageWrapper.tsx +18 -0
- package/src/defaults/layout/WizardLayout.tsx +130 -0
- package/src/defaults/styles/forma-defaults.css +696 -0
- package/src/events.ts +4 -1
- package/src/types.ts +16 -4
- package/src/useForma.ts +48 -34
package/src/types.ts
CHANGED
|
@@ -26,8 +26,13 @@ export interface BaseFieldProps {
|
|
|
26
26
|
required: boolean;
|
|
27
27
|
/** Whether the field is disabled */
|
|
28
28
|
disabled: boolean;
|
|
29
|
-
/** Validation errors for this field */
|
|
29
|
+
/** Validation errors for this field (always populated — use visibleErrors for display) */
|
|
30
30
|
errors: FieldError[];
|
|
31
|
+
/**
|
|
32
|
+
* Errors filtered by interaction state (touched or submitted).
|
|
33
|
+
* Use this for displaying errors in the UI to avoid showing errors on untouched fields.
|
|
34
|
+
*/
|
|
35
|
+
visibleErrors: FieldError[];
|
|
31
36
|
/** Handler for value changes */
|
|
32
37
|
onChange: (value: unknown) => void;
|
|
33
38
|
/** Handler for blur events */
|
|
@@ -322,7 +327,9 @@ export interface MatrixFieldProps extends Omit<
|
|
|
322
327
|
fieldType: "matrix";
|
|
323
328
|
/** Current matrix value: row ID → selected column value(s) */
|
|
324
329
|
value: Record<string, string | number | string[] | number[]> | null;
|
|
325
|
-
onChange: (
|
|
330
|
+
onChange: (
|
|
331
|
+
value: Record<string, string | number | string[] | number[]>,
|
|
332
|
+
) => void;
|
|
326
333
|
/** Row definitions with visibility state */
|
|
327
334
|
rows: Array<{ id: string; label: string; visible: boolean }>;
|
|
328
335
|
/** Column definitions (shared options for all rows) */
|
|
@@ -380,7 +387,7 @@ export interface ComponentMap {
|
|
|
380
387
|
*/
|
|
381
388
|
export interface LayoutProps {
|
|
382
389
|
children: React.ReactNode;
|
|
383
|
-
onSubmit: () => void;
|
|
390
|
+
onSubmit: (e?: React.FormEvent) => void;
|
|
384
391
|
isSubmitting: boolean;
|
|
385
392
|
isValid: boolean;
|
|
386
393
|
}
|
|
@@ -552,8 +559,13 @@ export interface GetFieldPropsResult {
|
|
|
552
559
|
showRequiredIndicator: boolean;
|
|
553
560
|
/** Whether field has been touched */
|
|
554
561
|
touched: boolean;
|
|
555
|
-
/** Validation errors for this field */
|
|
562
|
+
/** Validation errors for this field (always populated — use visibleErrors for display) */
|
|
556
563
|
errors: FieldError[];
|
|
564
|
+
/**
|
|
565
|
+
* Errors filtered by interaction state (touched or submitted).
|
|
566
|
+
* Use this for displaying errors in the UI to avoid showing errors on untouched fields.
|
|
567
|
+
*/
|
|
568
|
+
visibleErrors: FieldError[];
|
|
557
569
|
/** Handler for value changes */
|
|
558
570
|
onChange: (value: unknown) => void;
|
|
559
571
|
/** Handler for blur events */
|
package/src/useForma.ts
CHANGED
|
@@ -117,6 +117,12 @@ export interface WizardHelpers {
|
|
|
117
117
|
goToPage: (index: number) => void;
|
|
118
118
|
nextPage: () => void;
|
|
119
119
|
previousPage: () => void;
|
|
120
|
+
/**
|
|
121
|
+
* Safe "Next" handler for wizard navigation.
|
|
122
|
+
* Advances to the next page if one exists. Never triggers submission.
|
|
123
|
+
* Use this instead of conditionally calling nextPage/onSubmit in a single button.
|
|
124
|
+
*/
|
|
125
|
+
handleNext: () => void;
|
|
120
126
|
hasNextPage: boolean;
|
|
121
127
|
hasPreviousPage: boolean;
|
|
122
128
|
canProceed: boolean;
|
|
@@ -337,10 +343,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
337
343
|
|
|
338
344
|
// Helper: fire an event to both declarative `on` handlers and imperative listeners
|
|
339
345
|
const fireEvent = useCallback(
|
|
340
|
-
<K extends keyof FormaEventMap>(
|
|
341
|
-
event: K,
|
|
342
|
-
payload: FormaEventMap[K],
|
|
343
|
-
) => {
|
|
346
|
+
<K extends keyof FormaEventMap>(event: K, payload: FormaEventMap[K]) => {
|
|
344
347
|
// Declarative handler (via ref for latest callback)
|
|
345
348
|
try {
|
|
346
349
|
const handler = onEventsRef.current?.[event];
|
|
@@ -494,11 +497,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
494
497
|
|
|
495
498
|
// Queue a fieldChanged event (captures previousValue from current state ref)
|
|
496
499
|
const queueFieldChangedEvent = useCallback(
|
|
497
|
-
(
|
|
498
|
-
path: string,
|
|
499
|
-
value: unknown,
|
|
500
|
-
source: "user" | "reset" | "setValues",
|
|
501
|
-
) => {
|
|
500
|
+
(path: string, value: unknown, source: "user" | "reset" | "setValues") => {
|
|
502
501
|
if (isFiringEventsRef.current) return; // recursion guard
|
|
503
502
|
const previousValue = getValueAtPath(path);
|
|
504
503
|
if (previousValue === value) return; // no actual change
|
|
@@ -584,8 +583,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
584
583
|
postSubmitPayload = {
|
|
585
584
|
data: submissionData,
|
|
586
585
|
success: false,
|
|
587
|
-
error:
|
|
588
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
586
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
589
587
|
};
|
|
590
588
|
}
|
|
591
589
|
} else {
|
|
@@ -676,6 +674,21 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
676
674
|
const hasPreviousPage = clampedPageIndex > 0;
|
|
677
675
|
const isLastPage = clampedPageIndex === visiblePages.length - 1;
|
|
678
676
|
|
|
677
|
+
const advanceToNextPage = () => {
|
|
678
|
+
if (hasNextPage) {
|
|
679
|
+
const toIndex = clampedPageIndex + 1;
|
|
680
|
+
dispatch({ type: "SET_PAGE", page: toIndex });
|
|
681
|
+
const newPage = visiblePages[toIndex];
|
|
682
|
+
if (newPage) {
|
|
683
|
+
fireEvent("pageChanged", {
|
|
684
|
+
fromIndex: clampedPageIndex,
|
|
685
|
+
toIndex,
|
|
686
|
+
page: newPage,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
679
692
|
return {
|
|
680
693
|
pages,
|
|
681
694
|
currentPageIndex: clampedPageIndex,
|
|
@@ -694,20 +707,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
694
707
|
}
|
|
695
708
|
}
|
|
696
709
|
},
|
|
697
|
-
nextPage:
|
|
698
|
-
if (hasNextPage) {
|
|
699
|
-
const toIndex = clampedPageIndex + 1;
|
|
700
|
-
dispatch({ type: "SET_PAGE", page: toIndex });
|
|
701
|
-
const newPage = visiblePages[toIndex];
|
|
702
|
-
if (newPage) {
|
|
703
|
-
fireEvent("pageChanged", {
|
|
704
|
-
fromIndex: clampedPageIndex,
|
|
705
|
-
toIndex,
|
|
706
|
-
page: newPage,
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
},
|
|
710
|
+
nextPage: advanceToNextPage,
|
|
711
711
|
previousPage: () => {
|
|
712
712
|
if (hasPreviousPage) {
|
|
713
713
|
const toIndex = clampedPageIndex - 1;
|
|
@@ -722,6 +722,10 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
722
722
|
}
|
|
723
723
|
}
|
|
724
724
|
},
|
|
725
|
+
// Same function as nextPage — exposed as a separate name so consumers can
|
|
726
|
+
// bind a single "Next" button without risk of accidentally triggering submission.
|
|
727
|
+
// nextPage is already a no-op on the last page.
|
|
728
|
+
handleNext: advanceToNextPage,
|
|
725
729
|
hasNextPage,
|
|
726
730
|
hasPreviousPage,
|
|
727
731
|
canProceed: (() => {
|
|
@@ -756,7 +760,15 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
756
760
|
return pageErrors.length === 0;
|
|
757
761
|
},
|
|
758
762
|
};
|
|
759
|
-
}, [
|
|
763
|
+
}, [
|
|
764
|
+
spec,
|
|
765
|
+
state.data,
|
|
766
|
+
state.currentPage,
|
|
767
|
+
computed,
|
|
768
|
+
validation,
|
|
769
|
+
visibility,
|
|
770
|
+
fireEvent,
|
|
771
|
+
]);
|
|
760
772
|
|
|
761
773
|
// Flush pending events after render (fieldChanged, formReset)
|
|
762
774
|
useEffect(() => {
|
|
@@ -885,12 +897,12 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
885
897
|
|
|
886
898
|
const fieldErrors = validation.errors.filter((e) => e.field === path);
|
|
887
899
|
const isTouched = state.touched[path] ?? false;
|
|
888
|
-
const
|
|
900
|
+
const shouldShowErrors =
|
|
889
901
|
validateOn === "change" ||
|
|
890
902
|
(validateOn === "blur" && isTouched) ||
|
|
891
903
|
state.isSubmitted;
|
|
892
|
-
const
|
|
893
|
-
const
|
|
904
|
+
const visibleFieldErrors = shouldShowErrors ? fieldErrors : [];
|
|
905
|
+
const hasVisibleErrors = visibleFieldErrors.length > 0;
|
|
894
906
|
const isRequired = required[path] ?? false;
|
|
895
907
|
|
|
896
908
|
// Boolean fields: hide asterisk unless they have validation rules (consent pattern)
|
|
@@ -922,12 +934,13 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
922
934
|
required: isRequired,
|
|
923
935
|
showRequiredIndicator,
|
|
924
936
|
touched: isTouched,
|
|
925
|
-
errors:
|
|
937
|
+
errors: fieldErrors,
|
|
938
|
+
visibleErrors: visibleFieldErrors,
|
|
926
939
|
onChange: handlers.onChange,
|
|
927
940
|
onBlur: handlers.onBlur,
|
|
928
|
-
// ARIA accessibility attributes
|
|
929
|
-
"aria-invalid":
|
|
930
|
-
"aria-describedby":
|
|
941
|
+
// ARIA accessibility attributes (driven by visibleErrors, not all errors)
|
|
942
|
+
"aria-invalid": hasVisibleErrors || undefined,
|
|
943
|
+
"aria-describedby": hasVisibleErrors ? `${path}-error` : undefined,
|
|
931
944
|
"aria-required": isRequired || undefined,
|
|
932
945
|
// Adorner props (only for adornable field types)
|
|
933
946
|
...adornerProps,
|
|
@@ -1020,7 +1033,8 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
1020
1033
|
required: false, // TODO: Evaluate item field required
|
|
1021
1034
|
showRequiredIndicator: false, // Item fields don't show required indicator
|
|
1022
1035
|
touched: isTouched,
|
|
1023
|
-
errors:
|
|
1036
|
+
errors: fieldErrors,
|
|
1037
|
+
visibleErrors: showErrors ? fieldErrors : [],
|
|
1024
1038
|
onChange: handlers.onChange,
|
|
1025
1039
|
onBlur: handlers.onBlur,
|
|
1026
1040
|
options: visibleOptions,
|