@formality-ui/react 0.2.5 → 0.3.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.cjs +69 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +184 -17
- package/dist/index.d.ts +184 -17
- package/dist/index.js +69 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -201,13 +201,14 @@ declare function defineInputs<T extends Record<string, ReactInputConfig>>(inputs
|
|
|
201
201
|
*
|
|
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
|
-
* value, onChange, onBlur, and —
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
204
|
+
* value, onChange, onBlur, and `forwardRef` — RHF's ref callback, delivered as
|
|
205
|
+
* a regular enumerable top-level prop, NOT React's special `ref` key; see §20.1
|
|
206
|
+
* and "Runtime delivery" below). The three members below are the
|
|
207
|
+
* **injected-props contract**: `formState` always reaches templates and
|
|
208
|
+
* render-prop children, and reaches plain components that have opted into
|
|
209
|
+
* Formality state via `provideState` / `passSubscriptions`; `state`
|
|
210
|
+
* (subscribed field state) and `forwardRef` are delivered at runtime by
|
|
211
|
+
* `<Field>` (see "Runtime delivery" below).
|
|
211
212
|
*
|
|
212
213
|
* **Destructure before forwarding.** Component authors MUST destructure
|
|
213
214
|
* `state`, `formState`, and `forwardRef` OUT of props before spreading the
|
|
@@ -722,15 +723,15 @@ interface FieldRenderAPI {
|
|
|
722
723
|
formState: UseFormStateReturn<FieldValues>;
|
|
723
724
|
}
|
|
724
725
|
/**
|
|
725
|
-
* Field component -
|
|
726
|
+
* Field component - Thin wrapper over {@link useField}.
|
|
726
727
|
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* -
|
|
730
|
-
*
|
|
731
|
-
* -
|
|
732
|
-
*
|
|
733
|
-
* -
|
|
728
|
+
* The RHF Controller integration (props resolution / 8-layer merge, conditions,
|
|
729
|
+
* parse/format, validation, forwardRef delivery, template/host rendering,
|
|
730
|
+
* render-prop `children` application) lives in the {@link useField} hook
|
|
731
|
+
* (PRD §1.3.3 / §5.3 / §20). This component owns ONLY:
|
|
732
|
+
* - the form-registry registration `useEffect` (`registerField`/
|
|
733
|
+
* `unregisterField`, gated on `shouldRegister`) — contract bullet b; and
|
|
734
|
+
* - delegating to `useField(params)` and returning its `renderedField`.
|
|
734
735
|
*
|
|
735
736
|
* @example
|
|
736
737
|
* ```tsx
|
|
@@ -743,7 +744,7 @@ interface FieldRenderAPI {
|
|
|
743
744
|
* // Custom render
|
|
744
745
|
* <Field name="name">
|
|
745
746
|
* {({ renderedField, fieldState }) => (
|
|
746
|
-
* <div className={fieldState.error ?
|
|
747
|
+
* <div className={fieldState.error ? "has-error" : ""}>
|
|
747
748
|
* {renderedField}
|
|
748
749
|
* </div>
|
|
749
750
|
* )}
|
|
@@ -1106,4 +1107,170 @@ declare function useInferredInputs(options: UseInferredInputsOptions): string[];
|
|
|
1106
1107
|
*/
|
|
1107
1108
|
declare function useSubscriptions(fieldName: string, subscriptions: string[]): void;
|
|
1108
1109
|
|
|
1109
|
-
|
|
1110
|
+
/**
|
|
1111
|
+
* Parameters for {@link useField}.
|
|
1112
|
+
*
|
|
1113
|
+
* Mirrors the subset of `FieldProps` (in `../components/Field`) that the hook
|
|
1114
|
+
* consumes as the single seam for **RHF Controller integration** (gap_analysis.md
|
|
1115
|
+
* **G6**; PRD §1.3.3).
|
|
1116
|
+
*
|
|
1117
|
+
* Per PRD §1.3.3, `hooks/useField` is the module that owns **RHF Controller
|
|
1118
|
+
* integration** for a field — "Uses Core: transform/pipeline, validation/
|
|
1119
|
+
* validate". This interface is the **input contract** for that module: it
|
|
1120
|
+
* carries the field identity + the overrides + the passthrough props threaded
|
|
1121
|
+
* into the parse/format pipeline (§5.3.5), change handler (§5.3.6), validation
|
|
1122
|
+
* wiring (§5.3.7), forwardRef delivery (§20.1), and template/host rendering
|
|
1123
|
+
* (§5.3.8).
|
|
1124
|
+
*
|
|
1125
|
+
* Generic over `TName` (default `string`) so a narrowed name type from
|
|
1126
|
+
* `FieldProps<TName>` can be threaded straight through (PRD §C.4 / T2.1). The
|
|
1127
|
+
* default keeps a bare `UseFieldParams` identical to `UseFieldParams<string>`.
|
|
1128
|
+
*
|
|
1129
|
+
* @template TName - Field name literal/union type (default `string`).
|
|
1130
|
+
*/
|
|
1131
|
+
interface UseFieldParams<TName extends string = string> {
|
|
1132
|
+
/**
|
|
1133
|
+
* Field name (must match a key in Form's config). When `TName` is narrowed,
|
|
1134
|
+
* this is checked against `TName` at compile time.
|
|
1135
|
+
*/
|
|
1136
|
+
name: TName;
|
|
1137
|
+
/** Override the input type resolved from config. */
|
|
1138
|
+
type?: string;
|
|
1139
|
+
/**
|
|
1140
|
+
* Override disabled state. Highest priority in the §5.3.4 disabled
|
|
1141
|
+
* resolution (prop > condition > config).
|
|
1142
|
+
*/
|
|
1143
|
+
disabled?: boolean;
|
|
1144
|
+
/** Override hidden state (inverse of visible). */
|
|
1145
|
+
hidden?: boolean;
|
|
1146
|
+
/**
|
|
1147
|
+
* Whether to register this field in Form's field registry (default `true`).
|
|
1148
|
+
*
|
|
1149
|
+
* NOTE: the registration `useEffect` itself is OWNED by the `<Field>`
|
|
1150
|
+
* component (contract bullet b) — this flag is threaded through so a future
|
|
1151
|
+
* direct-hook consumer can opt out. `useField` does NOT call
|
|
1152
|
+
* `registerField`/`unregisterField`.
|
|
1153
|
+
*/
|
|
1154
|
+
shouldRegister?: boolean;
|
|
1155
|
+
/**
|
|
1156
|
+
* Per-field input-config override. Merged at the highest-priority layer over
|
|
1157
|
+
* provider/form config (e.g. per-field debounce). See `resolveInputConfig`.
|
|
1158
|
+
*/
|
|
1159
|
+
inputConfig?: Partial<InputConfig>;
|
|
1160
|
+
/**
|
|
1161
|
+
* Optional render-prop. Applied INSIDE the hook's Controller render callback
|
|
1162
|
+
* (§5.3.8) against {@link UseFieldReturn}, so the render-prop always sees the
|
|
1163
|
+
* live `fieldState`/`formState`/`fieldProps` values.
|
|
1164
|
+
*/
|
|
1165
|
+
children?: ReactNode | ((api: UseFieldReturn) => ReactNode);
|
|
1166
|
+
/**
|
|
1167
|
+
* Additional props forwarded to the input component (the 8-layer
|
|
1168
|
+
* `componentProps` merge layer, §5.3.2). Captured by `mergeFieldProps`.
|
|
1169
|
+
*/
|
|
1170
|
+
[key: string]: unknown;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Return value of {@link useField}.
|
|
1174
|
+
*
|
|
1175
|
+
* **Structurally identical** to `FieldRenderAPI` (in `../components/Field`) —
|
|
1176
|
+
* the data the render layer (template / host / render-prop `children`) consumes.
|
|
1177
|
+
* This equivalence is enforced bidirectionally by
|
|
1178
|
+
* `__typechecks__/useField.test-d.ts`.
|
|
1179
|
+
*
|
|
1180
|
+
* Per PRD §1.3.3, the hook owns the RHF Controller integration, so the fields
|
|
1181
|
+
* below are exactly the Controller-produced state the render layer needs:
|
|
1182
|
+
* `fieldState` + `formState` come from the Controller render callback;
|
|
1183
|
+
* `renderedField` is the RAW rendered input (template/component/host) when
|
|
1184
|
+
* visible, `null` when hidden (the Controller mounts ONLY when visible, so
|
|
1185
|
+
* hidden fields are not RHF-registered — §5.3.8 / §20);
|
|
1186
|
+
* `fieldProps` is the final 8-layer-merged props (§5.3.2, via `mergeFieldProps`);
|
|
1187
|
+
* `watchers` is the map of fields currently watching this field (§5.3.1 step 3).
|
|
1188
|
+
*/
|
|
1189
|
+
interface UseFieldReturn {
|
|
1190
|
+
/**
|
|
1191
|
+
* RHF field state from the Controller render callback
|
|
1192
|
+
* (`invalid` / `isTouched` / `isDirty` / `error` / …).
|
|
1193
|
+
*/
|
|
1194
|
+
fieldState: ControllerFieldState;
|
|
1195
|
+
/**
|
|
1196
|
+
* The RAW rendered input (template / component / host element), or `null`
|
|
1197
|
+
* when the field is not visible. The render-prop `children` is applied
|
|
1198
|
+
* INSIDE the Controller render callback that produces this element.
|
|
1199
|
+
*/
|
|
1200
|
+
renderedField: ReactNode;
|
|
1201
|
+
/**
|
|
1202
|
+
* Final merged props passed to the input — the output of the 8-layer
|
|
1203
|
+
* `mergeFieldProps` pipeline (§5.3.2).
|
|
1204
|
+
*/
|
|
1205
|
+
fieldProps: Record<string, unknown>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Map of fields watching this field (`true` = currently subscribed). Populated
|
|
1208
|
+
* via the watcher-setter registered with `useSubscriptions` (§5.3.1 step 3).
|
|
1209
|
+
*/
|
|
1210
|
+
watchers: Record<string, boolean>;
|
|
1211
|
+
/**
|
|
1212
|
+
* RHF form state from the Controller. Typed as `UseFormStateReturn<FieldValues>`
|
|
1213
|
+
* to match `FieldRenderAPI` exactly (NOT bare `UseFormStateReturn`).
|
|
1214
|
+
*/
|
|
1215
|
+
formState: UseFormStateReturn<FieldValues>;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* useField — RHF Controller integration for a single Field (PRD §1.3.3 / §20).
|
|
1219
|
+
*
|
|
1220
|
+
* This hook owns the **entire** Controller lifecycle for one field: it reads
|
|
1221
|
+
* the form/config/group contexts, resolves the input config, sets up watchers
|
|
1222
|
+
* + subscriptions + conditions, builds the setValue effect (ref pattern,
|
|
1223
|
+
* §7.1.1), resolves the disabled/visible/label state, builds the validation
|
|
1224
|
+
* rules + change handler, mounts the `<Controller>` element with its full
|
|
1225
|
+
* render callback (format → state injection → `mergeFieldProps` 8-layer merge →
|
|
1226
|
+
* forwardRef delivery §20.1/§20.4 → template/host/component → render-prop
|
|
1227
|
+
* `children`), and returns the {@link UseFieldReturn}.
|
|
1228
|
+
*
|
|
1229
|
+
* ## Responsibilities (PRD §1.3.3 + §5.3.x + §20)
|
|
1230
|
+
*
|
|
1231
|
+
* - **Controller integration** — wraps RHF's `<Controller>` render callback.
|
|
1232
|
+
* The `<Controller>` mounts ONLY when the field is visible (the
|
|
1233
|
+
* `renderedField` is `null` when hidden), preserving the
|
|
1234
|
+
* conditionally-hidden-field invariant (hidden fields are NOT RHF-registered).
|
|
1235
|
+
* - **Parse/format pipeline (§5.3.5)** — Core `parse` / `format` from
|
|
1236
|
+
* `@formality-ui/core` (`transform/pipeline`).
|
|
1237
|
+
* - **Change handler (§5.3.6)** — `changeField` + watcher fan-out.
|
|
1238
|
+
* - **Validation (§5.3.7)** — Core `runValidator` / `resolveErrorMessage`
|
|
1239
|
+
* (`validation/validate`).
|
|
1240
|
+
* - **forwardRef delivery (§20.1)** — delivers the input's ref via the merged
|
|
1241
|
+
* props' `forwardRef` key (the CURRENT implemented behavior — NOT the legacy
|
|
1242
|
+
* `ref` key shown in the stale §5.3.2 pseudo-code). The host-element path
|
|
1243
|
+
* (§20.4 narrow exception) translates `forwardRef` back into React's special
|
|
1244
|
+
* `ref` key for string components.
|
|
1245
|
+
* - **Props merge (§5.3.2)** — `resolveInputConfig` + `resolveLabel` +
|
|
1246
|
+
* `mergeFieldProps` (the 8-layer merge).
|
|
1247
|
+
* - **Template/host render (§5.3.8)** — produces the RAW `renderedField`.
|
|
1248
|
+
*
|
|
1249
|
+
* It consumes these context hooks (`useFormContext`, `useConfigContext`,
|
|
1250
|
+
* `useGroupContext`) and composed hooks (`useConditions`, `usePropsEvaluation`,
|
|
1251
|
+
* `useInferredInputs`, `useSubscriptions`), and the Core fns above.
|
|
1252
|
+
*
|
|
1253
|
+
* NOTE: field **registration** (`registerField`/`unregisterField`) is OWNED by
|
|
1254
|
+
* the `<Field>` component (contract bullet b), NOT this hook. The hook reads
|
|
1255
|
+
* `useFormContext()` for everything else (config, methods, changeField,
|
|
1256
|
+
* setFieldValidating, register/unregisterWatcherSetter).
|
|
1257
|
+
*
|
|
1258
|
+
* @param params - Field parameters (see {@link UseFieldParams}).
|
|
1259
|
+
* @returns The Controller-produced state + the RAW rendered input
|
|
1260
|
+
* (see {@link UseFieldReturn}).
|
|
1261
|
+
*
|
|
1262
|
+
* @example
|
|
1263
|
+
* ```tsx
|
|
1264
|
+
* // Direct hook usage (custom layout):
|
|
1265
|
+
* function MyField() {
|
|
1266
|
+
* const { renderedField, fieldState } = useField({ name: "email" });
|
|
1267
|
+
* return <div className={fieldState.error ? "has-error" : ""}>{renderedField}</div>;
|
|
1268
|
+
* }
|
|
1269
|
+
*
|
|
1270
|
+
* // Equivalent via the thin wrapper component:
|
|
1271
|
+
* <Field name="email" />;
|
|
1272
|
+
* ```
|
|
1273
|
+
*/
|
|
1274
|
+
declare function useField<TName extends string = string>({ name, type: typeProp, disabled: disabledProp, hidden: hiddenProp, children, inputConfig: inputConfigProp, ...restProps }: UseFieldParams<TName>): UseFieldReturn;
|
|
1275
|
+
|
|
1276
|
+
export { ConfigContext, type ConfigContextValue, type CustomFieldState, type DebouncedFunction, type ExtendedFormState, Field, FieldGroup, type FieldGroupProps, type FieldProps, type FieldRenderAPI, Form, FormContext, type FormContextValue, type FormProps, type FormRenderAPI, type FormalityFieldComponentProps, FormalityProvider, type FormalityProviderProps, GroupContext, type GroupContextValue, type GroupState, type InputTemplateProps, type ReactFieldConfig, type ReactFormFieldsConfig, type ReactInputConfig, UnusedFields, type UnusedFieldsProps, type UseFieldParams, type UseFieldReturn, type UseFormStateOptions, type WatcherSetterFn, defineInputs, makeDeepProxyState, makeProxyState, useConditions, useConfigContext, useField, useFormContext, useFormState, useGroupContext, useInferredInputs, usePropsEvaluation, useSubscriptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -201,13 +201,14 @@ declare function defineInputs<T extends Record<string, ReactInputConfig>>(inputs
|
|
|
201
201
|
*
|
|
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
|
-
* value, onChange, onBlur, and —
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
204
|
+
* value, onChange, onBlur, and `forwardRef` — RHF's ref callback, delivered as
|
|
205
|
+
* a regular enumerable top-level prop, NOT React's special `ref` key; see §20.1
|
|
206
|
+
* and "Runtime delivery" below). The three members below are the
|
|
207
|
+
* **injected-props contract**: `formState` always reaches templates and
|
|
208
|
+
* render-prop children, and reaches plain components that have opted into
|
|
209
|
+
* Formality state via `provideState` / `passSubscriptions`; `state`
|
|
210
|
+
* (subscribed field state) and `forwardRef` are delivered at runtime by
|
|
211
|
+
* `<Field>` (see "Runtime delivery" below).
|
|
211
212
|
*
|
|
212
213
|
* **Destructure before forwarding.** Component authors MUST destructure
|
|
213
214
|
* `state`, `formState`, and `forwardRef` OUT of props before spreading the
|
|
@@ -722,15 +723,15 @@ interface FieldRenderAPI {
|
|
|
722
723
|
formState: UseFormStateReturn<FieldValues>;
|
|
723
724
|
}
|
|
724
725
|
/**
|
|
725
|
-
* Field component -
|
|
726
|
+
* Field component - Thin wrapper over {@link useField}.
|
|
726
727
|
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* -
|
|
730
|
-
*
|
|
731
|
-
* -
|
|
732
|
-
*
|
|
733
|
-
* -
|
|
728
|
+
* The RHF Controller integration (props resolution / 8-layer merge, conditions,
|
|
729
|
+
* parse/format, validation, forwardRef delivery, template/host rendering,
|
|
730
|
+
* render-prop `children` application) lives in the {@link useField} hook
|
|
731
|
+
* (PRD §1.3.3 / §5.3 / §20). This component owns ONLY:
|
|
732
|
+
* - the form-registry registration `useEffect` (`registerField`/
|
|
733
|
+
* `unregisterField`, gated on `shouldRegister`) — contract bullet b; and
|
|
734
|
+
* - delegating to `useField(params)` and returning its `renderedField`.
|
|
734
735
|
*
|
|
735
736
|
* @example
|
|
736
737
|
* ```tsx
|
|
@@ -743,7 +744,7 @@ interface FieldRenderAPI {
|
|
|
743
744
|
* // Custom render
|
|
744
745
|
* <Field name="name">
|
|
745
746
|
* {({ renderedField, fieldState }) => (
|
|
746
|
-
* <div className={fieldState.error ?
|
|
747
|
+
* <div className={fieldState.error ? "has-error" : ""}>
|
|
747
748
|
* {renderedField}
|
|
748
749
|
* </div>
|
|
749
750
|
* )}
|
|
@@ -1106,4 +1107,170 @@ declare function useInferredInputs(options: UseInferredInputsOptions): string[];
|
|
|
1106
1107
|
*/
|
|
1107
1108
|
declare function useSubscriptions(fieldName: string, subscriptions: string[]): void;
|
|
1108
1109
|
|
|
1109
|
-
|
|
1110
|
+
/**
|
|
1111
|
+
* Parameters for {@link useField}.
|
|
1112
|
+
*
|
|
1113
|
+
* Mirrors the subset of `FieldProps` (in `../components/Field`) that the hook
|
|
1114
|
+
* consumes as the single seam for **RHF Controller integration** (gap_analysis.md
|
|
1115
|
+
* **G6**; PRD §1.3.3).
|
|
1116
|
+
*
|
|
1117
|
+
* Per PRD §1.3.3, `hooks/useField` is the module that owns **RHF Controller
|
|
1118
|
+
* integration** for a field — "Uses Core: transform/pipeline, validation/
|
|
1119
|
+
* validate". This interface is the **input contract** for that module: it
|
|
1120
|
+
* carries the field identity + the overrides + the passthrough props threaded
|
|
1121
|
+
* into the parse/format pipeline (§5.3.5), change handler (§5.3.6), validation
|
|
1122
|
+
* wiring (§5.3.7), forwardRef delivery (§20.1), and template/host rendering
|
|
1123
|
+
* (§5.3.8).
|
|
1124
|
+
*
|
|
1125
|
+
* Generic over `TName` (default `string`) so a narrowed name type from
|
|
1126
|
+
* `FieldProps<TName>` can be threaded straight through (PRD §C.4 / T2.1). The
|
|
1127
|
+
* default keeps a bare `UseFieldParams` identical to `UseFieldParams<string>`.
|
|
1128
|
+
*
|
|
1129
|
+
* @template TName - Field name literal/union type (default `string`).
|
|
1130
|
+
*/
|
|
1131
|
+
interface UseFieldParams<TName extends string = string> {
|
|
1132
|
+
/**
|
|
1133
|
+
* Field name (must match a key in Form's config). When `TName` is narrowed,
|
|
1134
|
+
* this is checked against `TName` at compile time.
|
|
1135
|
+
*/
|
|
1136
|
+
name: TName;
|
|
1137
|
+
/** Override the input type resolved from config. */
|
|
1138
|
+
type?: string;
|
|
1139
|
+
/**
|
|
1140
|
+
* Override disabled state. Highest priority in the §5.3.4 disabled
|
|
1141
|
+
* resolution (prop > condition > config).
|
|
1142
|
+
*/
|
|
1143
|
+
disabled?: boolean;
|
|
1144
|
+
/** Override hidden state (inverse of visible). */
|
|
1145
|
+
hidden?: boolean;
|
|
1146
|
+
/**
|
|
1147
|
+
* Whether to register this field in Form's field registry (default `true`).
|
|
1148
|
+
*
|
|
1149
|
+
* NOTE: the registration `useEffect` itself is OWNED by the `<Field>`
|
|
1150
|
+
* component (contract bullet b) — this flag is threaded through so a future
|
|
1151
|
+
* direct-hook consumer can opt out. `useField` does NOT call
|
|
1152
|
+
* `registerField`/`unregisterField`.
|
|
1153
|
+
*/
|
|
1154
|
+
shouldRegister?: boolean;
|
|
1155
|
+
/**
|
|
1156
|
+
* Per-field input-config override. Merged at the highest-priority layer over
|
|
1157
|
+
* provider/form config (e.g. per-field debounce). See `resolveInputConfig`.
|
|
1158
|
+
*/
|
|
1159
|
+
inputConfig?: Partial<InputConfig>;
|
|
1160
|
+
/**
|
|
1161
|
+
* Optional render-prop. Applied INSIDE the hook's Controller render callback
|
|
1162
|
+
* (§5.3.8) against {@link UseFieldReturn}, so the render-prop always sees the
|
|
1163
|
+
* live `fieldState`/`formState`/`fieldProps` values.
|
|
1164
|
+
*/
|
|
1165
|
+
children?: ReactNode | ((api: UseFieldReturn) => ReactNode);
|
|
1166
|
+
/**
|
|
1167
|
+
* Additional props forwarded to the input component (the 8-layer
|
|
1168
|
+
* `componentProps` merge layer, §5.3.2). Captured by `mergeFieldProps`.
|
|
1169
|
+
*/
|
|
1170
|
+
[key: string]: unknown;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Return value of {@link useField}.
|
|
1174
|
+
*
|
|
1175
|
+
* **Structurally identical** to `FieldRenderAPI` (in `../components/Field`) —
|
|
1176
|
+
* the data the render layer (template / host / render-prop `children`) consumes.
|
|
1177
|
+
* This equivalence is enforced bidirectionally by
|
|
1178
|
+
* `__typechecks__/useField.test-d.ts`.
|
|
1179
|
+
*
|
|
1180
|
+
* Per PRD §1.3.3, the hook owns the RHF Controller integration, so the fields
|
|
1181
|
+
* below are exactly the Controller-produced state the render layer needs:
|
|
1182
|
+
* `fieldState` + `formState` come from the Controller render callback;
|
|
1183
|
+
* `renderedField` is the RAW rendered input (template/component/host) when
|
|
1184
|
+
* visible, `null` when hidden (the Controller mounts ONLY when visible, so
|
|
1185
|
+
* hidden fields are not RHF-registered — §5.3.8 / §20);
|
|
1186
|
+
* `fieldProps` is the final 8-layer-merged props (§5.3.2, via `mergeFieldProps`);
|
|
1187
|
+
* `watchers` is the map of fields currently watching this field (§5.3.1 step 3).
|
|
1188
|
+
*/
|
|
1189
|
+
interface UseFieldReturn {
|
|
1190
|
+
/**
|
|
1191
|
+
* RHF field state from the Controller render callback
|
|
1192
|
+
* (`invalid` / `isTouched` / `isDirty` / `error` / …).
|
|
1193
|
+
*/
|
|
1194
|
+
fieldState: ControllerFieldState;
|
|
1195
|
+
/**
|
|
1196
|
+
* The RAW rendered input (template / component / host element), or `null`
|
|
1197
|
+
* when the field is not visible. The render-prop `children` is applied
|
|
1198
|
+
* INSIDE the Controller render callback that produces this element.
|
|
1199
|
+
*/
|
|
1200
|
+
renderedField: ReactNode;
|
|
1201
|
+
/**
|
|
1202
|
+
* Final merged props passed to the input — the output of the 8-layer
|
|
1203
|
+
* `mergeFieldProps` pipeline (§5.3.2).
|
|
1204
|
+
*/
|
|
1205
|
+
fieldProps: Record<string, unknown>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Map of fields watching this field (`true` = currently subscribed). Populated
|
|
1208
|
+
* via the watcher-setter registered with `useSubscriptions` (§5.3.1 step 3).
|
|
1209
|
+
*/
|
|
1210
|
+
watchers: Record<string, boolean>;
|
|
1211
|
+
/**
|
|
1212
|
+
* RHF form state from the Controller. Typed as `UseFormStateReturn<FieldValues>`
|
|
1213
|
+
* to match `FieldRenderAPI` exactly (NOT bare `UseFormStateReturn`).
|
|
1214
|
+
*/
|
|
1215
|
+
formState: UseFormStateReturn<FieldValues>;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* useField — RHF Controller integration for a single Field (PRD §1.3.3 / §20).
|
|
1219
|
+
*
|
|
1220
|
+
* This hook owns the **entire** Controller lifecycle for one field: it reads
|
|
1221
|
+
* the form/config/group contexts, resolves the input config, sets up watchers
|
|
1222
|
+
* + subscriptions + conditions, builds the setValue effect (ref pattern,
|
|
1223
|
+
* §7.1.1), resolves the disabled/visible/label state, builds the validation
|
|
1224
|
+
* rules + change handler, mounts the `<Controller>` element with its full
|
|
1225
|
+
* render callback (format → state injection → `mergeFieldProps` 8-layer merge →
|
|
1226
|
+
* forwardRef delivery §20.1/§20.4 → template/host/component → render-prop
|
|
1227
|
+
* `children`), and returns the {@link UseFieldReturn}.
|
|
1228
|
+
*
|
|
1229
|
+
* ## Responsibilities (PRD §1.3.3 + §5.3.x + §20)
|
|
1230
|
+
*
|
|
1231
|
+
* - **Controller integration** — wraps RHF's `<Controller>` render callback.
|
|
1232
|
+
* The `<Controller>` mounts ONLY when the field is visible (the
|
|
1233
|
+
* `renderedField` is `null` when hidden), preserving the
|
|
1234
|
+
* conditionally-hidden-field invariant (hidden fields are NOT RHF-registered).
|
|
1235
|
+
* - **Parse/format pipeline (§5.3.5)** — Core `parse` / `format` from
|
|
1236
|
+
* `@formality-ui/core` (`transform/pipeline`).
|
|
1237
|
+
* - **Change handler (§5.3.6)** — `changeField` + watcher fan-out.
|
|
1238
|
+
* - **Validation (§5.3.7)** — Core `runValidator` / `resolveErrorMessage`
|
|
1239
|
+
* (`validation/validate`).
|
|
1240
|
+
* - **forwardRef delivery (§20.1)** — delivers the input's ref via the merged
|
|
1241
|
+
* props' `forwardRef` key (the CURRENT implemented behavior — NOT the legacy
|
|
1242
|
+
* `ref` key shown in the stale §5.3.2 pseudo-code). The host-element path
|
|
1243
|
+
* (§20.4 narrow exception) translates `forwardRef` back into React's special
|
|
1244
|
+
* `ref` key for string components.
|
|
1245
|
+
* - **Props merge (§5.3.2)** — `resolveInputConfig` + `resolveLabel` +
|
|
1246
|
+
* `mergeFieldProps` (the 8-layer merge).
|
|
1247
|
+
* - **Template/host render (§5.3.8)** — produces the RAW `renderedField`.
|
|
1248
|
+
*
|
|
1249
|
+
* It consumes these context hooks (`useFormContext`, `useConfigContext`,
|
|
1250
|
+
* `useGroupContext`) and composed hooks (`useConditions`, `usePropsEvaluation`,
|
|
1251
|
+
* `useInferredInputs`, `useSubscriptions`), and the Core fns above.
|
|
1252
|
+
*
|
|
1253
|
+
* NOTE: field **registration** (`registerField`/`unregisterField`) is OWNED by
|
|
1254
|
+
* the `<Field>` component (contract bullet b), NOT this hook. The hook reads
|
|
1255
|
+
* `useFormContext()` for everything else (config, methods, changeField,
|
|
1256
|
+
* setFieldValidating, register/unregisterWatcherSetter).
|
|
1257
|
+
*
|
|
1258
|
+
* @param params - Field parameters (see {@link UseFieldParams}).
|
|
1259
|
+
* @returns The Controller-produced state + the RAW rendered input
|
|
1260
|
+
* (see {@link UseFieldReturn}).
|
|
1261
|
+
*
|
|
1262
|
+
* @example
|
|
1263
|
+
* ```tsx
|
|
1264
|
+
* // Direct hook usage (custom layout):
|
|
1265
|
+
* function MyField() {
|
|
1266
|
+
* const { renderedField, fieldState } = useField({ name: "email" });
|
|
1267
|
+
* return <div className={fieldState.error ? "has-error" : ""}>{renderedField}</div>;
|
|
1268
|
+
* }
|
|
1269
|
+
*
|
|
1270
|
+
* // Equivalent via the thin wrapper component:
|
|
1271
|
+
* <Field name="email" />;
|
|
1272
|
+
* ```
|
|
1273
|
+
*/
|
|
1274
|
+
declare function useField<TName extends string = string>({ name, type: typeProp, disabled: disabledProp, hidden: hiddenProp, children, inputConfig: inputConfigProp, ...restProps }: UseFieldParams<TName>): UseFieldReturn;
|
|
1275
|
+
|
|
1276
|
+
export { ConfigContext, type ConfigContextValue, type CustomFieldState, type DebouncedFunction, type ExtendedFormState, Field, FieldGroup, type FieldGroupProps, type FieldProps, type FieldRenderAPI, Form, FormContext, type FormContextValue, type FormProps, type FormRenderAPI, type FormalityFieldComponentProps, FormalityProvider, type FormalityProviderProps, GroupContext, type GroupContextValue, type GroupState, type InputTemplateProps, type ReactFieldConfig, type ReactFormFieldsConfig, type ReactInputConfig, UnusedFields, type UnusedFieldsProps, type UseFieldParams, type UseFieldReturn, type UseFormStateOptions, type WatcherSetterFn, defineInputs, makeDeepProxyState, makeProxyState, useConditions, useConfigContext, useField, useFormContext, useFormState, useGroupContext, useInferredInputs, usePropsEvaluation, useSubscriptions };
|
package/dist/index.js
CHANGED
|
@@ -162,12 +162,18 @@ function Form({
|
|
|
162
162
|
return result;
|
|
163
163
|
}, [providerConfig.inputs, formConfig.inputs]);
|
|
164
164
|
const defaultValues = useMemo(() => {
|
|
165
|
-
|
|
165
|
+
const resolved = resolveAllInitialValues(config, mergedInputs, record ?? {});
|
|
166
|
+
const baseline = { ...record ?? {}, ...resolved };
|
|
167
|
+
for (const fieldName of Object.keys(config)) {
|
|
168
|
+
if (!(fieldName in baseline)) baseline[fieldName] = void 0;
|
|
169
|
+
}
|
|
170
|
+
return baseline;
|
|
166
171
|
}, [config, mergedInputs, record]);
|
|
167
172
|
const methods = useForm({
|
|
168
173
|
mode: mode ?? "onChange",
|
|
169
174
|
defaultValues,
|
|
170
|
-
values:
|
|
175
|
+
values: defaultValues
|
|
176
|
+
// was: `record as any` — see baseline comment above
|
|
171
177
|
});
|
|
172
178
|
const fieldRegistry = useRef(/* @__PURE__ */ new Set());
|
|
173
179
|
const [registeredFields, setRegisteredFields] = useState(
|
|
@@ -333,8 +339,10 @@ function Form({
|
|
|
333
339
|
}, [methods, config, record, defaultValues]);
|
|
334
340
|
const handleSubmit = useCallback(
|
|
335
341
|
async (values, overrideOnSubmit) => {
|
|
336
|
-
for (const [, isValidating] of validatingFields.current) {
|
|
337
|
-
if (isValidating)
|
|
342
|
+
for (const [fieldName, isValidating] of validatingFields.current) {
|
|
343
|
+
if (!isValidating) continue;
|
|
344
|
+
const subscribers = invertedSubscriptions.current.get(fieldName);
|
|
345
|
+
if (subscribers && subscribers.size > 0) return;
|
|
338
346
|
}
|
|
339
347
|
if (validate) {
|
|
340
348
|
const errors = await validate(values);
|
|
@@ -932,13 +940,12 @@ function useSubscriptions(fieldName, subscriptions) {
|
|
|
932
940
|
};
|
|
933
941
|
}, [fieldName, subscriptions, addSubscription, removeSubscription]);
|
|
934
942
|
}
|
|
935
|
-
function
|
|
943
|
+
function useField({
|
|
936
944
|
name,
|
|
937
945
|
type: typeProp,
|
|
938
946
|
disabled: disabledProp,
|
|
939
947
|
hidden: hiddenProp,
|
|
940
948
|
children,
|
|
941
|
-
shouldRegister = true,
|
|
942
949
|
inputConfig: inputConfigProp,
|
|
943
950
|
...restProps
|
|
944
951
|
}) {
|
|
@@ -946,8 +953,6 @@ function Field({
|
|
|
946
953
|
config,
|
|
947
954
|
formConfig,
|
|
948
955
|
methods,
|
|
949
|
-
registerField,
|
|
950
|
-
unregisterField,
|
|
951
956
|
registerWatcherSetter,
|
|
952
957
|
unregisterWatcherSetter,
|
|
953
958
|
changeField,
|
|
@@ -976,12 +981,6 @@ function Field({
|
|
|
976
981
|
};
|
|
977
982
|
return inputConfigProp ? { ...baseInputConfig, ...inputConfigProp } : baseInputConfig;
|
|
978
983
|
}, [type, providerConfig.inputs, formConfig.inputs, inputConfigProp]);
|
|
979
|
-
useEffect(() => {
|
|
980
|
-
if (shouldRegister) {
|
|
981
|
-
registerField(name);
|
|
982
|
-
return () => unregisterField(name);
|
|
983
|
-
}
|
|
984
|
-
}, [name, shouldRegister, registerField, unregisterField]);
|
|
985
984
|
const [watchers, setWatchers] = useState({});
|
|
986
985
|
useEffect(() => {
|
|
987
986
|
registerWatcherSetter(name, setWatchers);
|
|
@@ -1194,10 +1193,14 @@ function Field({
|
|
|
1194
1193
|
inputConfig
|
|
1195
1194
|
]
|
|
1196
1195
|
);
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1196
|
+
const fieldStateRef = useRef(
|
|
1197
|
+
{}
|
|
1198
|
+
);
|
|
1199
|
+
const formStateRef = useRef(
|
|
1200
|
+
{}
|
|
1201
|
+
);
|
|
1202
|
+
const fieldPropsRef = useRef({});
|
|
1203
|
+
const renderedField = isVisible ? /* @__PURE__ */ jsx(
|
|
1201
1204
|
Controller,
|
|
1202
1205
|
{
|
|
1203
1206
|
control: methods.control,
|
|
@@ -1248,13 +1251,16 @@ function Field({
|
|
|
1248
1251
|
...stateInjection
|
|
1249
1252
|
}
|
|
1250
1253
|
});
|
|
1254
|
+
fieldStateRef.current = fieldState;
|
|
1255
|
+
formStateRef.current = formState;
|
|
1256
|
+
fieldPropsRef.current = finalProps;
|
|
1251
1257
|
const Component = inputConfig.component;
|
|
1252
1258
|
const isHostComponent = typeof inputConfig.component === "string";
|
|
1253
1259
|
const template = inputConfig.template ?? providerConfig.inputTemplates[type] ?? providerConfig.defaultInputTemplate;
|
|
1254
1260
|
const TemplateComponent = template;
|
|
1255
|
-
let
|
|
1261
|
+
let renderedFieldEl;
|
|
1256
1262
|
if (TemplateComponent) {
|
|
1257
|
-
|
|
1263
|
+
renderedFieldEl = /* @__PURE__ */ jsx(
|
|
1258
1264
|
TemplateComponent,
|
|
1259
1265
|
{
|
|
1260
1266
|
Field: Component,
|
|
@@ -1277,27 +1283,64 @@ function Field({
|
|
|
1277
1283
|
strippedHostProps[key] = value;
|
|
1278
1284
|
}
|
|
1279
1285
|
}
|
|
1280
|
-
|
|
1286
|
+
renderedFieldEl = createElement(inputConfig.component, {
|
|
1281
1287
|
...strippedHostProps,
|
|
1282
1288
|
ref: finalProps.forwardRef
|
|
1283
1289
|
});
|
|
1284
1290
|
} else {
|
|
1285
|
-
|
|
1291
|
+
renderedFieldEl = /* @__PURE__ */ jsx(Component, { ...finalProps });
|
|
1286
1292
|
}
|
|
1287
1293
|
if (typeof children === "function") {
|
|
1288
1294
|
const result = children({
|
|
1289
1295
|
fieldState,
|
|
1290
|
-
renderedField,
|
|
1296
|
+
renderedField: renderedFieldEl,
|
|
1291
1297
|
fieldProps: finalProps,
|
|
1292
1298
|
watchers,
|
|
1293
1299
|
formState
|
|
1294
1300
|
});
|
|
1295
1301
|
return /* @__PURE__ */ jsx(Fragment, { children: result });
|
|
1296
1302
|
}
|
|
1297
|
-
return
|
|
1303
|
+
return renderedFieldEl;
|
|
1298
1304
|
}
|
|
1299
1305
|
}
|
|
1300
|
-
);
|
|
1306
|
+
) : null;
|
|
1307
|
+
return {
|
|
1308
|
+
fieldState: fieldStateRef.current,
|
|
1309
|
+
renderedField,
|
|
1310
|
+
fieldProps: fieldPropsRef.current,
|
|
1311
|
+
watchers,
|
|
1312
|
+
formState: formStateRef.current
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// src/components/Field.tsx
|
|
1317
|
+
function Field({
|
|
1318
|
+
name,
|
|
1319
|
+
type: typeProp,
|
|
1320
|
+
disabled: disabledProp,
|
|
1321
|
+
hidden: hiddenProp,
|
|
1322
|
+
children,
|
|
1323
|
+
shouldRegister = true,
|
|
1324
|
+
inputConfig: inputConfigProp,
|
|
1325
|
+
...restProps
|
|
1326
|
+
}) {
|
|
1327
|
+
const { registerField, unregisterField } = useFormContext();
|
|
1328
|
+
useEffect(() => {
|
|
1329
|
+
if (shouldRegister) {
|
|
1330
|
+
registerField(name);
|
|
1331
|
+
return () => unregisterField(name);
|
|
1332
|
+
}
|
|
1333
|
+
}, [name, shouldRegister, registerField, unregisterField]);
|
|
1334
|
+
const { renderedField } = useField({
|
|
1335
|
+
name,
|
|
1336
|
+
type: typeProp,
|
|
1337
|
+
disabled: disabledProp,
|
|
1338
|
+
hidden: hiddenProp,
|
|
1339
|
+
children,
|
|
1340
|
+
inputConfig: inputConfigProp,
|
|
1341
|
+
...restProps
|
|
1342
|
+
});
|
|
1343
|
+
return renderedField;
|
|
1301
1344
|
}
|
|
1302
1345
|
function FieldGroup({ name, children }) {
|
|
1303
1346
|
const { formConfig } = useFormContext();
|
|
@@ -1441,6 +1484,6 @@ function defineInputs(inputs) {
|
|
|
1441
1484
|
return inputs;
|
|
1442
1485
|
}
|
|
1443
1486
|
|
|
1444
|
-
export { ConfigContext, Field, FieldGroup, Form, FormContext, FormalityProvider, GroupContext, UnusedFields, defineInputs, makeDeepProxyState, makeProxyState, useConditions, useConfigContext, useFormContext, useFormState, useGroupContext, useInferredInputs, usePropsEvaluation, useSubscriptions };
|
|
1487
|
+
export { ConfigContext, Field, FieldGroup, Form, FormContext, FormalityProvider, GroupContext, UnusedFields, defineInputs, makeDeepProxyState, makeProxyState, useConditions, useConfigContext, useField, useFormContext, useFormState, useGroupContext, useInferredInputs, usePropsEvaluation, useSubscriptions };
|
|
1445
1488
|
//# sourceMappingURL=index.js.map
|
|
1446
1489
|
//# sourceMappingURL=index.js.map
|