@formality-ui/react 0.0.0 → 0.2.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.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { FieldError, InputConfig, ValidatorsConfig, ErrorMessagesConfig, SelectValue, FormFieldsConfig, FormConfig, FormState, ConditionDescriptor, GroupConfig, ConditionResult } from '@formality-ui/core';
2
- export { ConditionDescriptor, ConditionResult, ErrorMessagesConfig, FieldConfig, FieldError, FieldState, FormConfig, FormFieldsConfig, FormState, FormalityProviderConfig, GroupConfig, InputConfig, SelectFunction, SelectValue, ValidationResult, ValidatorFunction, ValidatorSpec, ValidatorsConfig } from '@formality-ui/core';
1
+ import { FieldError, InputConfig, FieldConfig, ValidatorsConfig, ErrorMessagesConfig, SelectValue, FormFieldsConfig, FormConfig, FormState, ConditionDescriptor, GroupConfig, ConditionResult } from '@formality-ui/core';
2
+ export { ConditionDescriptor, ConditionResult, ErrorMessagesConfig, FieldConfig, FieldError, FieldState, FormConfig, FormFieldsConfig, FormState, FormalityProviderConfig, GroupConfig, InputConfig, SelectFunction, SelectValue, ValidationResult, ValidatorEntry, ValidatorFactory, ValidatorFunction, ValidatorSpec, ValidatorsConfig } from '@formality-ui/core';
3
3
  import * as react from 'react';
4
4
  import { ComponentType, ReactNode } from 'react';
5
- import { ControllerFieldState, UseFormStateReturn, FieldValues, UseFormReturn } from 'react-hook-form';
5
+ import { ControllerFieldState, UseFormStateReturn, FieldValues, RegisterOptions, RefCallBack, UseFormReturn } from 'react-hook-form';
6
+ export { FieldValues, RefCallBack, UseFormStateReturn } from 'react-hook-form';
6
7
 
7
8
  /**
8
9
  * Props passed to input template components
@@ -96,6 +97,154 @@ interface DebouncedFunction {
96
97
  pending: () => boolean;
97
98
  }
98
99
 
100
+ /**
101
+ * `InputConfig` as seen by React consumers.
102
+ *
103
+ * Narrows core's framework-agnostic `component: unknown` /
104
+ * `template?: unknown` to real React component types. `TValue` is preserved so
105
+ * `defaultValue` / `parser` / `formatter` can share a single value type.
106
+ *
107
+ * @example
108
+ * ```tsx
109
+ * import type { ReactInputConfig } from "@formality-ui/react";
110
+ *
111
+ * const textField = {
112
+ * component: TextField,
113
+ * defaultValue: "",
114
+ * } satisfies ReactInputConfig<string>;
115
+ * ```
116
+ *
117
+ * Prefer this over the core `InputConfig` type whenever you are writing React
118
+ * code — it will give you a **compile error** if `component` is set to a
119
+ * non-component (e.g. `component: 42` or `component: "textField"`).
120
+ */
121
+ interface ReactInputConfig<TValue = unknown> extends Omit<InputConfig<TValue>, "component" | "template"> {
122
+ /** The React component to render for this input type. */
123
+ component: ComponentType<any>;
124
+ /** Optional template wrapper (overrides provider/form defaults). */
125
+ template?: ComponentType<InputTemplateProps>;
126
+ }
127
+ /**
128
+ * `FieldConfig` as seen by React consumers.
129
+ *
130
+ * Narrows core's framework-agnostic `rules?: Record<string, unknown>` to
131
+ * react-hook-form's `RegisterOptions`, giving autocomplete and checking for
132
+ * `required`, `min`, `max`, `pattern`, `validate`, `valueAsNumber`, `deps`, …
133
+ *
134
+ * The generic `V` defaults to `FieldValues`; pass your form's values type for
135
+ * slightly tighter checking on path-based rules.
136
+ */
137
+ interface ReactFieldConfig<V extends FieldValues = FieldValues> extends Omit<FieldConfig, "rules"> {
138
+ /** react-hook-form register options forwarded to the field's Controller. */
139
+ rules?: RegisterOptions<V>;
140
+ }
141
+ /**
142
+ * Map of field name → {@link ReactFieldConfig}. Used by `<Form>`'s `config`
143
+ * prop (via {@link ReactFormFieldsConfig}) so React consumers get RHF-typed
144
+ * `rules` on every field.
145
+ *
146
+ * The keys are narrowed to `Extract<keyof V, string>`. When `V` is left at
147
+ * its default (`FieldValues`) any string key is accepted — byte-for-byte
148
+ * identical to before. When `V` is narrowed to a concrete field-values type
149
+ * (e.g. via `<Form<ClientValues>>`), unknown `config` keys become a
150
+ * **compile error**, catching typos like `ofice` at compile time (PRD §C.4 /
151
+ * T2.1).
152
+ *
153
+ * @example
154
+ * ```tsx
155
+ * import type { ReactFormFieldsConfig } from "@formality-ui/react";
156
+ * import type { FieldValues } from "react-hook-form";
157
+ *
158
+ * // Default — any string key accepted (non-breaking).
159
+ * const a: ReactFormFieldsConfig<FieldValues> = { anything: { type: "text" } };
160
+ *
161
+ * // Narrowed — only known field names accepted.
162
+ * type ClientValues = { name: string; email: string };
163
+ * const b: ReactFormFieldsConfig<ClientValues> = {
164
+ * name: { type: "text" },
165
+ * email: { type: "text" },
166
+ * };
167
+ *
168
+ * // @ts-expect-error — typo `ofice` is rejected.
169
+ * const c: ReactFormFieldsConfig<ClientValues> = { ofice: { type: "text" } };
170
+ * ```
171
+ */
172
+ type ReactFormFieldsConfig<V extends FieldValues = FieldValues> = Record<Extract<keyof V, string>, ReactFieldConfig<V>>;
173
+ /**
174
+ * Identity helper that lets consumers derive a union of their input-type keys.
175
+ * Opt-in: wrap your provider inputs to get `keyof` checking on `Field` `type`
176
+ * and `FieldConfig.type`.
177
+ *
178
+ * This is a PURE IDENTITY function — it returns `inputs` unchanged with zero
179
+ * runtime effect, so bundlers (tsup/esbuild/rollup) tree-shake it to nothing.
180
+ * It exists purely so consumers can write `type InputType = keyof typeof inputs`.
181
+ *
182
+ * The existing non-generic `Field` and `FieldConfig.type` / `FieldProps.type`
183
+ * (which default to `type?: string`) are UNCHANGED — this helper is purely
184
+ * additive and opt-in. End-to-end wiring of `InputType` into those types is a
185
+ * follow-up (PRD §C.4 T2.2 step 3).
186
+ *
187
+ * @example
188
+ * ```tsx
189
+ * import { defineInputs } from "@formality-ui/react";
190
+ *
191
+ * const inputs = defineInputs({
192
+ * textField: { component: TextField, defaultValue: "" },
193
+ * switch: { component: Switch, defaultValue: false },
194
+ * });
195
+ * export type InputType = keyof typeof inputs; // "textField" | "switch"
196
+ * ```
197
+ */
198
+ declare function defineInputs<T extends Record<string, ReactInputConfig>>(inputs: T): T;
199
+ /**
200
+ * Props Formality injects onto every field component.
201
+ *
202
+ * `<Field>` renders your input component via React Hook Form's `<Controller>`.
203
+ * At runtime Formality merges a `coreProps` bundle onto the component (name,
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).
209
+ *
210
+ * **Destructure before forwarding.** Component authors MUST destructure
211
+ * `state`, `formState`, and `forwardRef` OUT of props before spreading the
212
+ * rest onto the underlying DOM `<input>` — otherwise these non-DOM props leak
213
+ * to the DOM and React warns. Recommended pattern:
214
+ *
215
+ * ```tsx
216
+ * const TextField: ComponentType<FormalityFieldComponentProps<TextFieldProps>> =
217
+ * ({ state, formState, forwardRef, ...domProps }) => (
218
+ * <input ref={forwardRef} {...domProps} />
219
+ * );
220
+ * ```
221
+ *
222
+ * **Wiring `forwardRef` to the inner input.** `forwardRef` is RHF's
223
+ * `RefCallBack` (a function). For a plain `<input>` use `ref={forwardRef}`.
224
+ * For MUI v9 components that no longer accept a top-level `inputRef`, wire it
225
+ * via slots: `slotProps={{ input: { ref: forwardRef } }}` (PRD §5.3.8).
226
+ *
227
+ * **Runtime delivery (important).** `<Field>` delivers RHF's ref as a regular,
228
+ * top-level, enumerable prop named `forwardRef` — no `React.forwardRef` wrap
229
+ * is required for a plain function component that destructures `forwardRef`
230
+ * and wires it to the inner input (`ref={forwardRef}`). Consumers migrating
231
+ * off the old React-special `ref` key: a `React.forwardRef`-wrapped component
232
+ * should consume `props.forwardRef` (PRD §20.4), and under React 19
233
+ * ref-as-prop use `forwardRef` directly. The type ships the intended contract
234
+ * so consumers stop hand-rolling a lossy `WithFormality<P>`.
235
+ *
236
+ * @template P - the field component's own props (e.g. TextFieldProps). Defaults
237
+ * to `unknown` so existing `ComponentType<any>` casts remain valid.
238
+ */
239
+ type FormalityFieldComponentProps<P = unknown> = P & {
240
+ /** Subscribed/own field state when `provideState`/`passSubscriptions` is on. */
241
+ state?: CustomFieldState | Record<string, CustomFieldState>;
242
+ /** React Hook Form form state threaded from `<Controller>`. */
243
+ formState?: UseFormStateReturn<FieldValues>;
244
+ /** RHF ref callback (`RefCallBack`); wire to the inner input (see JSDoc). */
245
+ forwardRef?: RefCallBack;
246
+ };
247
+
99
248
  /**
100
249
  * ConfigContextValue - Global configuration provided by FormalityProvider
101
250
  *
@@ -104,7 +253,7 @@ interface DebouncedFunction {
104
253
  */
105
254
  interface ConfigContextValue {
106
255
  /** Input type definitions (e.g., textField, switch, autocomplete) */
107
- inputs: Record<string, InputConfig>;
256
+ inputs: Record<string, ReactInputConfig>;
108
257
  /** Named formatters for value → display transformation */
109
258
  formatters: Record<string, (value: unknown) => unknown>;
110
259
  /** Named parsers for input → value transformation */
@@ -188,8 +337,9 @@ interface FormContextValue<TFieldValues extends FieldValues = FieldValues> {
188
337
  * Programmatically change a field's value
189
338
  * @param name - Field name
190
339
  * @param value - New value
340
+ * @param inputConfig - Optional input configuration for this field type
191
341
  */
192
- changeField: (name: string, value: unknown) => void;
342
+ changeField: (name: string, value: unknown, inputConfig?: InputConfig) => void;
193
343
  /**
194
344
  * Set a field's validating state
195
345
  * @param name - Field name
@@ -299,8 +449,12 @@ interface FormalityProviderProps {
299
449
  * switch: { component: Switch, defaultValue: false },
300
450
  * }}
301
451
  * ```
452
+ *
453
+ * `component` is type-checked as a real React component (a non-component
454
+ * value such as `42` or `"textField"` is a compile error). See
455
+ * `ReactInputConfig` for the precise shape.
302
456
  */
303
- inputs: Record<string, InputConfig>;
457
+ inputs: Record<string, ReactInputConfig>;
304
458
  /**
305
459
  * Named formatters for value → display transformation
306
460
  *
@@ -418,7 +572,7 @@ interface FormProps<TFieldValues extends FieldValues = FieldValues> {
418
572
  /** Form content - can be static children or render function */
419
573
  children: ReactNode | ((api: FormRenderAPI<TFieldValues>) => ReactNode);
420
574
  /** Field configurations */
421
- config: FormFieldsConfig;
575
+ config: ReactFormFieldsConfig<TFieldValues>;
422
576
  /** Form-level configuration (title, groups, input overrides) */
423
577
  formConfig?: FormConfig;
424
578
  /** Submit handler */
@@ -427,8 +581,8 @@ interface FormProps<TFieldValues extends FieldValues = FieldValues> {
427
581
  record?: Partial<TFieldValues>;
428
582
  /** Enable auto-save on field changes */
429
583
  autoSave?: boolean;
430
- /** Debounce milliseconds for auto-save (default: 1000) */
431
- debounce?: number;
584
+ /** Debounce milliseconds for auto-save. false = immediate submission, number = delay in milliseconds (default: 1000) */
585
+ debounce?: number | false;
432
586
  /** Form-level validation */
433
587
  validate?: (values: Partial<TFieldValues>) => Record<string, string> | Promise<Record<string, string>>;
434
588
  }
@@ -439,9 +593,19 @@ interface FormRenderAPI<TFieldValues extends FieldValues = FieldValues> {
439
593
  /** Fields in config but not rendered */
440
594
  unusedFields: string[];
441
595
  /** React Hook Form formState */
442
- formState: UseFormReturn<TFieldValues>['formState'];
596
+ formState: UseFormReturn<TFieldValues>["formState"];
443
597
  /** React Hook Form methods */
444
598
  methods: UseFormReturn<TFieldValues>;
599
+ /**
600
+ * Submit handler that runs the full Formality submission pipeline.
601
+ *
602
+ * Use this in place of `methods.handleSubmit` to ensure form-level
603
+ * `validate` and `transformValuesForSubmit` (valueField extraction +
604
+ * getSubmitField rename) are applied before `onSubmit` receives the values.
605
+ * `methods.handleSubmit` remains available as a raw RHF escape hatch but
606
+ * bypasses those transforms.
607
+ */
608
+ handleSubmit: (onSubmit: (values: Partial<TFieldValues>) => void | Promise<void>) => (e?: React.BaseSyntheticEvent) => Promise<void>;
445
609
  /** Resolved form title (static or evaluated) */
446
610
  resolvedTitle?: string;
447
611
  }
@@ -473,11 +637,42 @@ interface FormRenderAPI<TFieldValues extends FieldValues = FieldValues> {
473
637
  declare function Form<TFieldValues extends FieldValues = FieldValues>({ children, config, formConfig, onSubmit, record, autoSave, debounce: debounceMs, validate, }: FormProps<TFieldValues>): JSX.Element;
474
638
 
475
639
  /**
476
- * Field component props
640
+ * Field component props.
641
+ *
642
+ * `FieldProps` is generic over the field `name`:
643
+ * `FieldProps<TName extends string = string>`. The default `TName = string`
644
+ * means `<Field name={anyString} />` compiles unchanged (no migration) and a
645
+ * bare `FieldProps` is identical to `FieldProps<string>`.
646
+ *
647
+ * To get compile-time checking of the field name, narrow `TName` explicitly —
648
+ * e.g. `FieldProps<"name" | "email">` or a wrapper that threads a
649
+ * `keyof ClientValues & string`. With a narrowed `TName`, a typo like
650
+ * `name="ofice"` is rejected at compile time instead of silently rendering
651
+ * nothing (the second half of PRD §C.4 / T2.1's "silent no-op" fix).
652
+ *
653
+ * Automatic per-form narrowing — where a `<Field>` automatically narrows its
654
+ * `name` against the enclosing `<Form<TFieldValues>>`'s key set — is a planned
655
+ * follow-up (PRD §C.4 T2.1) and is explicitly deferrable.
656
+ *
657
+ * @example
658
+ * ```tsx
659
+ * // Default usage — any string name compiles (unchanged behavior):
660
+ * <Field name="email" />;
661
+ *
662
+ * // Opt-in strict usage — typo names are compile errors:
663
+ * type Names = "name" | "email";
664
+ * const props: FieldProps<Names> = { name: "email" };
665
+ * ```
477
666
  */
478
- interface FieldProps {
479
- /** Field name (must match a key in Form's config) */
480
- name: string;
667
+ interface FieldProps<TName extends string = string> {
668
+ /**
669
+ * Field name (must match a key in Form's config).
670
+ *
671
+ * When `FieldProps` is narrowed (e.g. `FieldProps<"name" | "email">`), the
672
+ * name is checked against `TName` at compile time. With the default
673
+ * (`FieldProps` / `FieldProps<string>`), any string is accepted.
674
+ */
675
+ name: TName;
481
676
  /** Override the input type from config */
482
677
  type?: string;
483
678
  /** Override disabled state */
@@ -488,6 +683,8 @@ interface FieldProps {
488
683
  children?: ReactNode | ((api: FieldRenderAPI) => ReactNode);
489
684
  /** Whether to register this field in Form's field registry (default: true) */
490
685
  shouldRegister?: boolean;
686
+ /** Override input config for this field (e.g., debounce setting) */
687
+ inputConfig?: Partial<InputConfig>;
491
688
  /** Additional props to pass to the input component */
492
689
  [key: string]: unknown;
493
690
  }
@@ -535,7 +732,7 @@ interface FieldRenderAPI {
535
732
  * </Field>
536
733
  * ```
537
734
  */
538
- declare function Field({ name, type: typeProp, disabled: disabledProp, hidden: hiddenProp, children, shouldRegister, ...restProps }: FieldProps): JSX.Element | null;
735
+ declare function Field<TName extends string = string>({ name, type: typeProp, disabled: disabledProp, hidden: hiddenProp, children, shouldRegister, inputConfig: inputConfigProp, ...restProps }: FieldProps<TName>): JSX.Element | null;
539
736
 
540
737
  /**
541
738
  * FieldGroup component props
@@ -734,6 +931,8 @@ interface UseConditionsOptions {
734
931
  subscribesTo?: string[];
735
932
  /** Additional props for expression context */
736
933
  props?: Record<string, unknown>;
934
+ /** Optional: All field configs for computing disabled states in fieldStates */
935
+ allFieldsConfig?: FormFieldsConfig;
737
936
  }
738
937
  /**
739
938
  * Evaluates conditions against current field values
@@ -748,6 +947,11 @@ interface UseConditionsOptions {
748
947
  * - visible: AND logic (any false = hidden)
749
948
  * - setValue: last matching condition wins
750
949
  *
950
+ * Uses two-pass evaluation to populate the disabled property in field states:
951
+ * - Pass 1: Build base field states without disabled (prevents circular dependency)
952
+ * - Pass 2: Compute disabled for each field using Pass 1 states
953
+ * - Pass 3: Merge base states with disabled into final field states
954
+ *
751
955
  * @param options - Conditions and subscription config
752
956
  * @returns Evaluation result with disabled, visible, and setValue states
753
957
  *
@@ -765,16 +969,34 @@ interface UseConditionsOptions {
765
969
  */
766
970
  declare function useConditions(options: UseConditionsOptions): ConditionResult;
767
971
 
972
+ /**
973
+ * Result of evaluating all three prop sources separately
974
+ *
975
+ * Each layer is evaluated independently so that mergeFieldProps
976
+ * can correctly apply the 8-layer priority order.
977
+ */
978
+ interface EvaluatedPropsResult {
979
+ /** Evaluated provider-level selectDefaultFieldProps (layer 7 priority) */
980
+ providerSelectProps: Record<string, unknown>;
981
+ /** Evaluated form-level selectDefaultFieldProps (layer 5 priority) */
982
+ formSelectProps: Record<string, unknown>;
983
+ /** Evaluated field-level selectProps (layer 2 priority) */
984
+ fieldSelectProps: Record<string, unknown>;
985
+ }
768
986
  interface UsePropsEvaluationOptions {
769
987
  /** Dynamic props descriptor to evaluate */
770
988
  selectProps?: SelectValue;
989
+ /** Dynamic default field props from Form config (higher priority) */
990
+ formDefaultFieldProps?: SelectValue;
991
+ /** Dynamic default field props from Provider config (lower priority) */
992
+ providerDefaultFieldProps?: SelectValue;
771
993
  /** Explicit field subscriptions */
772
994
  subscribesTo?: string[];
773
995
  /** Current field name */
774
996
  fieldName: string;
775
997
  }
776
998
  /**
777
- * Evaluates selectProps against current field values
999
+ * Evaluates dynamic props against current field values
778
1000
  *
779
1001
  * This hook:
780
1002
  * 1. Infers which fields to watch from selectProps expressions
@@ -783,7 +1005,12 @@ interface UsePropsEvaluationOptions {
783
1005
  *
784
1006
  * Handles both expression-based selectProps and function-based selectProps.
785
1007
  *
786
- * @param options - selectProps and subscription config
1008
+ * @param options - Hook options including props to evaluate and subscription config
1009
+ * @param {SelectValue} [options.selectProps] - Dynamic props descriptor to evaluate
1010
+ * @param {SelectValue} [options.formDefaultFieldProps] - Dynamic default props from Form config (higher priority)
1011
+ * @param {SelectValue} [options.providerDefaultFieldProps] - Dynamic default props from Provider config (lower priority)
1012
+ * @param {string[]} [options.subscribesTo] - Explicit field subscriptions
1013
+ * @param {string} options.fieldName - Current field name
787
1014
  * @returns Evaluated props object
788
1015
  *
789
1016
  * @example
@@ -798,11 +1025,15 @@ interface UsePropsEvaluationOptions {
798
1025
  * // props.options and props.disabled are evaluated against current state
799
1026
  * ```
800
1027
  */
801
- declare function usePropsEvaluation(options: UsePropsEvaluationOptions): Record<string, unknown>;
1028
+ declare function usePropsEvaluation(options: UsePropsEvaluationOptions): EvaluatedPropsResult;
802
1029
 
803
1030
  interface UseInferredInputsOptions {
804
1031
  /** Dynamic props descriptor to analyze for field references */
805
1032
  selectProps?: SelectValue;
1033
+ /** Form-level default field props to analyze for field references */
1034
+ formDefaultFieldProps?: SelectValue;
1035
+ /** Provider-level default field props to analyze for field references */
1036
+ providerDefaultFieldProps?: SelectValue;
806
1037
  /** Conditions to analyze for field references */
807
1038
  conditions?: ConditionDescriptor[];
808
1039
  /** Explicit field subscriptions */
@@ -857,4 +1088,4 @@ declare function useInferredInputs(options: UseInferredInputsOptions): string[];
857
1088
  */
858
1089
  declare function useSubscriptions(fieldName: string, subscriptions: string[]): void;
859
1090
 
860
- 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, FormalityProvider, type FormalityProviderProps, GroupContext, type GroupContextValue, type GroupState, type InputTemplateProps, UnusedFields, type UnusedFieldsProps, type UseFormStateOptions, type WatcherSetterFn, makeDeepProxyState, makeProxyState, useConditions, useConfigContext, useFormContext, useFormState, useGroupContext, useInferredInputs, usePropsEvaluation, useSubscriptions };
1091
+ 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 UseFormStateOptions, type WatcherSetterFn, defineInputs, makeDeepProxyState, makeProxyState, useConditions, useConfigContext, useFormContext, useFormState, useGroupContext, useInferredInputs, usePropsEvaluation, useSubscriptions };