@fogpipe/forma-react 0.16.0 → 0.17.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/README.md +29 -26
- package/dist/index.d.ts +46 -6
- package/dist/index.js +95 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/FieldRenderer.tsx +34 -4
- package/src/FormRenderer.tsx +47 -9
- package/src/__tests__/FieldRenderer.test.tsx +186 -0
- package/src/__tests__/FormRenderer.test.tsx +146 -0
- package/src/__tests__/canProceed.test.ts +243 -0
- package/src/__tests__/events.test.ts +15 -5
- package/src/__tests__/useForma.test.ts +108 -5
- package/src/events.ts +4 -1
- package/src/index.ts +2 -0
- package/src/types.ts +43 -4
- package/src/useForma.ts +48 -34
package/README.md
CHANGED
|
@@ -199,13 +199,13 @@ The event system lets you observe form lifecycle events for side effects like an
|
|
|
199
199
|
|
|
200
200
|
### Available Events
|
|
201
201
|
|
|
202
|
-
| Event | Description
|
|
203
|
-
| -------------- |
|
|
204
|
-
| `fieldChanged` | Fires after a field value changes
|
|
205
|
-
| `preSubmit` | Fires before validation; `data` is mutable
|
|
206
|
-
| `postSubmit` | Fires after submission (success, error, or invalid)
|
|
207
|
-
| `pageChanged` | Fires when wizard page changes
|
|
208
|
-
| `formReset` | Fires after `resetForm()` completes
|
|
202
|
+
| Event | Description |
|
|
203
|
+
| -------------- | --------------------------------------------------- |
|
|
204
|
+
| `fieldChanged` | Fires after a field value changes |
|
|
205
|
+
| `preSubmit` | Fires before validation; `data` is mutable |
|
|
206
|
+
| `postSubmit` | Fires after submission (success, error, or invalid) |
|
|
207
|
+
| `pageChanged` | Fires when wizard page changes |
|
|
208
|
+
| `formReset` | Fires after `resetForm()` completes |
|
|
209
209
|
|
|
210
210
|
### Declarative Registration
|
|
211
211
|
|
|
@@ -217,7 +217,10 @@ const forma = useForma({
|
|
|
217
217
|
onSubmit: handleSubmit,
|
|
218
218
|
on: {
|
|
219
219
|
fieldChanged: (event) => {
|
|
220
|
-
analytics.track("field_changed", {
|
|
220
|
+
analytics.track("field_changed", {
|
|
221
|
+
path: event.path,
|
|
222
|
+
source: event.source,
|
|
223
|
+
});
|
|
221
224
|
},
|
|
222
225
|
preSubmit: async (event) => {
|
|
223
226
|
event.data.token = await getCSRFToken();
|
|
@@ -302,28 +305,28 @@ formRef.current?.setValues({ name: "John" });
|
|
|
302
305
|
|
|
303
306
|
### useForma Methods
|
|
304
307
|
|
|
305
|
-
| Method | Description
|
|
306
|
-
| --------------------------------- |
|
|
307
|
-
| `setFieldValue(path, value)` | Set field value
|
|
308
|
-
| `setFieldTouched(path, touched?)` | Mark field as touched
|
|
309
|
-
| `setValues(values)` | Set multiple values
|
|
310
|
-
| `validateField(path)` | Validate single field
|
|
311
|
-
| `validateForm()` | Validate entire form
|
|
312
|
-
| `submitForm()` | Submit the form
|
|
313
|
-
| `resetForm()` | Reset to initial values
|
|
308
|
+
| Method | Description |
|
|
309
|
+
| --------------------------------- | -------------------------------------------- |
|
|
310
|
+
| `setFieldValue(path, value)` | Set field value |
|
|
311
|
+
| `setFieldTouched(path, touched?)` | Mark field as touched |
|
|
312
|
+
| `setValues(values)` | Set multiple values |
|
|
313
|
+
| `validateField(path)` | Validate single field |
|
|
314
|
+
| `validateForm()` | Validate entire form |
|
|
315
|
+
| `submitForm()` | Submit the form |
|
|
316
|
+
| `resetForm()` | Reset to initial values |
|
|
314
317
|
| `on(event, listener)` | Register event listener; returns unsubscribe |
|
|
315
318
|
|
|
316
319
|
### useForma Options
|
|
317
320
|
|
|
318
|
-
| Option | Type | Default | Description
|
|
319
|
-
| ---------------------- | -------------------------------- | -------- |
|
|
320
|
-
| `spec` | `Forma` | required | The Forma specification
|
|
321
|
-
| `initialData` | `Record<string, unknown>` | `{}` | Initial form values
|
|
322
|
-
| `onSubmit` | `(data) => void` | - | Submit handler
|
|
323
|
-
| `onChange` | `(data, computed) => void` | - | Change handler
|
|
324
|
-
| `validateOn` | `"change" \| "blur" \| "submit"` | `"blur"` | When to validate
|
|
325
|
-
| `referenceData` | `Record<string, unknown>` | - | Additional reference data
|
|
326
|
-
| `validationDebounceMs` | `number` | `0` | Debounce validation (ms)
|
|
321
|
+
| Option | Type | Default | Description |
|
|
322
|
+
| ---------------------- | -------------------------------- | -------- | --------------------------- |
|
|
323
|
+
| `spec` | `Forma` | required | The Forma specification |
|
|
324
|
+
| `initialData` | `Record<string, unknown>` | `{}` | Initial form values |
|
|
325
|
+
| `onSubmit` | `(data) => void` | - | Submit handler |
|
|
326
|
+
| `onChange` | `(data, computed) => void` | - | Change handler |
|
|
327
|
+
| `validateOn` | `"change" \| "blur" \| "submit"` | `"blur"` | When to validate |
|
|
328
|
+
| `referenceData` | `Record<string, unknown>` | - | Additional reference data |
|
|
329
|
+
| `validationDebounceMs` | `number` | `0` | Debounce validation (ms) |
|
|
327
330
|
| `on` | `FormaEvents` | - | Declarative event listeners |
|
|
328
331
|
|
|
329
332
|
## Error Boundary
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FieldError, SelectOption, FieldDefinition, Forma, OptionsVisibilityResult, ValidationResult } from '@fogpipe/forma-core';
|
|
1
|
+
import { FieldError, SelectOption, FieldDefinition, Forma, MatrixColumn, OptionsVisibilityResult, ValidationResult } from '@fogpipe/forma-core';
|
|
2
2
|
export { ComputedField, FieldDefinition, FieldError, FieldType, Forma, PageDefinition, SelectOption, ValidationResult, ValidationRule } from '@fogpipe/forma-core';
|
|
3
3
|
import * as React$1 from 'react';
|
|
4
4
|
import React__default from 'react';
|
|
@@ -102,8 +102,13 @@ interface BaseFieldProps {
|
|
|
102
102
|
required: boolean;
|
|
103
103
|
/** Whether the field is disabled */
|
|
104
104
|
disabled: boolean;
|
|
105
|
-
/** Validation errors for this field */
|
|
105
|
+
/** Validation errors for this field (always populated — use visibleErrors for display) */
|
|
106
106
|
errors: FieldError[];
|
|
107
|
+
/**
|
|
108
|
+
* Errors filtered by interaction state (touched or submitted).
|
|
109
|
+
* Use this for displaying errors in the UI to avoid showing errors on untouched fields.
|
|
110
|
+
*/
|
|
111
|
+
visibleErrors: FieldError[];
|
|
107
112
|
/** Handler for value changes */
|
|
108
113
|
onChange: (value: unknown) => void;
|
|
109
114
|
/** Handler for blur events */
|
|
@@ -331,10 +336,29 @@ interface DisplayFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
|
|
|
331
336
|
onChange?: never;
|
|
332
337
|
value?: never;
|
|
333
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Props for matrix/grid fields
|
|
341
|
+
*/
|
|
342
|
+
interface MatrixFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
|
|
343
|
+
fieldType: "matrix";
|
|
344
|
+
/** Current matrix value: row ID → selected column value(s) */
|
|
345
|
+
value: Record<string, string | number | string[] | number[]> | null;
|
|
346
|
+
onChange: (value: Record<string, string | number | string[] | number[]>) => void;
|
|
347
|
+
/** Row definitions with visibility state */
|
|
348
|
+
rows: Array<{
|
|
349
|
+
id: string;
|
|
350
|
+
label: string;
|
|
351
|
+
visible: boolean;
|
|
352
|
+
}>;
|
|
353
|
+
/** Column definitions (shared options for all rows) */
|
|
354
|
+
columns: MatrixColumn[];
|
|
355
|
+
/** Whether multiple selections per row are allowed */
|
|
356
|
+
multiSelect: boolean;
|
|
357
|
+
}
|
|
334
358
|
/**
|
|
335
359
|
* Union of all field prop types
|
|
336
360
|
*/
|
|
337
|
-
type FieldProps = TextFieldProps | NumberFieldProps | IntegerFieldProps | BooleanFieldProps | DateFieldProps | DateTimeFieldProps | SelectFieldProps | MultiSelectFieldProps | ArrayFieldProps | ObjectFieldProps | ComputedFieldProps | DisplayFieldProps;
|
|
361
|
+
type FieldProps = TextFieldProps | NumberFieldProps | IntegerFieldProps | BooleanFieldProps | DateFieldProps | DateTimeFieldProps | SelectFieldProps | MultiSelectFieldProps | ArrayFieldProps | ObjectFieldProps | ComputedFieldProps | DisplayFieldProps | MatrixFieldProps;
|
|
338
362
|
/**
|
|
339
363
|
* Map of field types to React components
|
|
340
364
|
* Components receive wrapper props with { field, spec } structure
|
|
@@ -357,6 +381,7 @@ interface ComponentMap {
|
|
|
357
381
|
object?: React.ComponentType<ObjectComponentProps>;
|
|
358
382
|
computed?: React.ComponentType<ComputedComponentProps>;
|
|
359
383
|
display?: React.ComponentType<DisplayComponentProps>;
|
|
384
|
+
matrix?: React.ComponentType<MatrixComponentProps>;
|
|
360
385
|
fallback?: React.ComponentType<FieldComponentProps>;
|
|
361
386
|
}
|
|
362
387
|
/**
|
|
@@ -364,7 +389,7 @@ interface ComponentMap {
|
|
|
364
389
|
*/
|
|
365
390
|
interface LayoutProps {
|
|
366
391
|
children: React.ReactNode;
|
|
367
|
-
onSubmit: () => void;
|
|
392
|
+
onSubmit: (e?: React.FormEvent) => void;
|
|
368
393
|
isSubmitting: boolean;
|
|
369
394
|
isValid: boolean;
|
|
370
395
|
}
|
|
@@ -449,6 +474,10 @@ interface DisplayComponentProps {
|
|
|
449
474
|
field: DisplayFieldProps;
|
|
450
475
|
spec: Forma;
|
|
451
476
|
}
|
|
477
|
+
interface MatrixComponentProps {
|
|
478
|
+
field: MatrixFieldProps;
|
|
479
|
+
spec: Forma;
|
|
480
|
+
}
|
|
452
481
|
/**
|
|
453
482
|
* Generic field component props (for fallback/dynamic components)
|
|
454
483
|
*/
|
|
@@ -489,8 +518,13 @@ interface GetFieldPropsResult {
|
|
|
489
518
|
showRequiredIndicator: boolean;
|
|
490
519
|
/** Whether field has been touched */
|
|
491
520
|
touched: boolean;
|
|
492
|
-
/** Validation errors for this field */
|
|
521
|
+
/** Validation errors for this field (always populated — use visibleErrors for display) */
|
|
493
522
|
errors: FieldError[];
|
|
523
|
+
/**
|
|
524
|
+
* Errors filtered by interaction state (touched or submitted).
|
|
525
|
+
* Use this for displaying errors in the UI to avoid showing errors on untouched fields.
|
|
526
|
+
*/
|
|
527
|
+
visibleErrors: FieldError[];
|
|
494
528
|
/** Handler for value changes */
|
|
495
529
|
onChange: (value: unknown) => void;
|
|
496
530
|
/** Handler for blur events */
|
|
@@ -607,6 +641,12 @@ interface WizardHelpers {
|
|
|
607
641
|
goToPage: (index: number) => void;
|
|
608
642
|
nextPage: () => void;
|
|
609
643
|
previousPage: () => void;
|
|
644
|
+
/**
|
|
645
|
+
* Safe "Next" handler for wizard navigation.
|
|
646
|
+
* Advances to the next page if one exists. Never triggers submission.
|
|
647
|
+
* Use this instead of conditionally calling nextPage/onSubmit in a single button.
|
|
648
|
+
*/
|
|
649
|
+
handleNext: () => void;
|
|
610
650
|
hasNextPage: boolean;
|
|
611
651
|
hasPreviousPage: boolean;
|
|
612
652
|
canProceed: boolean;
|
|
@@ -814,4 +854,4 @@ declare const FormaContext: React$1.Context<UseFormaReturn | null>;
|
|
|
814
854
|
*/
|
|
815
855
|
declare function useFormaContext(): UseFormaReturn;
|
|
816
856
|
|
|
817
|
-
export { type ArrayComponentProps, type ArrayFieldProps, type ArrayHelpers, type ArrayItemFieldProps, type ArrayItemFieldPropsResult, type BaseFieldProps, type BooleanComponentProps, type BooleanFieldProps, type ComponentMap, type ComputedComponentProps, type ComputedFieldProps, type DateComponentProps, type DateFieldProps, type DateTimeComponentProps, type DateTimeFieldProps, type DisplayComponentProps, type DisplayFieldProps, type FieldComponentProps, type FieldProps, FieldRenderer, type FieldRendererProps, type FieldWrapperProps, FormRenderer, type FormRendererHandle, type FormRendererProps, type UseFormaReturn as FormState, FormaContext, FormaErrorBoundary, type FormaErrorBoundaryProps, type FormaEventListener, type FormaEventMap, type FormaEvents, type GetArrayHelpersResult, type GetFieldPropsResult, type GetSelectFieldPropsResult, type IntegerComponentProps, type IntegerFieldProps, type LayoutProps, type LegacyFieldProps, type MultiSelectComponentProps, type MultiSelectFieldProps, type NumberComponentProps, type NumberFieldProps, type ObjectComponentProps, type ObjectFieldProps, type PageState, type PageWrapperProps, type SelectComponentProps, type SelectFieldProps, type SelectionFieldProps, type TextComponentProps, type TextFieldProps, type UseFormaOptions, type UseFormaReturn, type WizardHelpers, useForma, useFormaContext };
|
|
857
|
+
export { type ArrayComponentProps, type ArrayFieldProps, type ArrayHelpers, type ArrayItemFieldProps, type ArrayItemFieldPropsResult, type BaseFieldProps, type BooleanComponentProps, type BooleanFieldProps, type ComponentMap, type ComputedComponentProps, type ComputedFieldProps, type DateComponentProps, type DateFieldProps, type DateTimeComponentProps, type DateTimeFieldProps, type DisplayComponentProps, type DisplayFieldProps, type FieldComponentProps, type FieldProps, FieldRenderer, type FieldRendererProps, type FieldWrapperProps, FormRenderer, type FormRendererHandle, type FormRendererProps, type UseFormaReturn as FormState, FormaContext, FormaErrorBoundary, type FormaErrorBoundaryProps, type FormaEventListener, type FormaEventMap, type FormaEvents, type GetArrayHelpersResult, type GetFieldPropsResult, type GetSelectFieldPropsResult, type IntegerComponentProps, type IntegerFieldProps, type LayoutProps, type LegacyFieldProps, type MatrixComponentProps, type MatrixFieldProps, type MultiSelectComponentProps, type MultiSelectFieldProps, type NumberComponentProps, type NumberFieldProps, type ObjectComponentProps, type ObjectFieldProps, type PageState, type PageWrapperProps, type SelectComponentProps, type SelectFieldProps, type SelectionFieldProps, type TextComponentProps, type TextFieldProps, type UseFormaOptions, type UseFormaReturn, type WizardHelpers, useForma, useFormaContext };
|
package/dist/index.js
CHANGED
|
@@ -443,6 +443,20 @@ function useForma(options) {
|
|
|
443
443
|
const hasNextPage = clampedPageIndex < visiblePages.length - 1;
|
|
444
444
|
const hasPreviousPage = clampedPageIndex > 0;
|
|
445
445
|
const isLastPage = clampedPageIndex === visiblePages.length - 1;
|
|
446
|
+
const advanceToNextPage = () => {
|
|
447
|
+
if (hasNextPage) {
|
|
448
|
+
const toIndex = clampedPageIndex + 1;
|
|
449
|
+
dispatch({ type: "SET_PAGE", page: toIndex });
|
|
450
|
+
const newPage = visiblePages[toIndex];
|
|
451
|
+
if (newPage) {
|
|
452
|
+
fireEvent("pageChanged", {
|
|
453
|
+
fromIndex: clampedPageIndex,
|
|
454
|
+
toIndex,
|
|
455
|
+
page: newPage
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
446
460
|
return {
|
|
447
461
|
pages,
|
|
448
462
|
currentPageIndex: clampedPageIndex,
|
|
@@ -461,20 +475,7 @@ function useForma(options) {
|
|
|
461
475
|
}
|
|
462
476
|
}
|
|
463
477
|
},
|
|
464
|
-
nextPage:
|
|
465
|
-
if (hasNextPage) {
|
|
466
|
-
const toIndex = clampedPageIndex + 1;
|
|
467
|
-
dispatch({ type: "SET_PAGE", page: toIndex });
|
|
468
|
-
const newPage = visiblePages[toIndex];
|
|
469
|
-
if (newPage) {
|
|
470
|
-
fireEvent("pageChanged", {
|
|
471
|
-
fromIndex: clampedPageIndex,
|
|
472
|
-
toIndex,
|
|
473
|
-
page: newPage
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
},
|
|
478
|
+
nextPage: advanceToNextPage,
|
|
478
479
|
previousPage: () => {
|
|
479
480
|
if (hasPreviousPage) {
|
|
480
481
|
const toIndex = clampedPageIndex - 1;
|
|
@@ -489,6 +490,10 @@ function useForma(options) {
|
|
|
489
490
|
}
|
|
490
491
|
}
|
|
491
492
|
},
|
|
493
|
+
// Same function as nextPage — exposed as a separate name so consumers can
|
|
494
|
+
// bind a single "Next" button without risk of accidentally triggering submission.
|
|
495
|
+
// nextPage is already a no-op on the last page.
|
|
496
|
+
handleNext: advanceToNextPage,
|
|
492
497
|
hasNextPage,
|
|
493
498
|
hasPreviousPage,
|
|
494
499
|
canProceed: (() => {
|
|
@@ -517,7 +522,15 @@ function useForma(options) {
|
|
|
517
522
|
return pageErrors.length === 0;
|
|
518
523
|
}
|
|
519
524
|
};
|
|
520
|
-
}, [
|
|
525
|
+
}, [
|
|
526
|
+
spec,
|
|
527
|
+
state.data,
|
|
528
|
+
state.currentPage,
|
|
529
|
+
computed,
|
|
530
|
+
validation,
|
|
531
|
+
visibility,
|
|
532
|
+
fireEvent
|
|
533
|
+
]);
|
|
521
534
|
useEffect(() => {
|
|
522
535
|
const events = pendingEventsRef.current;
|
|
523
536
|
if (events.length === 0) return;
|
|
@@ -621,9 +634,9 @@ function useForma(options) {
|
|
|
621
634
|
}
|
|
622
635
|
const fieldErrors = validation.errors.filter((e) => e.field === path);
|
|
623
636
|
const isTouched = state.touched[path] ?? false;
|
|
624
|
-
const
|
|
625
|
-
const
|
|
626
|
-
const
|
|
637
|
+
const shouldShowErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
|
|
638
|
+
const visibleFieldErrors = shouldShowErrors ? fieldErrors : [];
|
|
639
|
+
const hasVisibleErrors = visibleFieldErrors.length > 0;
|
|
627
640
|
const isRequired = required[path] ?? false;
|
|
628
641
|
const schemaProperty = spec.schema.properties[path];
|
|
629
642
|
const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
|
|
@@ -643,12 +656,13 @@ function useForma(options) {
|
|
|
643
656
|
required: isRequired,
|
|
644
657
|
showRequiredIndicator,
|
|
645
658
|
touched: isTouched,
|
|
646
|
-
errors:
|
|
659
|
+
errors: fieldErrors,
|
|
660
|
+
visibleErrors: visibleFieldErrors,
|
|
647
661
|
onChange: handlers.onChange,
|
|
648
662
|
onBlur: handlers.onBlur,
|
|
649
|
-
// ARIA accessibility attributes
|
|
650
|
-
"aria-invalid":
|
|
651
|
-
"aria-describedby":
|
|
663
|
+
// ARIA accessibility attributes (driven by visibleErrors, not all errors)
|
|
664
|
+
"aria-invalid": hasVisibleErrors || void 0,
|
|
665
|
+
"aria-describedby": hasVisibleErrors ? `${path}-error` : void 0,
|
|
652
666
|
"aria-required": isRequired || void 0,
|
|
653
667
|
// Adorner props (only for adornable field types)
|
|
654
668
|
...adornerProps,
|
|
@@ -719,7 +733,8 @@ function useForma(options) {
|
|
|
719
733
|
showRequiredIndicator: false,
|
|
720
734
|
// Item fields don't show required indicator
|
|
721
735
|
touched: isTouched,
|
|
722
|
-
errors:
|
|
736
|
+
errors: fieldErrors,
|
|
737
|
+
visibleErrors: showErrors ? fieldErrors : [],
|
|
723
738
|
onChange: handlers.onChange,
|
|
724
739
|
onBlur: handlers.onBlur,
|
|
725
740
|
options: visibleOptions
|
|
@@ -871,19 +886,10 @@ function useFormaContext() {
|
|
|
871
886
|
// src/FormRenderer.tsx
|
|
872
887
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
873
888
|
function DefaultLayout({ children, onSubmit, isSubmitting }) {
|
|
874
|
-
return /* @__PURE__ */ jsxs(
|
|
875
|
-
|
|
876
|
-
{
|
|
877
|
-
|
|
878
|
-
e.preventDefault();
|
|
879
|
-
onSubmit();
|
|
880
|
-
},
|
|
881
|
-
children: [
|
|
882
|
-
children,
|
|
883
|
-
/* @__PURE__ */ jsx("button", { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Submitting..." : "Submit" })
|
|
884
|
-
]
|
|
885
|
-
}
|
|
886
|
-
);
|
|
889
|
+
return /* @__PURE__ */ jsxs("form", { onSubmit, children: [
|
|
890
|
+
children,
|
|
891
|
+
/* @__PURE__ */ jsx("button", { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Submitting..." : "Submit" })
|
|
892
|
+
] });
|
|
887
893
|
}
|
|
888
894
|
function DefaultFieldWrapper({
|
|
889
895
|
fieldPath,
|
|
@@ -967,7 +973,7 @@ var FormRenderer = forwardRef(
|
|
|
967
973
|
layout: Layout = DefaultLayout,
|
|
968
974
|
fieldWrapper: FieldWrapper = DefaultFieldWrapper,
|
|
969
975
|
pageWrapper: PageWrapper = DefaultPageWrapper,
|
|
970
|
-
validateOn
|
|
976
|
+
validateOn = "blur"
|
|
971
977
|
} = props;
|
|
972
978
|
const forma = useForma({
|
|
973
979
|
spec,
|
|
@@ -1012,6 +1018,7 @@ var FormRenderer = forwardRef(
|
|
|
1012
1018
|
optionsVisibility: formaOptionsVisibility,
|
|
1013
1019
|
touched: formaTouched,
|
|
1014
1020
|
errors: formaErrors,
|
|
1021
|
+
isSubmitted: formaIsSubmitted,
|
|
1015
1022
|
setFieldValue,
|
|
1016
1023
|
setFieldTouched,
|
|
1017
1024
|
getArrayHelpers
|
|
@@ -1045,6 +1052,8 @@ var FormRenderer = forwardRef(
|
|
|
1045
1052
|
}
|
|
1046
1053
|
const errors = formaErrors.filter((e) => e.field === fieldPath);
|
|
1047
1054
|
const touched = formaTouched[fieldPath] ?? false;
|
|
1055
|
+
const showErrors = validateOn === "change" || validateOn === "blur" && touched || formaIsSubmitted;
|
|
1056
|
+
const visibleErrors = showErrors ? errors : [];
|
|
1048
1057
|
const required = formaRequired[fieldPath] ?? false;
|
|
1049
1058
|
const disabled = formaEnabled[fieldPath] === false;
|
|
1050
1059
|
const schemaProperty = spec.schema.properties[fieldPath];
|
|
@@ -1060,6 +1069,7 @@ var FormRenderer = forwardRef(
|
|
|
1060
1069
|
required,
|
|
1061
1070
|
disabled,
|
|
1062
1071
|
errors,
|
|
1072
|
+
visibleErrors,
|
|
1063
1073
|
onChange: (value) => setFieldValue(fieldPath, value),
|
|
1064
1074
|
onBlur: () => setFieldTouched(fieldPath),
|
|
1065
1075
|
// Convenience properties
|
|
@@ -1143,6 +1153,22 @@ var FormRenderer = forwardRef(
|
|
|
1143
1153
|
minItems,
|
|
1144
1154
|
maxItems
|
|
1145
1155
|
};
|
|
1156
|
+
} else if (fieldType === "matrix" && fieldDef.type === "matrix") {
|
|
1157
|
+
const matrixValue = baseProps.value ?? null;
|
|
1158
|
+
const rows = fieldDef.rows.map((row) => ({
|
|
1159
|
+
id: row.id,
|
|
1160
|
+
label: row.label,
|
|
1161
|
+
visible: formaVisibility[`${fieldPath}.${row.id}`] !== false
|
|
1162
|
+
}));
|
|
1163
|
+
fieldProps = {
|
|
1164
|
+
...baseProps,
|
|
1165
|
+
fieldType: "matrix",
|
|
1166
|
+
value: matrixValue,
|
|
1167
|
+
onChange: baseProps.onChange,
|
|
1168
|
+
rows,
|
|
1169
|
+
columns: fieldDef.columns,
|
|
1170
|
+
multiSelect: fieldDef.multiSelect ?? false
|
|
1171
|
+
};
|
|
1146
1172
|
} else if (fieldType === "display" && fieldDef.type === "display") {
|
|
1147
1173
|
const sourceValue = fieldDef.source ? formaData[fieldDef.source] ?? formaComputed[fieldDef.source] : void 0;
|
|
1148
1174
|
const {
|
|
@@ -1196,6 +1222,8 @@ var FormRenderer = forwardRef(
|
|
|
1196
1222
|
formaOptionsVisibility,
|
|
1197
1223
|
formaTouched,
|
|
1198
1224
|
formaErrors,
|
|
1225
|
+
formaIsSubmitted,
|
|
1226
|
+
validateOn,
|
|
1199
1227
|
setFieldValue,
|
|
1200
1228
|
setFieldTouched,
|
|
1201
1229
|
getArrayHelpers
|
|
@@ -1222,10 +1250,17 @@ var FormRenderer = forwardRef(
|
|
|
1222
1250
|
}
|
|
1223
1251
|
return /* @__PURE__ */ jsx(Fragment, { children: renderedFields });
|
|
1224
1252
|
}, [spec.pages, forma.wizard, PageWrapper, renderedFields]);
|
|
1253
|
+
const handleSubmit = useCallback2(
|
|
1254
|
+
(e) => {
|
|
1255
|
+
e == null ? void 0 : e.preventDefault();
|
|
1256
|
+
forma.submitForm();
|
|
1257
|
+
},
|
|
1258
|
+
[forma.submitForm]
|
|
1259
|
+
);
|
|
1225
1260
|
return /* @__PURE__ */ jsx(FormaContext.Provider, { value: forma, children: /* @__PURE__ */ jsx(
|
|
1226
1261
|
Layout,
|
|
1227
1262
|
{
|
|
1228
|
-
onSubmit:
|
|
1263
|
+
onSubmit: handleSubmit,
|
|
1229
1264
|
isSubmitting: forma.isSubmitting,
|
|
1230
1265
|
isValid: forma.isValid,
|
|
1231
1266
|
children: content
|
|
@@ -1291,6 +1326,7 @@ function FieldRenderer({
|
|
|
1291
1326
|
}
|
|
1292
1327
|
const errors = forma.errors.filter((e) => e.field === fieldPath);
|
|
1293
1328
|
const touched = forma.touched[fieldPath] ?? false;
|
|
1329
|
+
const visibleErrors = touched || forma.isSubmitted ? errors : [];
|
|
1294
1330
|
const required = forma.required[fieldPath] ?? false;
|
|
1295
1331
|
const disabled = forma.enabled[fieldPath] === false;
|
|
1296
1332
|
const schemaProperty = spec.schema.properties[fieldPath];
|
|
@@ -1303,6 +1339,7 @@ function FieldRenderer({
|
|
|
1303
1339
|
required,
|
|
1304
1340
|
disabled,
|
|
1305
1341
|
errors,
|
|
1342
|
+
visibleErrors,
|
|
1306
1343
|
onChange: (value) => forma.setFieldValue(fieldPath, value),
|
|
1307
1344
|
onBlur: () => forma.setFieldTouched(fieldPath),
|
|
1308
1345
|
// Convenience properties
|
|
@@ -1442,13 +1479,33 @@ function FieldRenderer({
|
|
|
1442
1479
|
minItems,
|
|
1443
1480
|
maxItems
|
|
1444
1481
|
};
|
|
1482
|
+
} else if (fieldType === "matrix" && fieldDef.type === "matrix") {
|
|
1483
|
+
const matrixValue = baseProps.value ?? null;
|
|
1484
|
+
const rows = fieldDef.rows.map((row) => ({
|
|
1485
|
+
id: row.id,
|
|
1486
|
+
label: row.label,
|
|
1487
|
+
visible: forma.visibility[`${fieldPath}.${row.id}`] !== false
|
|
1488
|
+
}));
|
|
1489
|
+
fieldProps = {
|
|
1490
|
+
...baseProps,
|
|
1491
|
+
fieldType: "matrix",
|
|
1492
|
+
value: matrixValue,
|
|
1493
|
+
onChange: baseProps.onChange,
|
|
1494
|
+
rows,
|
|
1495
|
+
columns: fieldDef.columns,
|
|
1496
|
+
multiSelect: fieldDef.multiSelect ?? false
|
|
1497
|
+
};
|
|
1445
1498
|
} else if (fieldType === "display" && fieldDef.type === "display") {
|
|
1446
1499
|
const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : void 0;
|
|
1447
1500
|
const {
|
|
1448
1501
|
onChange: _onChange,
|
|
1502
|
+
// omit from display props
|
|
1449
1503
|
value: _value,
|
|
1504
|
+
// omit from display props
|
|
1450
1505
|
...displayBaseProps
|
|
1451
1506
|
} = baseProps;
|
|
1507
|
+
void _onChange;
|
|
1508
|
+
void _value;
|
|
1452
1509
|
fieldProps = {
|
|
1453
1510
|
...displayBaseProps,
|
|
1454
1511
|
fieldType: "display",
|