@dynamic-field-kit/react 1.5.1 → 1.7.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/dist/index.js CHANGED
@@ -32,24 +32,30 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DynamicFormDevTools: () => DynamicFormDevTools,
34
34
  DynamicInput: () => DynamicInput_default,
35
+ FIELD_RENDERER_PROP_KEYS: () => import_core8.FIELD_RENDERER_PROP_KEYS,
35
36
  FieldInput: () => FieldInput_default,
36
- FieldRegistry: () => import_core6.FieldRegistry,
37
+ FieldRegistry: () => import_core7.FieldRegistry,
37
38
  FieldRegistryProvider: () => FieldRegistryProvider,
38
39
  MultiFieldInput: () => MultiFieldInput_default,
40
+ buildFieldRendererProps: () => import_core8.buildFieldRendererProps,
41
+ collectFieldPaths: () => import_core9.collectFieldPaths,
39
42
  defaultRenderersMap: () => defaultRenderersMap,
40
43
  fieldRegistry: () => fieldRegistry,
41
44
  getDefaultRenderer: () => getDefaultRenderer,
45
+ indexGroupPathMap: () => import_core9.indexGroupPathMap,
42
46
  layoutRegistry: () => layoutRegistry,
43
- resolveDisabled: () => import_core7.resolveDisabled,
44
- resolveOptions: () => import_core7.resolveOptions,
45
- resolveReadOnly: () => import_core7.resolveReadOnly,
47
+ makeErrorId: () => import_core8.makeErrorId,
48
+ makeFieldId: () => import_core8.makeFieldId,
49
+ resolveDisabled: () => import_core9.resolveDisabled,
50
+ resolveOptions: () => import_core9.resolveOptions,
51
+ resolveReadOnly: () => import_core9.resolveReadOnly,
46
52
  useDynamicForm: () => useDynamicForm,
47
53
  useFieldRegistry: () => useFieldRegistry,
48
- validateField: () => import_core7.validateField,
49
- validateFieldAsync: () => import_core7.validateFieldAsync,
50
- validateFields: () => import_core7.validateFields,
51
- validateFieldsAsync: () => import_core7.validateFieldsAsync,
52
- validators: () => import_core7.validators
54
+ validateField: () => import_core9.validateField,
55
+ validateFieldAsync: () => import_core9.validateFieldAsync,
56
+ validateFields: () => import_core9.validateFields,
57
+ validateFieldsAsync: () => import_core9.validateFieldsAsync,
58
+ validators: () => import_core9.validators
53
59
  });
54
60
  module.exports = __toCommonJS(index_exports);
55
61
 
@@ -166,6 +172,7 @@ layoutRegistry.register("responsive", ({ children, config }) => {
166
172
  });
167
173
 
168
174
  // src/components/DynamicInput.tsx
175
+ var import_core2 = require("@dynamic-field-kit/core");
169
176
  var import_react3 = __toESM(require("react"));
170
177
 
171
178
  // src/defaultRenderers.tsx
@@ -491,56 +498,42 @@ function useFieldRegistry() {
491
498
  var import_jsx_runtime5 = require("react/jsx-runtime");
492
499
  var DynamicInputInner = ({
493
500
  type,
494
- value,
495
501
  onChange,
496
502
  onBlur,
497
- label,
498
- options,
499
- className,
500
- description,
501
- disabled,
502
- readOnly,
503
- required,
504
- touched,
505
- dirty,
506
- error,
507
- id,
508
- ariaInvalid,
509
- ariaDescribedBy,
510
- ariaRequired,
511
- extraProps
503
+ extraProps,
504
+ onOptionsQuery,
505
+ ...rendererProps
512
506
  }) => {
513
507
  const registry = useFieldRegistry();
514
- const Renderer = (0, import_react3.useMemo)(
515
- () => registry.get(type) || getDefaultRenderer(type),
516
- [registry, type]
517
- );
508
+ const { Renderer, isDefault } = (0, import_react3.useMemo)(() => {
509
+ const registered = registry.get(type);
510
+ return {
511
+ Renderer: registered ?? getDefaultRenderer(type),
512
+ isDefault: !registered
513
+ };
514
+ }, [registry, type]);
518
515
  if (!Renderer) {
519
516
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
520
517
  "Unknown field type: ",
521
518
  type
522
519
  ] });
523
520
  }
524
- return import_react3.default.createElement(Renderer, {
521
+ const { error, id } = rendererProps;
522
+ const control = import_react3.default.createElement(Renderer, {
525
523
  ...extraProps,
526
- value,
524
+ ...rendererProps,
527
525
  onValueChange: onChange,
528
526
  onBlur,
529
- label,
530
- options,
531
- className,
532
- description,
533
- disabled,
534
- readOnly,
535
- required,
536
- touched,
537
- dirty,
538
- error,
539
- id,
540
- ariaInvalid,
541
- ariaDescribedBy,
542
- ariaRequired
527
+ onOptionsQuery
543
528
  });
529
+ const firstError = Array.isArray(error) ? error[0] : error;
530
+ if (!isDefault || !firstError || !id) {
531
+ return control;
532
+ }
533
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
534
+ control,
535
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { id: (0, import_core2.makeErrorId)(id), className: "dfk-field-error", role: "alert", children: firstError })
536
+ ] });
544
537
  };
545
538
  var DynamicInput = /* @__PURE__ */ import_react3.default.memo(
546
539
  DynamicInputInner
@@ -548,16 +541,16 @@ var DynamicInput = /* @__PURE__ */ import_react3.default.memo(
548
541
  var DynamicInput_default = DynamicInput;
549
542
 
550
543
  // src/components/FieldInput.tsx
551
- var import_core4 = require("@dynamic-field-kit/core");
544
+ var import_core5 = require("@dynamic-field-kit/core");
552
545
  var import_react6 = __toESM(require("react"));
553
546
 
554
547
  // src/components/FieldGroupInput.tsx
555
- var import_core3 = require("@dynamic-field-kit/core");
548
+ var import_core4 = require("@dynamic-field-kit/core");
556
549
  var import_react5 = require("react");
557
550
 
558
551
  // src/components/MultiFieldInput.tsx
559
- var import_core2 = require("@dynamic-field-kit/core");
560
- var import_react4 = require("react");
552
+ var import_core3 = require("@dynamic-field-kit/core");
553
+ var import_react4 = __toESM(require("react"));
561
554
  var import_jsx_runtime6 = require("react/jsx-runtime");
562
555
  function resolveLayout(layout) {
563
556
  if (!layout) {
@@ -568,25 +561,46 @@ function resolveLayout(layout) {
568
561
  }
569
562
  return { type: layout.type, config: layout };
570
563
  }
571
- var MultiFieldInput = ({
564
+ var MultiFieldInputInner = ({
572
565
  fieldDescriptions,
573
566
  properties,
574
567
  onChange,
575
568
  layout,
569
+ idPrefix,
570
+ initialProperties,
576
571
  rootData,
577
572
  onValidityChange,
578
- onBlurField
579
- }) => {
573
+ onBlurField,
574
+ touched: touchedProp,
575
+ errors: errorsProp,
576
+ onTouchedChange,
577
+ form
578
+ }, ref) => {
579
+ const effectiveProperties = properties ?? form?.data;
580
+ const effectiveOnChange = onChange ?? form?.handleChange;
581
+ const effectiveOnBlurField = onBlurField ?? form?.handleBlur;
582
+ const controlledTouched = touchedProp ?? form?.touched;
583
+ const effectiveErrors = errorsProp ?? form?.errors;
580
584
  const [data, setData] = (0, import_react4.useState)({});
581
- const [touchedFields, setTouchedFields] = (0, import_react4.useState)(
582
- {}
585
+ const [internalTouched, setInternalTouched] = (0, import_react4.useState)({});
586
+ const fallbackBaselineRef = (0, import_react4.useRef)(
587
+ effectiveProperties
583
588
  );
584
- const initialPropertiesRef = (0, import_react4.useRef)(properties ?? {});
589
+ if (fallbackBaselineRef.current === void 0) {
590
+ fallbackBaselineRef.current = effectiveProperties;
591
+ }
592
+ const baseline = initialProperties ?? form?.baselineValues ?? fallbackBaselineRef.current ?? {};
593
+ const autoId = (0, import_react4.useId)().replace(/[^a-zA-Z0-9]/g, "");
594
+ const effectiveIdPrefix = idPrefix ?? `dfk-${autoId}`;
595
+ const isTouchedControlled = controlledTouched !== void 0;
596
+ const effectiveTouched = controlledTouched ?? internalTouched;
585
597
  (0, import_react4.useEffect)(() => {
586
- if (properties) {
587
- setData((0, import_core2.applyComputedValues)(fieldDescriptions, properties, rootData));
598
+ if (effectiveProperties) {
599
+ setData(
600
+ (0, import_core3.applyComputedValues)(fieldDescriptions, effectiveProperties, rootData)
601
+ );
588
602
  }
589
- }, [properties]);
603
+ }, [effectiveProperties]);
590
604
  const effectiveRoot = rootData ?? data;
591
605
  const visibleFields = (0, import_react4.useMemo)(
592
606
  () => fieldDescriptions.filter(
@@ -596,24 +610,30 @@ var MultiFieldInput = ({
596
610
  );
597
611
  const dataRef = (0, import_react4.useRef)(data);
598
612
  dataRef.current = data;
599
- const onChangeRef = (0, import_react4.useRef)(onChange);
600
- onChangeRef.current = onChange;
613
+ const onChangeRef = (0, import_react4.useRef)(effectiveOnChange);
614
+ onChangeRef.current = effectiveOnChange;
601
615
  const fieldDescriptionsRef = (0, import_react4.useRef)(fieldDescriptions);
602
616
  fieldDescriptionsRef.current = fieldDescriptions;
603
617
  const rootDataRef = (0, import_react4.useRef)(rootData);
604
618
  rootDataRef.current = rootData;
605
619
  const onValidityChangeRef = (0, import_react4.useRef)(onValidityChange);
606
620
  onValidityChangeRef.current = onValidityChange;
607
- const onBlurFieldRef = (0, import_react4.useRef)(onBlurField);
608
- onBlurFieldRef.current = onBlurField;
621
+ const onBlurFieldRef = (0, import_react4.useRef)(effectiveOnBlurField);
622
+ onBlurFieldRef.current = effectiveOnBlurField;
623
+ const onTouchedChangeRef = (0, import_react4.useRef)(onTouchedChange);
624
+ onTouchedChangeRef.current = onTouchedChange;
625
+ const isTouchedControlledRef = (0, import_react4.useRef)(isTouchedControlled);
626
+ isTouchedControlledRef.current = isTouchedControlled;
627
+ const effectiveTouchedRef = (0, import_react4.useRef)(effectiveTouched);
628
+ effectiveTouchedRef.current = effectiveTouched;
609
629
  (0, import_react4.useEffect)(() => {
610
630
  onValidityChangeRef.current?.(
611
- (0, import_core2.validateFields)(fieldDescriptions, data, rootData)
631
+ (0, import_core3.validateFields)(fieldDescriptions, data, rootData)
612
632
  );
613
633
  }, [data, fieldDescriptions, rootData]);
614
634
  const handleValueChangeField = (0, import_react4.useCallback)((value, key) => {
615
635
  const merged = { ...dataRef.current, [key]: value };
616
- const next = (0, import_core2.applyComputedValues)(
636
+ const next = (0, import_core3.applyComputedValues)(
617
637
  fieldDescriptionsRef.current,
618
638
  merged,
619
639
  rootDataRef.current
@@ -622,10 +642,39 @@ var MultiFieldInput = ({
622
642
  setData(next);
623
643
  onChangeRef.current?.(next);
624
644
  }, []);
625
- const handleBlurField = (0, import_react4.useCallback)((key) => {
626
- setTouchedFields((prev) => prev[key] ? prev : { ...prev, [key]: true });
627
- onBlurFieldRef.current?.(key);
645
+ const markTouched = (0, import_react4.useCallback)((key, isTouched) => {
646
+ const current = effectiveTouchedRef.current;
647
+ if (Boolean(current[key]) === isTouched) {
648
+ return;
649
+ }
650
+ const next = { ...current, [key]: isTouched };
651
+ if (!isTouchedControlledRef.current) {
652
+ effectiveTouchedRef.current = next;
653
+ setInternalTouched(next);
654
+ }
655
+ onTouchedChangeRef.current?.(next);
628
656
  }, []);
657
+ const handleBlurField = (0, import_react4.useCallback)(
658
+ (key) => {
659
+ markTouched(key, true);
660
+ onBlurFieldRef.current?.(key);
661
+ },
662
+ [markTouched]
663
+ );
664
+ (0, import_react4.useImperativeHandle)(
665
+ ref,
666
+ () => ({
667
+ resetTouched: () => {
668
+ if (!isTouchedControlledRef.current) {
669
+ effectiveTouchedRef.current = {};
670
+ }
671
+ setInternalTouched({});
672
+ },
673
+ setFieldTouched: (fieldName, isTouched = true) => markTouched(fieldName, isTouched),
674
+ getTouched: () => effectiveTouchedRef.current
675
+ }),
676
+ [markTouched]
677
+ );
629
678
  const { type, config } = resolveLayout(layout);
630
679
  const Layout = layoutRegistry.get(type);
631
680
  if (!Layout) {
@@ -637,22 +686,31 @@ var MultiFieldInput = ({
637
686
  fieldDescription: f,
638
687
  renderInfos: data,
639
688
  rootData: effectiveRoot,
640
- touched: Boolean(touchedFields[f.name]),
641
- dirty: data[f.name] !== initialPropertiesRef.current[f.name],
689
+ idPrefix: effectiveIdPrefix,
690
+ touched: Boolean(effectiveTouched[f.name]),
691
+ touchedMap: (0, import_core3.isFieldGroup)(f) ? effectiveTouched : void 0,
692
+ errors: effectiveErrors,
693
+ dirty: !Object.is(data[f.name], baseline[f.name]),
642
694
  onBlurField: handleBlurField,
643
695
  onValueChangeField: handleValueChangeField
644
696
  },
645
697
  f.name
646
698
  )) });
647
699
  };
700
+ var MultiFieldInput = /* @__PURE__ */ import_react4.default.forwardRef(MultiFieldInputInner);
701
+ MultiFieldInput.displayName = "MultiFieldInput";
648
702
  var MultiFieldInput_default = MultiFieldInput;
649
703
 
650
704
  // src/components/FieldGroupInput.tsx
651
705
  var import_jsx_runtime7 = require("react/jsx-runtime");
706
+ var EMPTY_TOUCHED = Object.freeze({});
652
707
  var FieldGroupInput = ({
653
708
  fieldDescription,
654
709
  items,
655
710
  rootData,
711
+ errors,
712
+ touched,
713
+ onBlurField,
656
714
  onChange
657
715
  }) => {
658
716
  const {
@@ -666,6 +724,14 @@ var FieldGroupInput = ({
666
724
  const addText = addLabel ?? "Add";
667
725
  const removeText = removeLabel ?? "Remove";
668
726
  const groupName = label ?? fieldDescription.name;
727
+ const errorsByItem = (0, import_react5.useMemo)(
728
+ () => (0, import_core4.indexGroupPathMap)(errors, fieldDescription.name),
729
+ [errors, fieldDescription.name]
730
+ );
731
+ const touchedByItem = (0, import_react5.useMemo)(
732
+ () => (0, import_core4.indexGroupPathMap)(touched, fieldDescription.name),
733
+ [touched, fieldDescription.name]
734
+ );
669
735
  const handleItemChange = (0, import_react5.useCallback)(
670
736
  (index, next) => {
671
737
  const nextItems = items.slice();
@@ -675,14 +741,14 @@ var FieldGroupInput = ({
675
741
  [items, onChange]
676
742
  );
677
743
  const handleAdd = (0, import_react5.useCallback)(() => {
678
- if (!(0, import_core3.canAddGroupItem)(fieldDescription, items)) {
744
+ if (!(0, import_core4.canAddGroupItem)(fieldDescription, items)) {
679
745
  return;
680
746
  }
681
- onChange([...items, (0, import_core3.createGroupItem)(fieldDescription)]);
747
+ onChange([...items, (0, import_core4.createGroupItem)(fieldDescription)]);
682
748
  }, [fieldDescription, items, onChange]);
683
749
  const handleRemove = (0, import_react5.useCallback)(
684
750
  (index) => {
685
- if (!(0, import_core3.canRemoveGroupItem)(fieldDescription, items)) {
751
+ if (!(0, import_core4.canRemoveGroupItem)(fieldDescription, items)) {
686
752
  return;
687
753
  }
688
754
  onChange(items.filter((_, i) => i !== index));
@@ -702,6 +768,9 @@ var FieldGroupInput = ({
702
768
  fieldDescriptions: fields,
703
769
  properties: item,
704
770
  rootData,
771
+ errors: errorsByItem?.[index],
772
+ touched: touched === void 0 ? void 0 : touchedByItem?.[index] ?? EMPTY_TOUCHED,
773
+ onBlurField: (key) => onBlurField?.(`${fieldDescription.name}[${index}].${key}`),
705
774
  onChange: (next) => handleItemChange(index, next)
706
775
  }
707
776
  ) }),
@@ -711,7 +780,7 @@ var FieldGroupInput = ({
711
780
  type: "button",
712
781
  "aria-label": `${removeText} ${groupName} ${index + 1}`,
713
782
  onClick: () => handleRemove(index),
714
- disabled: !(0, import_core3.canRemoveGroupItem)(fieldDescription, items),
783
+ disabled: !(0, import_core4.canRemoveGroupItem)(fieldDescription, items),
715
784
  children: removeText
716
785
  }
717
786
  )
@@ -725,7 +794,7 @@ var FieldGroupInput = ({
725
794
  type: "button",
726
795
  "aria-label": `${addText} ${groupName}`,
727
796
  onClick: handleAdd,
728
- disabled: !(0, import_core3.canAddGroupItem)(fieldDescription, items),
797
+ disabled: !(0, import_core4.canAddGroupItem)(fieldDescription, items),
729
798
  children: addText
730
799
  }
731
800
  )
@@ -739,12 +808,50 @@ var FieldInputInner = ({
739
808
  fieldDescription,
740
809
  renderInfos,
741
810
  rootData,
811
+ idPrefix = "dfk-field",
742
812
  touched,
813
+ touchedMap,
743
814
  dirty,
815
+ errors,
744
816
  onBlurField,
745
817
  onValueChangeField
746
818
  }) => {
747
- const { name, type, label, className, description, props, fields, required } = fieldDescription;
819
+ const { name, fields } = fieldDescription;
820
+ const isAsync = (0, import_core5.isAsyncOptions)(fieldDescription);
821
+ const [optionsState, setOptionsState] = (0, import_react6.useState)(
822
+ isAsync ? { status: "idle" } : void 0
823
+ );
824
+ const loaderRef = (0, import_react6.useRef)(void 0);
825
+ const fieldRef = (0, import_react6.useRef)(fieldDescription);
826
+ fieldRef.current = fieldDescription;
827
+ const ensureLoader = (0, import_react6.useCallback)(() => {
828
+ if (!(0, import_core5.isAsyncOptions)(fieldRef.current)) {
829
+ return void 0;
830
+ }
831
+ if (!loaderRef.current) {
832
+ loaderRef.current = (0, import_core5.createOptionsLoader)(
833
+ fieldRef.current,
834
+ setOptionsState
835
+ );
836
+ }
837
+ return loaderRef.current;
838
+ }, []);
839
+ (0, import_react6.useEffect)(
840
+ () => () => {
841
+ loaderRef.current?.dispose();
842
+ loaderRef.current = void 0;
843
+ },
844
+ []
845
+ );
846
+ (0, import_react6.useEffect)(() => {
847
+ ensureLoader()?.update(renderInfos, rootData);
848
+ });
849
+ const handleOptionsQuery = (0, import_react6.useCallback)(
850
+ (query) => {
851
+ ensureLoader()?.setQuery(query);
852
+ },
853
+ [ensureLoader]
854
+ );
748
855
  const handleChange = (0, import_react6.useCallback)(
749
856
  (v) => onValueChangeField(v, name),
750
857
  [onValueChangeField, name]
@@ -761,51 +868,40 @@ var FieldInputInner = ({
761
868
  fieldDescription,
762
869
  items,
763
870
  rootData,
871
+ errors,
872
+ touched: touchedMap,
873
+ onBlurField,
764
874
  onChange: handleChange
765
875
  }
766
876
  );
767
877
  }
768
- const effectiveDisabled = (0, import_core4.resolveDisabled)(
878
+ const rendererProps = (0, import_core5.buildFieldRendererProps)({
769
879
  fieldDescription,
770
- renderInfos,
771
- rootData
772
- );
773
- const readOnly = (0, import_core4.resolveReadOnly)(fieldDescription, renderInfos, rootData);
774
- const resolvedOptionsList = (0, import_core4.resolveOptions)(
775
- fieldDescription,
776
- renderInfos,
777
- rootData
778
- );
779
- const errors = effectiveDisabled ? [] : (0, import_core4.validateField)(fieldDescription, renderInfos[name], renderInfos, rootData);
780
- const error = errors.length > 0 ? errors : void 0;
781
- const fieldId = `dfk-field-${name}`;
880
+ data: renderInfos,
881
+ rootData,
882
+ id: (0, import_core5.makeFieldId)(fieldDescription, idPrefix),
883
+ touched,
884
+ dirty,
885
+ validationErrors: errors === void 0 ? void 0 : errors[name] ?? [],
886
+ optionsState
887
+ });
782
888
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
783
889
  DynamicInput_default,
784
890
  {
785
- id: fieldId,
786
- type,
787
- label,
788
- value: renderInfos[name],
789
- options: resolvedOptionsList,
790
- className,
791
- description,
792
- disabled: effectiveDisabled,
793
- readOnly,
794
- required,
795
- touched,
796
- dirty,
797
- error,
798
- ariaInvalid: Boolean(error),
799
- ariaRequired: Boolean(required),
800
- extraProps: props,
891
+ ...rendererProps,
892
+ description: rendererProps.description,
801
893
  onChange: handleChange,
802
- onBlur: handleBlur
894
+ onBlur: handleBlur,
895
+ onOptionsQuery: isAsync ? handleOptionsQuery : void 0
803
896
  }
804
897
  );
805
898
  };
806
899
  var FieldInput = /* @__PURE__ */ import_react6.default.memo(FieldInputInner, (prev, next) => {
807
900
  const name = prev.fieldDescription.name;
808
- return prev.fieldDescription === next.fieldDescription && prev.onValueChangeField === next.onValueChangeField && prev.onBlurField === next.onBlurField && prev.rootData === next.rootData && prev.touched === next.touched && prev.dirty === next.dirty && prev.renderInfos[name] === next.renderInfos[name];
901
+ if ((0, import_core5.isAsyncOptions)(prev.fieldDescription) && prev.renderInfos !== next.renderInfos) {
902
+ return false;
903
+ }
904
+ return prev.fieldDescription === next.fieldDescription && prev.onValueChangeField === next.onValueChangeField && prev.onBlurField === next.onBlurField && prev.rootData === next.rootData && prev.idPrefix === next.idPrefix && prev.touched === next.touched && prev.touchedMap === next.touchedMap && prev.dirty === next.dirty && prev.errors === next.errors && prev.renderInfos[name] === next.renderInfos[name];
809
905
  });
810
906
  var FieldInput_default = FieldInput;
811
907
 
@@ -1010,38 +1106,134 @@ var DynamicFormDevTools = ({
1010
1106
  };
1011
1107
 
1012
1108
  // src/useDynamicForm.ts
1013
- var import_core5 = require("@dynamic-field-kit/core");
1109
+ var import_core6 = require("@dynamic-field-kit/core");
1014
1110
  var import_react8 = require("react");
1111
+ function sameStringMap(left, right) {
1112
+ const keys = Object.keys(left);
1113
+ return keys.length === Object.keys(right).length && keys.every(
1114
+ (key) => left[key]?.length === right[key]?.length && left[key]?.every((value, index) => value === right[key]?.[index])
1115
+ );
1116
+ }
1117
+ function sameValidationResult(left, right) {
1118
+ return left.valid === right.valid && left.complete === right.complete && left.status === right.status && (left.pending ?? []).join("\0") === (right.pending ?? []).join("\0") && sameStringMap(left.errors, right.errors);
1119
+ }
1015
1120
  function useDynamicForm({
1016
1121
  fields,
1017
1122
  initialValues = {},
1018
1123
  validateOnBlur = true,
1019
- validateOnChange = false
1124
+ validateOnChange = false,
1125
+ messages
1020
1126
  }) {
1127
+ const validationContextRef = (0, import_react8.useRef)({
1128
+ t: (0, import_core6.createMessageResolver)(messages)
1129
+ });
1130
+ validationContextRef.current.t = (0, import_core6.createMessageResolver)(messages);
1021
1131
  const [data, setData] = (0, import_react8.useState)(
1022
- () => (0, import_core5.applyComputedValues)(fields, initialValues)
1132
+ () => (0, import_core6.applyComputedValues)(fields, initialValues)
1023
1133
  );
1024
1134
  const [errors, setErrors] = (0, import_react8.useState)({});
1025
1135
  const [isDirty, setIsDirty] = (0, import_react8.useState)(false);
1026
1136
  const [touched, setTouched] = (0, import_react8.useState)({});
1027
1137
  const [isSubmitting, setIsSubmitting] = (0, import_react8.useState)(false);
1028
1138
  const [isSubmitted, setIsSubmitted] = (0, import_react8.useState)(false);
1139
+ const [validationResult, setValidationResult] = (0, import_react8.useState)(
1140
+ () => (0, import_core6.validateFields)(fields, data, void 0, validationContextRef.current)
1141
+ );
1142
+ const [isValidating, setIsValidating] = (0, import_react8.useState)(false);
1143
+ const validationRunRef = (0, import_react8.useRef)(0);
1144
+ const validationAbortRef = (0, import_react8.useRef)(void 0);
1145
+ const submitRunRef = (0, import_react8.useRef)(0);
1146
+ const submitAbortRef = (0, import_react8.useRef)(void 0);
1147
+ const dataRef = (0, import_react8.useRef)(data);
1148
+ dataRef.current = data;
1149
+ const baselineRef = (0, import_react8.useRef)(dataRef.current);
1150
+ const [baselineValues, setBaselineValues] = (0, import_react8.useState)(
1151
+ () => baselineRef.current
1152
+ );
1153
+ const commitSyncResult = (0, import_react8.useCallback)((res) => {
1154
+ setValidationResult(
1155
+ (previous) => sameValidationResult(previous, res) ? previous : res
1156
+ );
1157
+ return res.valid;
1158
+ }, []);
1159
+ const lastValidatedRef = (0, import_react8.useRef)(void 0);
1160
+ const fieldsRef = (0, import_react8.useRef)(fields);
1161
+ if (fieldsRef.current !== fields) {
1162
+ fieldsRef.current = fields;
1163
+ lastValidatedRef.current = void 0;
1164
+ }
1165
+ (0, import_react8.useEffect)(() => {
1166
+ if (lastValidatedRef.current === data) {
1167
+ return;
1168
+ }
1169
+ lastValidatedRef.current = data;
1170
+ commitSyncResult(
1171
+ (0, import_core6.validateFields)(fields, data, void 0, validationContextRef.current)
1172
+ );
1173
+ }, [fields, data, commitSyncResult]);
1174
+ (0, import_react8.useEffect)(
1175
+ () => () => {
1176
+ validationAbortRef.current?.abort();
1177
+ submitAbortRef.current?.abort();
1178
+ },
1179
+ []
1180
+ );
1029
1181
  const validate = (0, import_react8.useCallback)(() => {
1030
- const res = (0, import_core5.validateFields)(fields, data);
1182
+ const res = (0, import_core6.validateFields)(
1183
+ fields,
1184
+ data,
1185
+ void 0,
1186
+ validationContextRef.current
1187
+ );
1031
1188
  setErrors(res.errors);
1032
- return res.valid;
1189
+ return commitSyncResult(res);
1190
+ }, [fields, data, commitSyncResult]);
1191
+ const validateAsync = (0, import_react8.useCallback)(async () => {
1192
+ const run = ++validationRunRef.current;
1193
+ validationAbortRef.current?.abort();
1194
+ const controller = new AbortController();
1195
+ validationAbortRef.current = controller;
1196
+ const snapshot = data;
1197
+ setIsValidating(true);
1198
+ try {
1199
+ const res = await (0, import_core6.validateFieldsAsync)(fields, snapshot, snapshot, {
1200
+ ...validationContextRef.current,
1201
+ signal: controller.signal
1202
+ });
1203
+ if (run !== validationRunRef.current || dataRef.current !== snapshot) {
1204
+ return res.valid;
1205
+ }
1206
+ setErrors(res.errors);
1207
+ setValidationResult(res);
1208
+ return res.valid;
1209
+ } finally {
1210
+ if (run === validationRunRef.current) {
1211
+ setIsValidating(false);
1212
+ }
1213
+ }
1033
1214
  }, [fields, data]);
1034
1215
  const handleChange = (0, import_react8.useCallback)(
1035
1216
  (newData) => {
1036
- const next = (0, import_core5.applyComputedValues)(fields, newData);
1217
+ const next = (0, import_core6.applyComputedValues)(fields, newData);
1037
1218
  setData(next);
1219
+ dataRef.current = next;
1038
1220
  setIsDirty(true);
1221
+ validationAbortRef.current?.abort();
1222
+ validationRunRef.current += 1;
1223
+ setIsValidating(false);
1224
+ lastValidatedRef.current = next;
1225
+ const res = (0, import_core6.validateFields)(
1226
+ fields,
1227
+ next,
1228
+ void 0,
1229
+ validationContextRef.current
1230
+ );
1231
+ commitSyncResult(res);
1039
1232
  if (validateOnChange) {
1040
- const res = (0, import_core5.validateFields)(fields, next);
1041
1233
  setErrors(res.errors);
1042
1234
  }
1043
1235
  },
1044
- [fields, validateOnChange]
1236
+ [fields, validateOnChange, commitSyncResult]
1045
1237
  );
1046
1238
  const setFieldValue = (0, import_react8.useCallback)(
1047
1239
  (name, value) => {
@@ -1052,26 +1244,57 @@ function useDynamicForm({
1052
1244
  const setFieldTouched = (0, import_react8.useCallback)((name, isTouched = true) => {
1053
1245
  setTouched((prev) => ({ ...prev, [name]: isTouched }));
1054
1246
  }, []);
1247
+ const touchAll = (0, import_react8.useCallback)(() => {
1248
+ setTouched(
1249
+ Object.fromEntries(
1250
+ (0, import_core6.collectFieldPaths)(fields, data).map((path) => [path, true])
1251
+ )
1252
+ );
1253
+ }, [fields, data]);
1254
+ const resetTouched = (0, import_react8.useCallback)(() => setTouched({}), []);
1255
+ const getDirtyValues = (0, import_react8.useCallback)(() => {
1256
+ const baseline = baselineRef.current;
1257
+ const current = dataRef.current;
1258
+ const dirty = {};
1259
+ for (const key of Object.keys(current)) {
1260
+ if (!Object.is(current[key], baseline[key])) {
1261
+ dirty[key] = current[key];
1262
+ }
1263
+ }
1264
+ return dirty;
1265
+ }, []);
1055
1266
  const handleBlur = (0, import_react8.useCallback)(
1056
1267
  (fieldName) => {
1057
1268
  setFieldTouched(fieldName, true);
1058
1269
  if (validateOnBlur) {
1059
- const res = (0, import_core5.validateFields)(fields, data);
1270
+ const res = (0, import_core6.validateFields)(
1271
+ fields,
1272
+ data,
1273
+ void 0,
1274
+ validationContextRef.current
1275
+ );
1060
1276
  setErrors(res.errors);
1277
+ commitSyncResult(res);
1061
1278
  }
1062
1279
  },
1063
- [fields, data, validateOnBlur, setFieldTouched]
1280
+ [fields, data, validateOnBlur, setFieldTouched, commitSyncResult]
1064
1281
  );
1065
1282
  const reset = (0, import_react8.useCallback)(
1066
1283
  (newValues) => {
1067
1284
  const seed = newValues ?? initialValues;
1068
- const next = (0, import_core5.applyComputedValues)(fields, seed);
1285
+ const next = (0, import_core6.applyComputedValues)(fields, seed);
1069
1286
  setData(next);
1287
+ dataRef.current = next;
1288
+ baselineRef.current = next;
1289
+ setBaselineValues(next);
1070
1290
  setErrors({});
1071
1291
  setIsDirty(false);
1072
1292
  setTouched({});
1073
1293
  setIsSubmitting(false);
1074
1294
  setIsSubmitted(false);
1295
+ validationAbortRef.current?.abort();
1296
+ validationRunRef.current += 1;
1297
+ setIsValidating(false);
1075
1298
  },
1076
1299
  [fields, initialValues]
1077
1300
  );
@@ -1081,56 +1304,103 @@ function useDynamicForm({
1081
1304
  e.preventDefault();
1082
1305
  }
1083
1306
  setIsSubmitting(true);
1307
+ const submitRun = ++submitRunRef.current;
1084
1308
  try {
1085
- const res = (0, import_core5.validateFields)(fields, data);
1086
- setErrors(res.errors);
1309
+ touchAll();
1310
+ const run = ++validationRunRef.current;
1311
+ validationAbortRef.current?.abort();
1312
+ submitAbortRef.current?.abort();
1313
+ const controller = new AbortController();
1314
+ submitAbortRef.current = controller;
1315
+ const snapshot = data;
1316
+ setIsValidating(true);
1317
+ const res = await (0, import_core6.validateFieldsAsync)(fields, snapshot, snapshot, {
1318
+ ...validationContextRef.current,
1319
+ signal: controller.signal
1320
+ });
1321
+ if (submitRun !== submitRunRef.current) {
1322
+ return;
1323
+ }
1324
+ if (dataRef.current === snapshot && run === validationRunRef.current) {
1325
+ setErrors(res.errors);
1326
+ setValidationResult(res);
1327
+ } else {
1328
+ const live = (0, import_core6.validateFields)(
1329
+ fields,
1330
+ dataRef.current,
1331
+ void 0,
1332
+ validationContextRef.current
1333
+ );
1334
+ setErrors(live.errors);
1335
+ commitSyncResult(live);
1336
+ }
1087
1337
  setIsSubmitted(true);
1088
1338
  if (res.valid) {
1089
- await onValid(data);
1339
+ await onValid(snapshot);
1090
1340
  } else if (onInvalid) {
1091
1341
  onInvalid(res.errors);
1092
1342
  }
1093
1343
  } finally {
1344
+ if (submitRun === submitRunRef.current) {
1345
+ setIsValidating(false);
1346
+ }
1094
1347
  setIsSubmitting(false);
1095
1348
  }
1096
1349
  },
1097
- [fields, data]
1350
+ [fields, data, touchAll, commitSyncResult]
1098
1351
  );
1099
- const isValid = Object.keys(errors).length === 0;
1100
1352
  return {
1101
1353
  data,
1102
1354
  errors,
1103
- isValid,
1355
+ isValid: validationResult.valid,
1356
+ isValidating,
1357
+ // Matches Vue and Angular: a run still in flight is not complete, whatever
1358
+ // the last finished pass concluded.
1359
+ isValidationComplete: validationResult.complete && !isValidating,
1360
+ validationStatus: isValidating ? "pending" : validationResult.status,
1104
1361
  isDirty,
1362
+ baselineValues,
1363
+ getDirtyValues,
1105
1364
  isSubmitting,
1106
1365
  isSubmitted,
1107
1366
  touched,
1108
1367
  setData,
1109
1368
  setFieldValue,
1110
1369
  setFieldTouched,
1370
+ setTouched,
1371
+ touchAll,
1372
+ resetTouched,
1111
1373
  handleChange,
1112
1374
  handleBlur,
1113
1375
  reset,
1114
1376
  validate,
1377
+ validateAsync,
1115
1378
  handleSubmit
1116
1379
  };
1117
1380
  }
1118
1381
 
1119
1382
  // src/index.ts
1120
- var import_core6 = require("@dynamic-field-kit/core");
1121
1383
  var import_core7 = require("@dynamic-field-kit/core");
1384
+ var import_core8 = require("@dynamic-field-kit/core");
1385
+ var import_core9 = require("@dynamic-field-kit/core");
1122
1386
  // Annotate the CommonJS export names for ESM import in node:
1123
1387
  0 && (module.exports = {
1124
1388
  DynamicFormDevTools,
1125
1389
  DynamicInput,
1390
+ FIELD_RENDERER_PROP_KEYS,
1126
1391
  FieldInput,
1127
1392
  FieldRegistry,
1128
1393
  FieldRegistryProvider,
1129
1394
  MultiFieldInput,
1395
+ buildFieldRendererProps,
1396
+ collectFieldPaths,
1130
1397
  defaultRenderersMap,
1131
1398
  fieldRegistry,
1132
1399
  getDefaultRenderer,
1400
+ indexGroupPathMap,
1133
1401
  layoutRegistry,
1402
+ makeErrorId,
1403
+ makeFieldId,
1134
1404
  resolveDisabled,
1135
1405
  resolveOptions,
1136
1406
  resolveReadOnly,