@formality-ui/react 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -581,7 +581,15 @@ interface FormProps<TFieldValues extends FieldValues = FieldValues> {
581
581
  record?: Partial<TFieldValues>;
582
582
  /** Enable auto-save on field changes */
583
583
  autoSave?: boolean;
584
- /** Debounce milliseconds for auto-save. false = immediate submission, number = delay in milliseconds (default: 1000) */
584
+ /**
585
+ * Form-level auto-save debounce, used as the fallback for any field whose
586
+ * `InputConfig.debounce` is unset.
587
+ * - `false` — submit immediately (no debounce timer).
588
+ * - `number` — delay auto-save by this many milliseconds (default: 1000).
589
+ *
590
+ * A field can override its own cadence via `InputConfig.debounce`
591
+ * (`false` for immediate, or a number for a per-field delay).
592
+ */
585
593
  debounce?: number | false;
586
594
  /** Form-level validation */
587
595
  validate?: (values: Partial<TFieldValues>) => Record<string, string> | Promise<Record<string, string>>;
package/dist/index.d.ts CHANGED
@@ -581,7 +581,15 @@ interface FormProps<TFieldValues extends FieldValues = FieldValues> {
581
581
  record?: Partial<TFieldValues>;
582
582
  /** Enable auto-save on field changes */
583
583
  autoSave?: boolean;
584
- /** Debounce milliseconds for auto-save. false = immediate submission, number = delay in milliseconds (default: 1000) */
584
+ /**
585
+ * Form-level auto-save debounce, used as the fallback for any field whose
586
+ * `InputConfig.debounce` is unset.
587
+ * - `false` — submit immediately (no debounce timer).
588
+ * - `number` — delay auto-save by this many milliseconds (default: 1000).
589
+ *
590
+ * A field can override its own cadence via `InputConfig.debounce`
591
+ * (`false` for immediate, or a number for a per-field delay).
592
+ */
585
593
  debounce?: number | false;
586
594
  /** Form-level validation */
587
595
  validate?: (values: Partial<TFieldValues>) => Record<string, string> | Promise<Record<string, string>>;
package/dist/index.js CHANGED
@@ -179,6 +179,8 @@ function Form({
179
179
  const pendingChangedFields = useRef(/* @__PURE__ */ new Set());
180
180
  const pendingAffectedFields = useRef(/* @__PURE__ */ new Set());
181
181
  const executionVersionRef = useRef(0);
182
+ const fieldDebouncersRef = useRef(/* @__PURE__ */ new Map());
183
+ const getOrCreateDebouncedRef = useRef();
182
184
  const registerField = useCallback((name) => {
183
185
  fieldRegistry.current.add(name);
184
186
  setRegisteredFields(new Set(fieldRegistry.current));
@@ -279,8 +281,11 @@ function Form({
279
281
  for (const field of affected) {
280
282
  pendingAffectedFields.current.add(field);
281
283
  }
282
- if (inputConfig?.debounce === false) {
284
+ const fieldDebounce = inputConfig?.debounce;
285
+ if (fieldDebounce === false) {
283
286
  executeAutoSaveRef.current?.();
287
+ } else if (typeof fieldDebounce === "number") {
288
+ getOrCreateDebouncedRef.current?.(fieldDebounce)();
284
289
  } else {
285
290
  debouncedSubmitRef.current?.();
286
291
  }
@@ -421,53 +426,60 @@ function Form({
421
426
  return;
422
427
  }
423
428
  }
424
- const formState = methods.formState;
425
- if (Object.keys(formState.errors).length > 0) {
426
- return;
427
- }
428
429
  const values = methods.getValues();
429
430
  await handleSubmit(values);
430
431
  }, [methods, handleSubmit, waitForFieldValidation]);
431
432
  executeAutoSaveRef.current = executeAutoSave;
433
+ const getOrCreateDebounced = useCallback((ms) => {
434
+ const cached = fieldDebouncersRef.current.get(ms);
435
+ if (cached) return cached;
436
+ const fn = wrapDebounced(() => {
437
+ executeAutoSaveRef.current?.();
438
+ }, ms);
439
+ fieldDebouncersRef.current.set(ms, fn);
440
+ return fn;
441
+ }, []);
442
+ getOrCreateDebouncedRef.current = getOrCreateDebounced;
432
443
  useEffect(() => {
444
+ return () => {
445
+ fieldDebouncersRef.current.forEach((fn) => fn.cancel());
446
+ fieldDebouncersRef.current.clear();
447
+ };
448
+ }, [getOrCreateDebounced]);
449
+ const debouncedSubmit = useMemo(() => {
433
450
  if (debounceMs === false) {
434
- const immediateFn = Object.assign(
451
+ return Object.assign(
435
452
  () => {
436
- executeAutoSave();
453
+ executeAutoSaveRef.current?.();
437
454
  },
438
455
  {
439
456
  cancel: () => {
440
457
  },
441
458
  // No-op for immediate function
442
- flush: () => executeAutoSave(),
459
+ flush: () => executeAutoSaveRef.current?.(),
443
460
  // Execute immediately on flush
444
461
  pending: () => false
445
462
  // Never pending when immediate
446
463
  }
447
464
  );
448
- debouncedSubmitRef.current = immediateFn;
449
- return () => {
450
- };
451
465
  }
452
- const debouncedFn = debounce(() => {
453
- executeAutoSave();
466
+ return wrapDebounced(() => {
467
+ executeAutoSaveRef.current?.();
454
468
  }, debounceMs);
455
- const fn = Object.assign(debouncedFn, {
456
- pending: () => false
457
- // lodash debounce handles this internally
458
- });
459
- debouncedSubmitRef.current = fn;
469
+ }, [debounceMs]);
470
+ debouncedSubmitRef.current = debouncedSubmit;
471
+ useEffect(() => {
460
472
  return () => {
461
- debouncedFn.cancel();
473
+ debouncedSubmit.cancel();
462
474
  };
463
- }, [executeAutoSave, debounceMs]);
475
+ }, [debouncedSubmit]);
464
476
  const submitImmediate = useCallback(() => {
465
- if (debouncedSubmitRef.current?.flush) {
466
- debouncedSubmitRef.current.flush();
467
- } else {
468
- executeAutoSave();
469
- }
470
- }, [executeAutoSave]);
477
+ const anyPending = debouncedSubmitRef.current?.pending() === true || [...fieldDebouncersRef.current.values()].some((fn) => fn.pending());
478
+ if (!anyPending) return;
479
+ debouncedSubmitRef.current?.cancel();
480
+ fieldDebouncersRef.current.forEach((fn) => fn.cancel());
481
+ executeAutoSaveRef.current?.();
482
+ }, []);
471
483
  const unusedFields = useMemo(() => {
472
484
  const allFields = Object.keys(config);
473
485
  return allFields.filter((name) => !registeredFields.has(name));
@@ -506,7 +518,7 @@ function Form({
506
518
  setFieldValidating,
507
519
  getFormState,
508
520
  onSubmit,
509
- debouncedSubmit: debouncedSubmitRef.current,
521
+ debouncedSubmit,
510
522
  submitImmediate,
511
523
  unusedFields,
512
524
  methods
@@ -525,6 +537,7 @@ function Form({
525
537
  setFieldValidating,
526
538
  getFormState,
527
539
  onSubmit,
540
+ debouncedSubmit,
528
541
  submitImmediate,
529
542
  unusedFields,
530
543
  methods
@@ -539,6 +552,30 @@ function Form({
539
552
  resolvedTitle
540
553
  }) : children }) }) });
541
554
  }
555
+ function wrapDebounced(callback, ms) {
556
+ let isPending = false;
557
+ const debounced = debounce(() => {
558
+ isPending = false;
559
+ callback();
560
+ }, ms);
561
+ return Object.assign(
562
+ () => {
563
+ isPending = true;
564
+ debounced();
565
+ },
566
+ {
567
+ cancel: () => {
568
+ isPending = false;
569
+ debounced.cancel();
570
+ },
571
+ flush: () => {
572
+ isPending = false;
573
+ debounced.flush();
574
+ },
575
+ pending: () => isPending
576
+ }
577
+ );
578
+ }
542
579
  function transformValuesForSubmit(values, config, inputs) {
543
580
  const result = {};
544
581
  for (const [name, value] of Object.entries(values)) {
@@ -986,18 +1023,52 @@ function Field({
986
1023
  }
987
1024
  }
988
1025
  }, [effectiveSetValue.hasCondition, effectiveSetValue.value, name]);
1026
+ const { providerSelectProps, formSelectProps, fieldSelectProps } = usePropsEvaluation({
1027
+ selectProps: fieldConfig.selectProps,
1028
+ formDefaultFieldProps: formConfig.selectDefaultFieldProps,
1029
+ providerDefaultFieldProps: providerConfig.selectDefaultFieldProps,
1030
+ subscribesTo: fieldConfig.subscribesTo,
1031
+ fieldName: name
1032
+ });
989
1033
  const isDisabled = useMemo(() => {
990
1034
  if (disabledProp !== void 0) return disabledProp;
991
1035
  if (fieldConfig.disabled !== void 0) return fieldConfig.disabled;
992
1036
  if (conditionResult.hasDisabledCondition)
993
1037
  return conditionResult.disabled ?? false;
994
1038
  if (groupContext.state.isDisabled) return true;
1039
+ const propsLayers = [
1040
+ fieldSelectProps,
1041
+ // layer 2: field-level selectProps
1042
+ formSelectProps,
1043
+ // layer 5: form-level selectDefaultFieldProps
1044
+ providerSelectProps,
1045
+ // layer 7: provider-level selectDefaultFieldProps
1046
+ inputConfig.props,
1047
+ // layer 4: input config props
1048
+ fieldConfig.props,
1049
+ // layer 3: field config props
1050
+ formConfig.defaultFieldProps,
1051
+ // layer 6: form-level defaultFieldProps
1052
+ providerConfig.defaultFieldProps
1053
+ // layer 8: provider-level defaultFieldProps
1054
+ ];
1055
+ for (const layer of propsLayers) {
1056
+ const layerDisabled = layer?.disabled;
1057
+ if (layerDisabled !== void 0) return Boolean(layerDisabled);
1058
+ }
995
1059
  return false;
996
1060
  }, [
997
1061
  disabledProp,
998
1062
  fieldConfig.disabled,
1063
+ fieldConfig.props,
999
1064
  conditionResult,
1000
- groupContext.state.isDisabled
1065
+ groupContext.state.isDisabled,
1066
+ fieldSelectProps,
1067
+ formSelectProps,
1068
+ providerSelectProps,
1069
+ inputConfig.props,
1070
+ formConfig.defaultFieldProps,
1071
+ providerConfig.defaultFieldProps
1001
1072
  ]);
1002
1073
  const isVisible = useMemo(() => {
1003
1074
  if (hiddenProp !== void 0) return !hiddenProp;
@@ -1012,13 +1083,6 @@ function Field({
1012
1083
  conditionResult,
1013
1084
  groupContext.state.isVisible
1014
1085
  ]);
1015
- const { providerSelectProps, formSelectProps, fieldSelectProps } = usePropsEvaluation({
1016
- selectProps: fieldConfig.selectProps,
1017
- formDefaultFieldProps: formConfig.selectDefaultFieldProps,
1018
- providerDefaultFieldProps: providerConfig.selectDefaultFieldProps,
1019
- subscribesTo: fieldConfig.subscribesTo,
1020
- fieldName: name
1021
- });
1022
1086
  const label = useMemo(() => {
1023
1087
  return resolveLabel(name, fieldConfig, fieldSelectProps, restProps);
1024
1088
  }, [name, fieldConfig, fieldSelectProps, restProps]);