@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.mjs CHANGED
@@ -111,6 +111,9 @@ layoutRegistry.register("responsive", ({ children, config }) => {
111
111
  });
112
112
 
113
113
  // src/components/DynamicInput.tsx
114
+ import {
115
+ makeErrorId
116
+ } from "@dynamic-field-kit/core";
114
117
  import React3, { useMemo } from "react";
115
118
 
116
119
  // src/defaultRenderers.tsx
@@ -435,59 +438,45 @@ function useFieldRegistry() {
435
438
  }
436
439
 
437
440
  // src/components/DynamicInput.tsx
438
- import { jsxs as jsxs3 } from "react/jsx-runtime";
441
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
439
442
  var DynamicInputInner = ({
440
443
  type,
441
- value,
442
444
  onChange,
443
445
  onBlur,
444
- label,
445
- options,
446
- className,
447
- description,
448
- disabled,
449
- readOnly,
450
- required,
451
- touched,
452
- dirty,
453
- error,
454
- id,
455
- ariaInvalid,
456
- ariaDescribedBy,
457
- ariaRequired,
458
- extraProps
446
+ extraProps,
447
+ onOptionsQuery,
448
+ ...rendererProps
459
449
  }) => {
460
450
  const registry = useFieldRegistry();
461
- const Renderer = useMemo(
462
- () => registry.get(type) || getDefaultRenderer(type),
463
- [registry, type]
464
- );
451
+ const { Renderer, isDefault } = useMemo(() => {
452
+ const registered = registry.get(type);
453
+ return {
454
+ Renderer: registered ?? getDefaultRenderer(type),
455
+ isDefault: !registered
456
+ };
457
+ }, [registry, type]);
465
458
  if (!Renderer) {
466
459
  return /* @__PURE__ */ jsxs3("div", { children: [
467
460
  "Unknown field type: ",
468
461
  type
469
462
  ] });
470
463
  }
471
- return React3.createElement(Renderer, {
464
+ const { error, id } = rendererProps;
465
+ const control = React3.createElement(Renderer, {
472
466
  ...extraProps,
473
- value,
467
+ ...rendererProps,
474
468
  onValueChange: onChange,
475
469
  onBlur,
476
- label,
477
- options,
478
- className,
479
- description,
480
- disabled,
481
- readOnly,
482
- required,
483
- touched,
484
- dirty,
485
- error,
486
- id,
487
- ariaInvalid,
488
- ariaDescribedBy,
489
- ariaRequired
470
+ onOptionsQuery
490
471
  });
472
+ const firstError = Array.isArray(error) ? error[0] : error;
473
+ if (!isDefault || !firstError || !id) {
474
+ return control;
475
+ }
476
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
477
+ control,
478
+ /* @__PURE__ */ jsx5("div", { id: makeErrorId(id), className: "dfk-field-error", role: "alert", children: firstError })
479
+ ] });
491
480
  };
492
481
  var DynamicInput = /* @__PURE__ */ React3.memo(
493
482
  DynamicInputInner
@@ -496,34 +485,38 @@ var DynamicInput_default = DynamicInput;
496
485
 
497
486
  // src/components/FieldInput.tsx
498
487
  import {
499
- resolveDisabled,
500
- resolveOptions,
501
- resolveReadOnly,
502
- validateField
488
+ buildFieldRendererProps,
489
+ createOptionsLoader,
490
+ isAsyncOptions,
491
+ makeFieldId
503
492
  } from "@dynamic-field-kit/core";
504
- import React6, { useCallback as useCallback3 } from "react";
493
+ import React6, { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
505
494
 
506
495
  // src/components/FieldGroupInput.tsx
507
496
  import {
508
497
  canAddGroupItem,
509
498
  canRemoveGroupItem,
510
- createGroupItem
499
+ createGroupItem,
500
+ indexGroupPathMap
511
501
  } from "@dynamic-field-kit/core";
512
- import { useCallback as useCallback2 } from "react";
502
+ import { useCallback as useCallback2, useMemo as useMemo3 } from "react";
513
503
 
514
504
  // src/components/MultiFieldInput.tsx
515
505
  import {
516
506
  applyComputedValues,
507
+ isFieldGroup,
517
508
  validateFields
518
509
  } from "@dynamic-field-kit/core";
519
- import {
510
+ import React4, {
520
511
  useCallback,
521
512
  useEffect as useEffect2,
513
+ useId,
514
+ useImperativeHandle,
522
515
  useMemo as useMemo2,
523
516
  useRef,
524
517
  useState as useState2
525
518
  } from "react";
526
- import { jsx as jsx5 } from "react/jsx-runtime";
519
+ import { jsx as jsx6 } from "react/jsx-runtime";
527
520
  function resolveLayout(layout) {
528
521
  if (!layout) {
529
522
  return { type: "column", config: {} };
@@ -533,25 +526,46 @@ function resolveLayout(layout) {
533
526
  }
534
527
  return { type: layout.type, config: layout };
535
528
  }
536
- var MultiFieldInput = ({
529
+ var MultiFieldInputInner = ({
537
530
  fieldDescriptions,
538
531
  properties,
539
532
  onChange,
540
533
  layout,
534
+ idPrefix,
535
+ initialProperties,
541
536
  rootData,
542
537
  onValidityChange,
543
- onBlurField
544
- }) => {
538
+ onBlurField,
539
+ touched: touchedProp,
540
+ errors: errorsProp,
541
+ onTouchedChange,
542
+ form
543
+ }, ref) => {
544
+ const effectiveProperties = properties ?? form?.data;
545
+ const effectiveOnChange = onChange ?? form?.handleChange;
546
+ const effectiveOnBlurField = onBlurField ?? form?.handleBlur;
547
+ const controlledTouched = touchedProp ?? form?.touched;
548
+ const effectiveErrors = errorsProp ?? form?.errors;
545
549
  const [data, setData] = useState2({});
546
- const [touchedFields, setTouchedFields] = useState2(
547
- {}
550
+ const [internalTouched, setInternalTouched] = useState2({});
551
+ const fallbackBaselineRef = useRef(
552
+ effectiveProperties
548
553
  );
549
- const initialPropertiesRef = useRef(properties ?? {});
554
+ if (fallbackBaselineRef.current === void 0) {
555
+ fallbackBaselineRef.current = effectiveProperties;
556
+ }
557
+ const baseline = initialProperties ?? form?.baselineValues ?? fallbackBaselineRef.current ?? {};
558
+ const autoId = useId().replace(/[^a-zA-Z0-9]/g, "");
559
+ const effectiveIdPrefix = idPrefix ?? `dfk-${autoId}`;
560
+ const isTouchedControlled = controlledTouched !== void 0;
561
+ const effectiveTouched = controlledTouched ?? internalTouched;
550
562
  useEffect2(() => {
551
- if (properties) {
552
- setData(applyComputedValues(fieldDescriptions, properties, rootData));
563
+ if (effectiveProperties) {
564
+ setData(
565
+ applyComputedValues(fieldDescriptions, effectiveProperties, rootData)
566
+ );
553
567
  }
554
- }, [properties]);
568
+ }, [effectiveProperties]);
555
569
  const effectiveRoot = rootData ?? data;
556
570
  const visibleFields = useMemo2(
557
571
  () => fieldDescriptions.filter(
@@ -561,16 +575,22 @@ var MultiFieldInput = ({
561
575
  );
562
576
  const dataRef = useRef(data);
563
577
  dataRef.current = data;
564
- const onChangeRef = useRef(onChange);
565
- onChangeRef.current = onChange;
578
+ const onChangeRef = useRef(effectiveOnChange);
579
+ onChangeRef.current = effectiveOnChange;
566
580
  const fieldDescriptionsRef = useRef(fieldDescriptions);
567
581
  fieldDescriptionsRef.current = fieldDescriptions;
568
582
  const rootDataRef = useRef(rootData);
569
583
  rootDataRef.current = rootData;
570
584
  const onValidityChangeRef = useRef(onValidityChange);
571
585
  onValidityChangeRef.current = onValidityChange;
572
- const onBlurFieldRef = useRef(onBlurField);
573
- onBlurFieldRef.current = onBlurField;
586
+ const onBlurFieldRef = useRef(effectiveOnBlurField);
587
+ onBlurFieldRef.current = effectiveOnBlurField;
588
+ const onTouchedChangeRef = useRef(onTouchedChange);
589
+ onTouchedChangeRef.current = onTouchedChange;
590
+ const isTouchedControlledRef = useRef(isTouchedControlled);
591
+ isTouchedControlledRef.current = isTouchedControlled;
592
+ const effectiveTouchedRef = useRef(effectiveTouched);
593
+ effectiveTouchedRef.current = effectiveTouched;
574
594
  useEffect2(() => {
575
595
  onValidityChangeRef.current?.(
576
596
  validateFields(fieldDescriptions, data, rootData)
@@ -587,37 +607,75 @@ var MultiFieldInput = ({
587
607
  setData(next);
588
608
  onChangeRef.current?.(next);
589
609
  }, []);
590
- const handleBlurField = useCallback((key) => {
591
- setTouchedFields((prev) => prev[key] ? prev : { ...prev, [key]: true });
592
- onBlurFieldRef.current?.(key);
610
+ const markTouched = useCallback((key, isTouched) => {
611
+ const current = effectiveTouchedRef.current;
612
+ if (Boolean(current[key]) === isTouched) {
613
+ return;
614
+ }
615
+ const next = { ...current, [key]: isTouched };
616
+ if (!isTouchedControlledRef.current) {
617
+ effectiveTouchedRef.current = next;
618
+ setInternalTouched(next);
619
+ }
620
+ onTouchedChangeRef.current?.(next);
593
621
  }, []);
622
+ const handleBlurField = useCallback(
623
+ (key) => {
624
+ markTouched(key, true);
625
+ onBlurFieldRef.current?.(key);
626
+ },
627
+ [markTouched]
628
+ );
629
+ useImperativeHandle(
630
+ ref,
631
+ () => ({
632
+ resetTouched: () => {
633
+ if (!isTouchedControlledRef.current) {
634
+ effectiveTouchedRef.current = {};
635
+ }
636
+ setInternalTouched({});
637
+ },
638
+ setFieldTouched: (fieldName, isTouched = true) => markTouched(fieldName, isTouched),
639
+ getTouched: () => effectiveTouchedRef.current
640
+ }),
641
+ [markTouched]
642
+ );
594
643
  const { type, config } = resolveLayout(layout);
595
644
  const Layout = layoutRegistry.get(type);
596
645
  if (!Layout) {
597
646
  throw new Error(`Unknown layout: ${type}`);
598
647
  }
599
- return /* @__PURE__ */ jsx5(Layout, { config, children: visibleFields.map((f) => /* @__PURE__ */ jsx5(
648
+ return /* @__PURE__ */ jsx6(Layout, { config, children: visibleFields.map((f) => /* @__PURE__ */ jsx6(
600
649
  FieldInput_default,
601
650
  {
602
651
  fieldDescription: f,
603
652
  renderInfos: data,
604
653
  rootData: effectiveRoot,
605
- touched: Boolean(touchedFields[f.name]),
606
- dirty: data[f.name] !== initialPropertiesRef.current[f.name],
654
+ idPrefix: effectiveIdPrefix,
655
+ touched: Boolean(effectiveTouched[f.name]),
656
+ touchedMap: isFieldGroup(f) ? effectiveTouched : void 0,
657
+ errors: effectiveErrors,
658
+ dirty: !Object.is(data[f.name], baseline[f.name]),
607
659
  onBlurField: handleBlurField,
608
660
  onValueChangeField: handleValueChangeField
609
661
  },
610
662
  f.name
611
663
  )) });
612
664
  };
665
+ var MultiFieldInput = /* @__PURE__ */ React4.forwardRef(MultiFieldInputInner);
666
+ MultiFieldInput.displayName = "MultiFieldInput";
613
667
  var MultiFieldInput_default = MultiFieldInput;
614
668
 
615
669
  // src/components/FieldGroupInput.tsx
616
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
670
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
671
+ var EMPTY_TOUCHED = Object.freeze({});
617
672
  var FieldGroupInput = ({
618
673
  fieldDescription,
619
674
  items,
620
675
  rootData,
676
+ errors,
677
+ touched,
678
+ onBlurField,
621
679
  onChange
622
680
  }) => {
623
681
  const {
@@ -631,6 +689,14 @@ var FieldGroupInput = ({
631
689
  const addText = addLabel ?? "Add";
632
690
  const removeText = removeLabel ?? "Remove";
633
691
  const groupName = label ?? fieldDescription.name;
692
+ const errorsByItem = useMemo3(
693
+ () => indexGroupPathMap(errors, fieldDescription.name),
694
+ [errors, fieldDescription.name]
695
+ );
696
+ const touchedByItem = useMemo3(
697
+ () => indexGroupPathMap(touched, fieldDescription.name),
698
+ [touched, fieldDescription.name]
699
+ );
634
700
  const handleItemChange = useCallback2(
635
701
  (index, next) => {
636
702
  const nextItems = items.slice();
@@ -655,22 +721,25 @@ var FieldGroupInput = ({
655
721
  [fieldDescription, items, onChange]
656
722
  );
657
723
  return /* @__PURE__ */ jsxs4("div", { className: fieldDescription.className, children: [
658
- label && /* @__PURE__ */ jsx6("div", { children: label }),
724
+ label && /* @__PURE__ */ jsx7("div", { children: label }),
659
725
  items.map((item, index) => /* @__PURE__ */ jsxs4(
660
726
  "div",
661
727
  {
662
728
  style: { display: "flex", alignItems: "flex-start", gap: 8 },
663
729
  children: [
664
- /* @__PURE__ */ jsx6("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsx6(
730
+ /* @__PURE__ */ jsx7("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsx7(
665
731
  MultiFieldInput_default,
666
732
  {
667
733
  fieldDescriptions: fields,
668
734
  properties: item,
669
735
  rootData,
736
+ errors: errorsByItem?.[index],
737
+ touched: touched === void 0 ? void 0 : touchedByItem?.[index] ?? EMPTY_TOUCHED,
738
+ onBlurField: (key) => onBlurField?.(`${fieldDescription.name}[${index}].${key}`),
670
739
  onChange: (next) => handleItemChange(index, next)
671
740
  }
672
741
  ) }),
673
- /* @__PURE__ */ jsx6(
742
+ /* @__PURE__ */ jsx7(
674
743
  "button",
675
744
  {
676
745
  type: "button",
@@ -684,7 +753,7 @@ var FieldGroupInput = ({
684
753
  },
685
754
  itemKey(item, index)
686
755
  )),
687
- /* @__PURE__ */ jsx6(
756
+ /* @__PURE__ */ jsx7(
688
757
  "button",
689
758
  {
690
759
  type: "button",
@@ -699,17 +768,55 @@ var FieldGroupInput = ({
699
768
  var FieldGroupInput_default = FieldGroupInput;
700
769
 
701
770
  // src/components/FieldInput.tsx
702
- import { jsx as jsx7 } from "react/jsx-runtime";
771
+ import { jsx as jsx8 } from "react/jsx-runtime";
703
772
  var FieldInputInner = ({
704
773
  fieldDescription,
705
774
  renderInfos,
706
775
  rootData,
776
+ idPrefix = "dfk-field",
707
777
  touched,
778
+ touchedMap,
708
779
  dirty,
780
+ errors,
709
781
  onBlurField,
710
782
  onValueChangeField
711
783
  }) => {
712
- const { name, type, label, className, description, props, fields, required } = fieldDescription;
784
+ const { name, fields } = fieldDescription;
785
+ const isAsync = isAsyncOptions(fieldDescription);
786
+ const [optionsState, setOptionsState] = useState3(
787
+ isAsync ? { status: "idle" } : void 0
788
+ );
789
+ const loaderRef = useRef2(void 0);
790
+ const fieldRef = useRef2(fieldDescription);
791
+ fieldRef.current = fieldDescription;
792
+ const ensureLoader = useCallback3(() => {
793
+ if (!isAsyncOptions(fieldRef.current)) {
794
+ return void 0;
795
+ }
796
+ if (!loaderRef.current) {
797
+ loaderRef.current = createOptionsLoader(
798
+ fieldRef.current,
799
+ setOptionsState
800
+ );
801
+ }
802
+ return loaderRef.current;
803
+ }, []);
804
+ useEffect3(
805
+ () => () => {
806
+ loaderRef.current?.dispose();
807
+ loaderRef.current = void 0;
808
+ },
809
+ []
810
+ );
811
+ useEffect3(() => {
812
+ ensureLoader()?.update(renderInfos, rootData);
813
+ });
814
+ const handleOptionsQuery = useCallback3(
815
+ (query) => {
816
+ ensureLoader()?.setQuery(query);
817
+ },
818
+ [ensureLoader]
819
+ );
713
820
  const handleChange = useCallback3(
714
821
  (v) => onValueChangeField(v, name),
715
822
  [onValueChangeField, name]
@@ -720,63 +827,52 @@ var FieldInputInner = ({
720
827
  );
721
828
  if (fields) {
722
829
  const items = Array.isArray(renderInfos[name]) ? renderInfos[name] : [];
723
- return /* @__PURE__ */ jsx7(
830
+ return /* @__PURE__ */ jsx8(
724
831
  FieldGroupInput_default,
725
832
  {
726
833
  fieldDescription,
727
834
  items,
728
835
  rootData,
836
+ errors,
837
+ touched: touchedMap,
838
+ onBlurField,
729
839
  onChange: handleChange
730
840
  }
731
841
  );
732
842
  }
733
- const effectiveDisabled = resolveDisabled(
843
+ const rendererProps = buildFieldRendererProps({
734
844
  fieldDescription,
735
- renderInfos,
736
- rootData
737
- );
738
- const readOnly = resolveReadOnly(fieldDescription, renderInfos, rootData);
739
- const resolvedOptionsList = resolveOptions(
740
- fieldDescription,
741
- renderInfos,
742
- rootData
743
- );
744
- const errors = effectiveDisabled ? [] : validateField(fieldDescription, renderInfos[name], renderInfos, rootData);
745
- const error = errors.length > 0 ? errors : void 0;
746
- const fieldId = `dfk-field-${name}`;
747
- return /* @__PURE__ */ jsx7(
845
+ data: renderInfos,
846
+ rootData,
847
+ id: makeFieldId(fieldDescription, idPrefix),
848
+ touched,
849
+ dirty,
850
+ validationErrors: errors === void 0 ? void 0 : errors[name] ?? [],
851
+ optionsState
852
+ });
853
+ return /* @__PURE__ */ jsx8(
748
854
  DynamicInput_default,
749
855
  {
750
- id: fieldId,
751
- type,
752
- label,
753
- value: renderInfos[name],
754
- options: resolvedOptionsList,
755
- className,
756
- description,
757
- disabled: effectiveDisabled,
758
- readOnly,
759
- required,
760
- touched,
761
- dirty,
762
- error,
763
- ariaInvalid: Boolean(error),
764
- ariaRequired: Boolean(required),
765
- extraProps: props,
856
+ ...rendererProps,
857
+ description: rendererProps.description,
766
858
  onChange: handleChange,
767
- onBlur: handleBlur
859
+ onBlur: handleBlur,
860
+ onOptionsQuery: isAsync ? handleOptionsQuery : void 0
768
861
  }
769
862
  );
770
863
  };
771
864
  var FieldInput = /* @__PURE__ */ React6.memo(FieldInputInner, (prev, next) => {
772
865
  const name = prev.fieldDescription.name;
773
- 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];
866
+ if (isAsyncOptions(prev.fieldDescription) && prev.renderInfos !== next.renderInfos) {
867
+ return false;
868
+ }
869
+ 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];
774
870
  });
775
871
  var FieldInput_default = FieldInput;
776
872
 
777
873
  // src/components/DynamicFormDevTools.tsx
778
- import { useState as useState3 } from "react";
779
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
874
+ import { useState as useState4 } from "react";
875
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
780
876
  var DynamicFormDevTools = ({
781
877
  data,
782
878
  errors = {},
@@ -785,8 +881,8 @@ var DynamicFormDevTools = ({
785
881
  fields = [],
786
882
  position = "bottom-right"
787
883
  }) => {
788
- const [isOpen, setIsOpen] = useState3(false);
789
- const [activeTab, setActiveTab] = useState3("data");
884
+ const [isOpen, setIsOpen] = useState4(false);
885
+ const [activeTab, setActiveTab] = useState4("data");
790
886
  const errorCount = Object.keys(errors).length;
791
887
  const posStyle = position === "bottom-left" ? { left: "16px", bottom: "16px" } : { right: "16px", bottom: "16px" };
792
888
  if (!isOpen) {
@@ -813,8 +909,8 @@ var DynamicFormDevTools = ({
813
909
  gap: "6px"
814
910
  },
815
911
  children: [
816
- /* @__PURE__ */ jsx8("span", { children: "\u{1F50D} DevTools" }),
817
- errorCount > 0 && /* @__PURE__ */ jsx8(
912
+ /* @__PURE__ */ jsx9("span", { children: "\u{1F50D} DevTools" }),
913
+ errorCount > 0 && /* @__PURE__ */ jsx9(
818
914
  "span",
819
915
  {
820
916
  style: {
@@ -864,8 +960,8 @@ var DynamicFormDevTools = ({
864
960
  borderBottom: "1px solid #334155"
865
961
  },
866
962
  children: [
867
- /* @__PURE__ */ jsx8("span", { style: { fontWeight: "bold", color: "#38bdf8" }, children: "\u{1F6E0}\uFE0F Form DevTools" }),
868
- /* @__PURE__ */ jsx8(
963
+ /* @__PURE__ */ jsx9("span", { style: { fontWeight: "bold", color: "#38bdf8" }, children: "\u{1F6E0}\uFE0F Form DevTools" }),
964
+ /* @__PURE__ */ jsx9(
869
965
  "button",
870
966
  {
871
967
  type: "button",
@@ -883,7 +979,7 @@ var DynamicFormDevTools = ({
883
979
  ]
884
980
  }
885
981
  ),
886
- /* @__PURE__ */ jsx8(
982
+ /* @__PURE__ */ jsx9(
887
983
  "div",
888
984
  {
889
985
  style: {
@@ -918,7 +1014,7 @@ var DynamicFormDevTools = ({
918
1014
  }
919
1015
  ),
920
1016
  /* @__PURE__ */ jsxs5("div", { style: { padding: "12px", overflowY: "auto", flex: 1 }, children: [
921
- activeTab === "data" && /* @__PURE__ */ jsx8(
1017
+ activeTab === "data" && /* @__PURE__ */ jsx9(
922
1018
  "pre",
923
1019
  {
924
1020
  style: {
@@ -930,24 +1026,24 @@ var DynamicFormDevTools = ({
930
1026
  children: JSON.stringify(data, null, 2)
931
1027
  }
932
1028
  ),
933
- activeTab === "errors" && /* @__PURE__ */ jsx8("div", { children: Object.keys(errors).length === 0 ? /* @__PURE__ */ jsx8("span", { style: { color: "#4ade80" }, children: "\u2713 No validation errors" }) : Object.entries(errors).map(([field, msgs]) => /* @__PURE__ */ jsxs5("div", { style: { marginBottom: "8px" }, children: [
1029
+ activeTab === "errors" && /* @__PURE__ */ jsx9("div", { children: Object.keys(errors).length === 0 ? /* @__PURE__ */ jsx9("span", { style: { color: "#4ade80" }, children: "\u2713 No validation errors" }) : Object.entries(errors).map(([field, msgs]) => /* @__PURE__ */ jsxs5("div", { style: { marginBottom: "8px" }, children: [
934
1030
  /* @__PURE__ */ jsxs5("span", { style: { color: "#f87171", fontWeight: "bold" }, children: [
935
1031
  field,
936
1032
  ":"
937
1033
  ] }),
938
- /* @__PURE__ */ jsx8("ul", { style: { margin: "4px 0 0 16px", padding: 0 }, children: msgs.map((m, i) => /* @__PURE__ */ jsx8("li", { style: { color: "#fca5a5" }, children: m }, i)) })
1034
+ /* @__PURE__ */ jsx9("ul", { style: { margin: "4px 0 0 16px", padding: 0 }, children: msgs.map((m, i) => /* @__PURE__ */ jsx9("li", { style: { color: "#fca5a5" }, children: m }, i)) })
939
1035
  ] }, field)) }),
940
1036
  activeTab === "meta" && /* @__PURE__ */ jsxs5("div", { children: [
941
1037
  /* @__PURE__ */ jsxs5("div", { style: { marginBottom: "6px" }, children: [
942
- /* @__PURE__ */ jsx8("span", { style: { color: "#94a3b8" }, children: "isDirty: " }),
943
- /* @__PURE__ */ jsx8("span", { style: { color: isDirty ? "#facc15" : "#4ade80" }, children: String(isDirty) })
1038
+ /* @__PURE__ */ jsx9("span", { style: { color: "#94a3b8" }, children: "isDirty: " }),
1039
+ /* @__PURE__ */ jsx9("span", { style: { color: isDirty ? "#facc15" : "#4ade80" }, children: String(isDirty) })
944
1040
  ] }),
945
1041
  /* @__PURE__ */ jsxs5("div", { children: [
946
- /* @__PURE__ */ jsx8("span", { style: { color: "#94a3b8" }, children: "Touched Fields:" }),
947
- /* @__PURE__ */ jsx8("pre", { style: { margin: "4px 0 0 0", color: "#cbd5e1" }, children: JSON.stringify(touched, null, 2) })
1042
+ /* @__PURE__ */ jsx9("span", { style: { color: "#94a3b8" }, children: "Touched Fields:" }),
1043
+ /* @__PURE__ */ jsx9("pre", { style: { margin: "4px 0 0 0", color: "#cbd5e1" }, children: JSON.stringify(touched, null, 2) })
948
1044
  ] })
949
1045
  ] }),
950
- activeTab === "fields" && /* @__PURE__ */ jsx8("div", { children: fields.length === 0 ? /* @__PURE__ */ jsx8("span", { style: { color: "#94a3b8" }, children: "No field descriptions passed" }) : fields.map((f) => /* @__PURE__ */ jsxs5(
1046
+ activeTab === "fields" && /* @__PURE__ */ jsx9("div", { children: fields.length === 0 ? /* @__PURE__ */ jsx9("span", { style: { color: "#94a3b8" }, children: "No field descriptions passed" }) : fields.map((f) => /* @__PURE__ */ jsxs5(
951
1047
  "div",
952
1048
  {
953
1049
  style: {
@@ -957,7 +1053,7 @@ var DynamicFormDevTools = ({
957
1053
  borderRadius: "4px"
958
1054
  },
959
1055
  children: [
960
- /* @__PURE__ */ jsx8("div", { style: { color: "#38bdf8", fontWeight: "bold" }, children: f.name }),
1056
+ /* @__PURE__ */ jsx9("div", { style: { color: "#38bdf8", fontWeight: "bold" }, children: f.name }),
961
1057
  /* @__PURE__ */ jsxs5("div", { style: { color: "#94a3b8", fontSize: "10px" }, children: [
962
1058
  "type: ",
963
1059
  f.type,
@@ -977,39 +1073,138 @@ var DynamicFormDevTools = ({
977
1073
  // src/useDynamicForm.ts
978
1074
  import {
979
1075
  applyComputedValues as applyComputedValues2,
980
- validateFields as validateFields2
1076
+ collectFieldPaths,
1077
+ createMessageResolver,
1078
+ validateFields as validateFields2,
1079
+ validateFieldsAsync
981
1080
  } from "@dynamic-field-kit/core";
982
- import { useCallback as useCallback4, useState as useState4 } from "react";
1081
+ import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
1082
+ function sameStringMap(left, right) {
1083
+ const keys = Object.keys(left);
1084
+ return keys.length === Object.keys(right).length && keys.every(
1085
+ (key) => left[key]?.length === right[key]?.length && left[key]?.every((value, index) => value === right[key]?.[index])
1086
+ );
1087
+ }
1088
+ function sameValidationResult(left, right) {
1089
+ 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);
1090
+ }
983
1091
  function useDynamicForm({
984
1092
  fields,
985
1093
  initialValues = {},
986
1094
  validateOnBlur = true,
987
- validateOnChange = false
1095
+ validateOnChange = false,
1096
+ messages
988
1097
  }) {
989
- const [data, setData] = useState4(
1098
+ const validationContextRef = useRef3({
1099
+ t: createMessageResolver(messages)
1100
+ });
1101
+ validationContextRef.current.t = createMessageResolver(messages);
1102
+ const [data, setData] = useState5(
990
1103
  () => applyComputedValues2(fields, initialValues)
991
1104
  );
992
- const [errors, setErrors] = useState4({});
993
- const [isDirty, setIsDirty] = useState4(false);
994
- const [touched, setTouched] = useState4({});
995
- const [isSubmitting, setIsSubmitting] = useState4(false);
996
- const [isSubmitted, setIsSubmitted] = useState4(false);
1105
+ const [errors, setErrors] = useState5({});
1106
+ const [isDirty, setIsDirty] = useState5(false);
1107
+ const [touched, setTouched] = useState5({});
1108
+ const [isSubmitting, setIsSubmitting] = useState5(false);
1109
+ const [isSubmitted, setIsSubmitted] = useState5(false);
1110
+ const [validationResult, setValidationResult] = useState5(
1111
+ () => validateFields2(fields, data, void 0, validationContextRef.current)
1112
+ );
1113
+ const [isValidating, setIsValidating] = useState5(false);
1114
+ const validationRunRef = useRef3(0);
1115
+ const validationAbortRef = useRef3(void 0);
1116
+ const submitRunRef = useRef3(0);
1117
+ const submitAbortRef = useRef3(void 0);
1118
+ const dataRef = useRef3(data);
1119
+ dataRef.current = data;
1120
+ const baselineRef = useRef3(dataRef.current);
1121
+ const [baselineValues, setBaselineValues] = useState5(
1122
+ () => baselineRef.current
1123
+ );
1124
+ const commitSyncResult = useCallback4((res) => {
1125
+ setValidationResult(
1126
+ (previous) => sameValidationResult(previous, res) ? previous : res
1127
+ );
1128
+ return res.valid;
1129
+ }, []);
1130
+ const lastValidatedRef = useRef3(void 0);
1131
+ const fieldsRef = useRef3(fields);
1132
+ if (fieldsRef.current !== fields) {
1133
+ fieldsRef.current = fields;
1134
+ lastValidatedRef.current = void 0;
1135
+ }
1136
+ useEffect4(() => {
1137
+ if (lastValidatedRef.current === data) {
1138
+ return;
1139
+ }
1140
+ lastValidatedRef.current = data;
1141
+ commitSyncResult(
1142
+ validateFields2(fields, data, void 0, validationContextRef.current)
1143
+ );
1144
+ }, [fields, data, commitSyncResult]);
1145
+ useEffect4(
1146
+ () => () => {
1147
+ validationAbortRef.current?.abort();
1148
+ submitAbortRef.current?.abort();
1149
+ },
1150
+ []
1151
+ );
997
1152
  const validate = useCallback4(() => {
998
- const res = validateFields2(fields, data);
1153
+ const res = validateFields2(
1154
+ fields,
1155
+ data,
1156
+ void 0,
1157
+ validationContextRef.current
1158
+ );
999
1159
  setErrors(res.errors);
1000
- return res.valid;
1160
+ return commitSyncResult(res);
1161
+ }, [fields, data, commitSyncResult]);
1162
+ const validateAsync = useCallback4(async () => {
1163
+ const run = ++validationRunRef.current;
1164
+ validationAbortRef.current?.abort();
1165
+ const controller = new AbortController();
1166
+ validationAbortRef.current = controller;
1167
+ const snapshot = data;
1168
+ setIsValidating(true);
1169
+ try {
1170
+ const res = await validateFieldsAsync(fields, snapshot, snapshot, {
1171
+ ...validationContextRef.current,
1172
+ signal: controller.signal
1173
+ });
1174
+ if (run !== validationRunRef.current || dataRef.current !== snapshot) {
1175
+ return res.valid;
1176
+ }
1177
+ setErrors(res.errors);
1178
+ setValidationResult(res);
1179
+ return res.valid;
1180
+ } finally {
1181
+ if (run === validationRunRef.current) {
1182
+ setIsValidating(false);
1183
+ }
1184
+ }
1001
1185
  }, [fields, data]);
1002
1186
  const handleChange = useCallback4(
1003
1187
  (newData) => {
1004
1188
  const next = applyComputedValues2(fields, newData);
1005
1189
  setData(next);
1190
+ dataRef.current = next;
1006
1191
  setIsDirty(true);
1192
+ validationAbortRef.current?.abort();
1193
+ validationRunRef.current += 1;
1194
+ setIsValidating(false);
1195
+ lastValidatedRef.current = next;
1196
+ const res = validateFields2(
1197
+ fields,
1198
+ next,
1199
+ void 0,
1200
+ validationContextRef.current
1201
+ );
1202
+ commitSyncResult(res);
1007
1203
  if (validateOnChange) {
1008
- const res = validateFields2(fields, next);
1009
1204
  setErrors(res.errors);
1010
1205
  }
1011
1206
  },
1012
- [fields, validateOnChange]
1207
+ [fields, validateOnChange, commitSyncResult]
1013
1208
  );
1014
1209
  const setFieldValue = useCallback4(
1015
1210
  (name, value) => {
@@ -1020,26 +1215,57 @@ function useDynamicForm({
1020
1215
  const setFieldTouched = useCallback4((name, isTouched = true) => {
1021
1216
  setTouched((prev) => ({ ...prev, [name]: isTouched }));
1022
1217
  }, []);
1218
+ const touchAll = useCallback4(() => {
1219
+ setTouched(
1220
+ Object.fromEntries(
1221
+ collectFieldPaths(fields, data).map((path) => [path, true])
1222
+ )
1223
+ );
1224
+ }, [fields, data]);
1225
+ const resetTouched = useCallback4(() => setTouched({}), []);
1226
+ const getDirtyValues = useCallback4(() => {
1227
+ const baseline = baselineRef.current;
1228
+ const current = dataRef.current;
1229
+ const dirty = {};
1230
+ for (const key of Object.keys(current)) {
1231
+ if (!Object.is(current[key], baseline[key])) {
1232
+ dirty[key] = current[key];
1233
+ }
1234
+ }
1235
+ return dirty;
1236
+ }, []);
1023
1237
  const handleBlur = useCallback4(
1024
1238
  (fieldName) => {
1025
1239
  setFieldTouched(fieldName, true);
1026
1240
  if (validateOnBlur) {
1027
- const res = validateFields2(fields, data);
1241
+ const res = validateFields2(
1242
+ fields,
1243
+ data,
1244
+ void 0,
1245
+ validationContextRef.current
1246
+ );
1028
1247
  setErrors(res.errors);
1248
+ commitSyncResult(res);
1029
1249
  }
1030
1250
  },
1031
- [fields, data, validateOnBlur, setFieldTouched]
1251
+ [fields, data, validateOnBlur, setFieldTouched, commitSyncResult]
1032
1252
  );
1033
1253
  const reset = useCallback4(
1034
1254
  (newValues) => {
1035
1255
  const seed = newValues ?? initialValues;
1036
1256
  const next = applyComputedValues2(fields, seed);
1037
1257
  setData(next);
1258
+ dataRef.current = next;
1259
+ baselineRef.current = next;
1260
+ setBaselineValues(next);
1038
1261
  setErrors({});
1039
1262
  setIsDirty(false);
1040
1263
  setTouched({});
1041
1264
  setIsSubmitting(false);
1042
1265
  setIsSubmitted(false);
1266
+ validationAbortRef.current?.abort();
1267
+ validationRunRef.current += 1;
1268
+ setIsValidating(false);
1043
1269
  },
1044
1270
  [fields, initialValues]
1045
1271
  );
@@ -1049,37 +1275,77 @@ function useDynamicForm({
1049
1275
  e.preventDefault();
1050
1276
  }
1051
1277
  setIsSubmitting(true);
1278
+ const submitRun = ++submitRunRef.current;
1052
1279
  try {
1053
- const res = validateFields2(fields, data);
1054
- setErrors(res.errors);
1280
+ touchAll();
1281
+ const run = ++validationRunRef.current;
1282
+ validationAbortRef.current?.abort();
1283
+ submitAbortRef.current?.abort();
1284
+ const controller = new AbortController();
1285
+ submitAbortRef.current = controller;
1286
+ const snapshot = data;
1287
+ setIsValidating(true);
1288
+ const res = await validateFieldsAsync(fields, snapshot, snapshot, {
1289
+ ...validationContextRef.current,
1290
+ signal: controller.signal
1291
+ });
1292
+ if (submitRun !== submitRunRef.current) {
1293
+ return;
1294
+ }
1295
+ if (dataRef.current === snapshot && run === validationRunRef.current) {
1296
+ setErrors(res.errors);
1297
+ setValidationResult(res);
1298
+ } else {
1299
+ const live = validateFields2(
1300
+ fields,
1301
+ dataRef.current,
1302
+ void 0,
1303
+ validationContextRef.current
1304
+ );
1305
+ setErrors(live.errors);
1306
+ commitSyncResult(live);
1307
+ }
1055
1308
  setIsSubmitted(true);
1056
1309
  if (res.valid) {
1057
- await onValid(data);
1310
+ await onValid(snapshot);
1058
1311
  } else if (onInvalid) {
1059
1312
  onInvalid(res.errors);
1060
1313
  }
1061
1314
  } finally {
1315
+ if (submitRun === submitRunRef.current) {
1316
+ setIsValidating(false);
1317
+ }
1062
1318
  setIsSubmitting(false);
1063
1319
  }
1064
1320
  },
1065
- [fields, data]
1321
+ [fields, data, touchAll, commitSyncResult]
1066
1322
  );
1067
- const isValid = Object.keys(errors).length === 0;
1068
1323
  return {
1069
1324
  data,
1070
1325
  errors,
1071
- isValid,
1326
+ isValid: validationResult.valid,
1327
+ isValidating,
1328
+ // Matches Vue and Angular: a run still in flight is not complete, whatever
1329
+ // the last finished pass concluded.
1330
+ isValidationComplete: validationResult.complete && !isValidating,
1331
+ validationStatus: isValidating ? "pending" : validationResult.status,
1072
1332
  isDirty,
1333
+ baselineValues,
1334
+ getDirtyValues,
1073
1335
  isSubmitting,
1074
1336
  isSubmitted,
1075
1337
  touched,
1076
1338
  setData,
1077
1339
  setFieldValue,
1078
1340
  setFieldTouched,
1341
+ setTouched,
1342
+ touchAll,
1343
+ resetTouched,
1079
1344
  handleChange,
1080
1345
  handleBlur,
1081
1346
  reset,
1082
1347
  validate,
1348
+ validateAsync,
1083
1349
  handleSubmit
1084
1350
  };
1085
1351
  }
@@ -1087,34 +1353,48 @@ function useDynamicForm({
1087
1353
  // src/index.ts
1088
1354
  import { FieldRegistry } from "@dynamic-field-kit/core";
1089
1355
  import {
1090
- validateField as validateField2,
1356
+ buildFieldRendererProps as buildFieldRendererProps2,
1357
+ makeFieldId as makeFieldId2,
1358
+ makeErrorId as makeErrorId2,
1359
+ FIELD_RENDERER_PROP_KEYS
1360
+ } from "@dynamic-field-kit/core";
1361
+ import {
1362
+ validateField,
1091
1363
  validateFieldAsync,
1092
1364
  validateFields as validateFields3,
1093
- validateFieldsAsync,
1094
- resolveDisabled as resolveDisabled2,
1095
- resolveReadOnly as resolveReadOnly2,
1096
- resolveOptions as resolveOptions2,
1365
+ validateFieldsAsync as validateFieldsAsync2,
1366
+ collectFieldPaths as collectFieldPaths2,
1367
+ indexGroupPathMap as indexGroupPathMap2,
1368
+ resolveDisabled,
1369
+ resolveReadOnly,
1370
+ resolveOptions,
1097
1371
  validators
1098
1372
  } from "@dynamic-field-kit/core";
1099
1373
  export {
1100
1374
  DynamicFormDevTools,
1101
1375
  DynamicInput_default as DynamicInput,
1376
+ FIELD_RENDERER_PROP_KEYS,
1102
1377
  FieldInput_default as FieldInput,
1103
1378
  FieldRegistry,
1104
1379
  FieldRegistryProvider,
1105
1380
  MultiFieldInput_default as MultiFieldInput,
1381
+ buildFieldRendererProps2 as buildFieldRendererProps,
1382
+ collectFieldPaths2 as collectFieldPaths,
1106
1383
  defaultRenderersMap,
1107
1384
  fieldRegistry,
1108
1385
  getDefaultRenderer,
1386
+ indexGroupPathMap2 as indexGroupPathMap,
1109
1387
  layoutRegistry,
1110
- resolveDisabled2 as resolveDisabled,
1111
- resolveOptions2 as resolveOptions,
1112
- resolveReadOnly2 as resolveReadOnly,
1388
+ makeErrorId2 as makeErrorId,
1389
+ makeFieldId2 as makeFieldId,
1390
+ resolveDisabled,
1391
+ resolveOptions,
1392
+ resolveReadOnly,
1113
1393
  useDynamicForm,
1114
1394
  useFieldRegistry,
1115
- validateField2 as validateField,
1395
+ validateField,
1116
1396
  validateFieldAsync,
1117
1397
  validateFields3 as validateFields,
1118
- validateFieldsAsync,
1398
+ validateFieldsAsync2 as validateFieldsAsync,
1119
1399
  validators
1120
1400
  };