@formality-ui/react 0.2.2 → 0.2.4

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
@@ -202,10 +202,12 @@ declare function defineInputs<T extends Record<string, ReactInputConfig>>(inputs
202
202
  * `<Field>` renders your input component via React Hook Form's `<Controller>`.
203
203
  * At runtime Formality merges a `coreProps` bundle onto the component (name,
204
204
  * value, onChange, onBlur, and — as a React-special key — `ref`). The three
205
- * members below are the **injected-props contract**: `formState` reaches
206
- * templates and render-prop children; `state` (subscribed field state) and a
207
- * top-level `forwardRef` key are delivered at runtime by `<Field>` (see
208
- * "Runtime delivery" below).
205
+ * members below are the **injected-props contract**: `formState` always
206
+ * reaches templates and render-prop children, and reaches plain components
207
+ * that have opted into Formality state via `provideState` /
208
+ * `passSubscriptions`; `state` (subscribed field state) and a top-level
209
+ * `forwardRef` key are delivered at runtime by `<Field>` (see "Runtime
210
+ * delivery" below).
209
211
  *
210
212
  * **Destructure before forwarding.** Component authors MUST destructure
211
213
  * `state`, `formState`, and `forwardRef` OUT of props before spreading the
package/dist/index.d.ts CHANGED
@@ -202,10 +202,12 @@ declare function defineInputs<T extends Record<string, ReactInputConfig>>(inputs
202
202
  * `<Field>` renders your input component via React Hook Form's `<Controller>`.
203
203
  * At runtime Formality merges a `coreProps` bundle onto the component (name,
204
204
  * value, onChange, onBlur, and — as a React-special key — `ref`). The three
205
- * members below are the **injected-props contract**: `formState` reaches
206
- * templates and render-prop children; `state` (subscribed field state) and a
207
- * top-level `forwardRef` key are delivered at runtime by `<Field>` (see
208
- * "Runtime delivery" below).
205
+ * members below are the **injected-props contract**: `formState` always
206
+ * reaches templates and render-prop children, and reaches plain components
207
+ * that have opted into Formality state via `provideState` /
208
+ * `passSubscriptions`; `state` (subscribed field state) and a top-level
209
+ * `forwardRef` key are delivered at runtime by `<Field>` (see "Runtime
210
+ * delivery" below).
209
211
  *
210
212
  * **Destructure before forwarding.** Component authors MUST destructure
211
213
  * `state`, `formState`, and `forwardRef` OUT of props before spreading the
package/dist/index.js CHANGED
@@ -430,22 +430,15 @@ function Form({
430
430
  await handleSubmit(values);
431
431
  }, [methods, handleSubmit, waitForFieldValidation]);
432
432
  executeAutoSaveRef.current = executeAutoSave;
433
- const getOrCreateDebounced = useCallback(
434
- (ms) => {
435
- const cached = fieldDebouncersRef.current.get(ms);
436
- if (cached) return cached;
437
- const debouncedFn = debounce(() => {
438
- executeAutoSaveRef.current?.();
439
- }, ms);
440
- const fn = Object.assign(debouncedFn, {
441
- pending: () => false
442
- // lodash debounce tracks pending internally
443
- });
444
- fieldDebouncersRef.current.set(ms, fn);
445
- return fn;
446
- },
447
- []
448
- );
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
+ }, []);
449
442
  getOrCreateDebouncedRef.current = getOrCreateDebounced;
450
443
  useEffect(() => {
451
444
  return () => {
@@ -470,13 +463,9 @@ function Form({
470
463
  }
471
464
  );
472
465
  }
473
- const debouncedFn = debounce(() => {
466
+ return wrapDebounced(() => {
474
467
  executeAutoSaveRef.current?.();
475
468
  }, debounceMs);
476
- return Object.assign(debouncedFn, {
477
- pending: () => false
478
- // lodash debounce handles this internally
479
- });
480
469
  }, [debounceMs]);
481
470
  debouncedSubmitRef.current = debouncedSubmit;
482
471
  useEffect(() => {
@@ -485,7 +474,11 @@ function Form({
485
474
  };
486
475
  }, [debouncedSubmit]);
487
476
  const submitImmediate = useCallback(() => {
488
- debouncedSubmitRef.current?.flush();
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?.();
489
482
  }, []);
490
483
  const unusedFields = useMemo(() => {
491
484
  const allFields = Object.keys(config);
@@ -559,6 +552,30 @@ function Form({
559
552
  resolvedTitle
560
553
  }) : children }) }) });
561
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
+ }
562
579
  function transformValuesForSubmit(values, config, inputs) {
563
580
  const result = {};
564
581
  for (const [name, value] of Object.entries(values)) {
@@ -1006,18 +1023,52 @@ function Field({
1006
1023
  }
1007
1024
  }
1008
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
+ });
1009
1033
  const isDisabled = useMemo(() => {
1010
1034
  if (disabledProp !== void 0) return disabledProp;
1011
1035
  if (fieldConfig.disabled !== void 0) return fieldConfig.disabled;
1012
1036
  if (conditionResult.hasDisabledCondition)
1013
1037
  return conditionResult.disabled ?? false;
1014
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
+ }
1015
1059
  return false;
1016
1060
  }, [
1017
1061
  disabledProp,
1018
1062
  fieldConfig.disabled,
1063
+ fieldConfig.props,
1019
1064
  conditionResult,
1020
- groupContext.state.isDisabled
1065
+ groupContext.state.isDisabled,
1066
+ fieldSelectProps,
1067
+ formSelectProps,
1068
+ providerSelectProps,
1069
+ inputConfig.props,
1070
+ formConfig.defaultFieldProps,
1071
+ providerConfig.defaultFieldProps
1021
1072
  ]);
1022
1073
  const isVisible = useMemo(() => {
1023
1074
  if (hiddenProp !== void 0) return !hiddenProp;
@@ -1032,16 +1083,43 @@ function Field({
1032
1083
  conditionResult,
1033
1084
  groupContext.state.isVisible
1034
1085
  ]);
1035
- const { providerSelectProps, formSelectProps, fieldSelectProps } = usePropsEvaluation({
1036
- selectProps: fieldConfig.selectProps,
1037
- formDefaultFieldProps: formConfig.selectDefaultFieldProps,
1038
- providerDefaultFieldProps: providerConfig.selectDefaultFieldProps,
1039
- subscribesTo: fieldConfig.subscribesTo,
1040
- fieldName: name
1041
- });
1042
1086
  const label = useMemo(() => {
1043
1087
  return resolveLabel(name, fieldConfig, fieldSelectProps, restProps);
1044
1088
  }, [name, fieldConfig, fieldSelectProps, restProps]);
1089
+ const passSubscriptionsEnabled = fieldConfig.passSubscriptions === true;
1090
+ const provideStateEnabled = fieldConfig.provideState === true;
1091
+ const subscribedWatchNames = useMemo(
1092
+ () => passSubscriptionsEnabled && allSubscriptions.length > 0 ? allSubscriptions : [],
1093
+ [passSubscriptionsEnabled, allSubscriptions]
1094
+ );
1095
+ const subscribedValues = useWatch({
1096
+ control: methods.control,
1097
+ name: subscribedWatchNames.length > 0 ? subscribedWatchNames : []
1098
+ });
1099
+ const subscribedState = useMemo(() => {
1100
+ if (!passSubscriptionsEnabled || subscribedWatchNames.length === 0) {
1101
+ return void 0;
1102
+ }
1103
+ const result = {};
1104
+ const values = Array.isArray(subscribedValues) ? subscribedValues : [subscribedValues];
1105
+ subscribedWatchNames.forEach((fieldName, i) => {
1106
+ const fs = methods.getFieldState(fieldName);
1107
+ result[fieldName] = makeProxyState({
1108
+ value: values[i],
1109
+ isTouched: fs.isTouched,
1110
+ isDirty: fs.isDirty,
1111
+ isValidating: fs.isValidating,
1112
+ error: fs.error,
1113
+ invalid: fs.invalid
1114
+ });
1115
+ });
1116
+ return result;
1117
+ }, [
1118
+ passSubscriptionsEnabled,
1119
+ subscribedWatchNames,
1120
+ subscribedValues,
1121
+ methods
1122
+ ]);
1045
1123
  const validationRules = useMemo(() => {
1046
1124
  return {
1047
1125
  ...fieldConfig.rules,
@@ -1119,6 +1197,24 @@ function Field({
1119
1197
  inputConfig.formatter,
1120
1198
  providerConfig.formatters
1121
1199
  );
1200
+ const stateInjection = {};
1201
+ if (provideStateEnabled) {
1202
+ stateInjection[providerConfig.defaultSubscriptionPropName] = makeProxyState({
1203
+ value: field.value,
1204
+ isTouched: fieldState.isTouched,
1205
+ isDirty: fieldState.isDirty,
1206
+ isValidating: fieldState.isValidating,
1207
+ error: fieldState.error,
1208
+ invalid: fieldState.invalid
1209
+ });
1210
+ }
1211
+ if (passSubscriptionsEnabled && subscribedState) {
1212
+ const subsPropName = fieldConfig.passSubscriptionsAs ?? providerConfig.defaultSubscriptionPropName;
1213
+ stateInjection[subsPropName] = subscribedState;
1214
+ }
1215
+ if (provideStateEnabled || passSubscriptionsEnabled) {
1216
+ stateInjection.formState = formState;
1217
+ }
1122
1218
  const finalProps = mergeFieldProps({
1123
1219
  providerDefaultFieldProps: providerConfig.defaultFieldProps,
1124
1220
  providerSelectDefaultFieldProps: providerSelectProps,
@@ -1136,7 +1232,8 @@ function Field({
1136
1232
  [inputConfig.inputFieldProp ?? "value"]: formattedValue,
1137
1233
  onChange: handleChange(field.onChange),
1138
1234
  onBlur: field.onBlur,
1139
- forwardRef: field.ref
1235
+ forwardRef: field.ref,
1236
+ ...stateInjection
1140
1237
  }
1141
1238
  });
1142
1239
  const Component = inputConfig.component;
@@ -1155,10 +1252,22 @@ function Field({
1155
1252
  }
1156
1253
  );
1157
1254
  } else if (isHostComponent) {
1158
- const { forwardRef: hostRef, ...restHostProps } = finalProps;
1255
+ const subsPropName = fieldConfig.passSubscriptionsAs ?? providerConfig.defaultSubscriptionPropName;
1256
+ const strippedHostProps = {};
1257
+ const nonDomKeys = /* @__PURE__ */ new Set([
1258
+ "forwardRef",
1259
+ "formState",
1260
+ "state",
1261
+ subsPropName
1262
+ ]);
1263
+ for (const [key, value] of Object.entries(finalProps)) {
1264
+ if (!nonDomKeys.has(key)) {
1265
+ strippedHostProps[key] = value;
1266
+ }
1267
+ }
1159
1268
  renderedField = createElement(inputConfig.component, {
1160
- ...restHostProps,
1161
- ref: hostRef
1269
+ ...strippedHostProps,
1270
+ ref: finalProps.forwardRef
1162
1271
  });
1163
1272
  } else {
1164
1273
  renderedField = /* @__PURE__ */ jsx(Component, { ...finalProps });