@formality-ui/react 0.2.3 → 0.2.5

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
@@ -591,6 +593,14 @@ interface FormProps<TFieldValues extends FieldValues = FieldValues> {
591
593
  * (`false` for immediate, or a number for a per-field delay).
592
594
  */
593
595
  debounce?: number | false;
596
+ /**
597
+ * React Hook Form validation trigger `mode`, forwarded to `useForm({ mode })`.
598
+ * One of `'onChange'` (default) | `'onBlur'` | `'onSubmit'` | `'onTouched'` |
599
+ * `'all'`. Honored as-is — auto-save is mode-agnostic: its validity gates
600
+ * trigger validation of the touched fields themselves via `methods.trigger`
601
+ * (which ignores `mode`), so it works under any mode. See PRD §12.
602
+ */
603
+ mode?: "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
594
604
  /** Form-level validation */
595
605
  validate?: (values: Partial<TFieldValues>) => Record<string, string> | Promise<Record<string, string>>;
596
606
  }
@@ -642,7 +652,7 @@ interface FormRenderAPI<TFieldValues extends FieldValues = FieldValues> {
642
652
  * </Form>
643
653
  * ```
644
654
  */
645
- declare function Form<TFieldValues extends FieldValues = FieldValues>({ children, config, formConfig, onSubmit, record, autoSave, debounce: debounceMs, validate, }: FormProps<TFieldValues>): JSX.Element;
655
+ declare function Form<TFieldValues extends FieldValues = FieldValues>({ children, config, formConfig, onSubmit, record, autoSave, debounce: debounceMs, mode, validate, }: FormProps<TFieldValues>): JSX.Element;
646
656
 
647
657
  /**
648
658
  * Field component props.
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
@@ -591,6 +593,14 @@ interface FormProps<TFieldValues extends FieldValues = FieldValues> {
591
593
  * (`false` for immediate, or a number for a per-field delay).
592
594
  */
593
595
  debounce?: number | false;
596
+ /**
597
+ * React Hook Form validation trigger `mode`, forwarded to `useForm({ mode })`.
598
+ * One of `'onChange'` (default) | `'onBlur'` | `'onSubmit'` | `'onTouched'` |
599
+ * `'all'`. Honored as-is — auto-save is mode-agnostic: its validity gates
600
+ * trigger validation of the touched fields themselves via `methods.trigger`
601
+ * (which ignores `mode`), so it works under any mode. See PRD §12.
602
+ */
603
+ mode?: "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
594
604
  /** Form-level validation */
595
605
  validate?: (values: Partial<TFieldValues>) => Record<string, string> | Promise<Record<string, string>>;
596
606
  }
@@ -642,7 +652,7 @@ interface FormRenderAPI<TFieldValues extends FieldValues = FieldValues> {
642
652
  * </Form>
643
653
  * ```
644
654
  */
645
- declare function Form<TFieldValues extends FieldValues = FieldValues>({ children, config, formConfig, onSubmit, record, autoSave, debounce: debounceMs, validate, }: FormProps<TFieldValues>): JSX.Element;
655
+ declare function Form<TFieldValues extends FieldValues = FieldValues>({ children, config, formConfig, onSubmit, record, autoSave, debounce: debounceMs, mode, validate, }: FormProps<TFieldValues>): JSX.Element;
646
656
 
647
657
  /**
648
658
  * Field component props.
package/dist/index.js CHANGED
@@ -147,6 +147,7 @@ function Form({
147
147
  record,
148
148
  autoSave = false,
149
149
  debounce: debounceMs = 1e3,
150
+ mode,
150
151
  validate
151
152
  }) {
152
153
  const providerConfig = useConfigContext();
@@ -164,7 +165,7 @@ function Form({
164
165
  return resolveAllInitialValues(config, mergedInputs, record ?? {});
165
166
  }, [config, mergedInputs, record]);
166
167
  const methods = useForm({
167
- mode: "onChange",
168
+ mode: mode ?? "onChange",
168
169
  defaultValues,
169
170
  values: record
170
171
  });
@@ -404,9 +405,20 @@ function Form({
404
405
  if (!validationsComplete || executionVersionRef.current !== executionVersion) {
405
406
  return;
406
407
  }
407
- for (const fieldName of changedFields) {
408
- const fieldState = methods.getFieldState(fieldName);
409
- if (fieldState.error) {
408
+ const changedArray = [...changedFields];
409
+ if (changedArray.length > 0) {
410
+ const changedValid = await methods.trigger(changedArray);
411
+ if (executionVersionRef.current !== executionVersion) {
412
+ return;
413
+ }
414
+ if (!changedValid) {
415
+ return;
416
+ }
417
+ const changedValidationsComplete = await waitForFieldValidation(
418
+ changedArray,
419
+ executionVersion
420
+ );
421
+ if (!changedValidationsComplete || executionVersionRef.current !== executionVersion) {
410
422
  return;
411
423
  }
412
424
  }
@@ -1086,6 +1098,40 @@ function Field({
1086
1098
  const label = useMemo(() => {
1087
1099
  return resolveLabel(name, fieldConfig, fieldSelectProps, restProps);
1088
1100
  }, [name, fieldConfig, fieldSelectProps, restProps]);
1101
+ const passSubscriptionsEnabled = fieldConfig.passSubscriptions === true;
1102
+ const provideStateEnabled = fieldConfig.provideState === true;
1103
+ const subscribedWatchNames = useMemo(
1104
+ () => passSubscriptionsEnabled && allSubscriptions.length > 0 ? allSubscriptions : [],
1105
+ [passSubscriptionsEnabled, allSubscriptions]
1106
+ );
1107
+ const subscribedValues = useWatch({
1108
+ control: methods.control,
1109
+ name: subscribedWatchNames.length > 0 ? subscribedWatchNames : []
1110
+ });
1111
+ const subscribedState = useMemo(() => {
1112
+ if (!passSubscriptionsEnabled || subscribedWatchNames.length === 0) {
1113
+ return void 0;
1114
+ }
1115
+ const result = {};
1116
+ const values = Array.isArray(subscribedValues) ? subscribedValues : [subscribedValues];
1117
+ subscribedWatchNames.forEach((fieldName, i) => {
1118
+ const fs = methods.getFieldState(fieldName);
1119
+ result[fieldName] = makeProxyState({
1120
+ value: values[i],
1121
+ isTouched: fs.isTouched,
1122
+ isDirty: fs.isDirty,
1123
+ isValidating: fs.isValidating,
1124
+ error: fs.error,
1125
+ invalid: fs.invalid
1126
+ });
1127
+ });
1128
+ return result;
1129
+ }, [
1130
+ passSubscriptionsEnabled,
1131
+ subscribedWatchNames,
1132
+ subscribedValues,
1133
+ methods
1134
+ ]);
1089
1135
  const validationRules = useMemo(() => {
1090
1136
  return {
1091
1137
  ...fieldConfig.rules,
@@ -1163,6 +1209,24 @@ function Field({
1163
1209
  inputConfig.formatter,
1164
1210
  providerConfig.formatters
1165
1211
  );
1212
+ const stateInjection = {};
1213
+ if (provideStateEnabled) {
1214
+ stateInjection[providerConfig.defaultSubscriptionPropName] = makeProxyState({
1215
+ value: field.value,
1216
+ isTouched: fieldState.isTouched,
1217
+ isDirty: fieldState.isDirty,
1218
+ isValidating: fieldState.isValidating,
1219
+ error: fieldState.error,
1220
+ invalid: fieldState.invalid
1221
+ });
1222
+ }
1223
+ if (passSubscriptionsEnabled && subscribedState) {
1224
+ const subsPropName = fieldConfig.passSubscriptionsAs ?? providerConfig.defaultSubscriptionPropName;
1225
+ stateInjection[subsPropName] = subscribedState;
1226
+ }
1227
+ if (provideStateEnabled || passSubscriptionsEnabled) {
1228
+ stateInjection.formState = formState;
1229
+ }
1166
1230
  const finalProps = mergeFieldProps({
1167
1231
  providerDefaultFieldProps: providerConfig.defaultFieldProps,
1168
1232
  providerSelectDefaultFieldProps: providerSelectProps,
@@ -1180,7 +1244,8 @@ function Field({
1180
1244
  [inputConfig.inputFieldProp ?? "value"]: formattedValue,
1181
1245
  onChange: handleChange(field.onChange),
1182
1246
  onBlur: field.onBlur,
1183
- forwardRef: field.ref
1247
+ forwardRef: field.ref,
1248
+ ...stateInjection
1184
1249
  }
1185
1250
  });
1186
1251
  const Component = inputConfig.component;
@@ -1199,10 +1264,22 @@ function Field({
1199
1264
  }
1200
1265
  );
1201
1266
  } else if (isHostComponent) {
1202
- const { forwardRef: hostRef, ...restHostProps } = finalProps;
1267
+ const subsPropName = fieldConfig.passSubscriptionsAs ?? providerConfig.defaultSubscriptionPropName;
1268
+ const strippedHostProps = {};
1269
+ const nonDomKeys = /* @__PURE__ */ new Set([
1270
+ "forwardRef",
1271
+ "formState",
1272
+ "state",
1273
+ subsPropName
1274
+ ]);
1275
+ for (const [key, value] of Object.entries(finalProps)) {
1276
+ if (!nonDomKeys.has(key)) {
1277
+ strippedHostProps[key] = value;
1278
+ }
1279
+ }
1203
1280
  renderedField = createElement(inputConfig.component, {
1204
- ...restHostProps,
1205
- ref: hostRef
1281
+ ...strippedHostProps,
1282
+ ref: finalProps.forwardRef
1206
1283
  });
1207
1284
  } else {
1208
1285
  renderedField = /* @__PURE__ */ jsx(Component, { ...finalProps });