@formality-ui/react 0.0.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/README.md +441 -0
- package/dist/index.cjs +1047 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +860 -0
- package/dist/index.d.ts +860 -0
- package/dist/index.js +1028 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
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';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ComponentType, ReactNode } from 'react';
|
|
5
|
+
import { ControllerFieldState, UseFormStateReturn, FieldValues, UseFormReturn } from 'react-hook-form';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Props passed to input template components
|
|
9
|
+
*
|
|
10
|
+
* Templates wrap input components to provide consistent styling,
|
|
11
|
+
* labels, error display, etc.
|
|
12
|
+
*/
|
|
13
|
+
interface InputTemplateProps {
|
|
14
|
+
/** The input component to render */
|
|
15
|
+
Field: ComponentType<any>;
|
|
16
|
+
/** Merged props to pass to the component */
|
|
17
|
+
fieldProps: Record<string, unknown>;
|
|
18
|
+
/** Current field state from Controller */
|
|
19
|
+
fieldState: ControllerFieldState;
|
|
20
|
+
/** Current form state from RHF */
|
|
21
|
+
formState: UseFormStateReturn<FieldValues>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Custom field state with proxy-optimized properties
|
|
25
|
+
*
|
|
26
|
+
* This is the Formality-enhanced field state that includes
|
|
27
|
+
* all necessary properties for expressions and conditions.
|
|
28
|
+
*/
|
|
29
|
+
interface CustomFieldState {
|
|
30
|
+
/** Current field value */
|
|
31
|
+
value: unknown;
|
|
32
|
+
/** Has the field been touched (focused then blurred) */
|
|
33
|
+
isTouched: boolean;
|
|
34
|
+
/** Has the value changed from default */
|
|
35
|
+
isDirty: boolean;
|
|
36
|
+
/** Is async validation currently running */
|
|
37
|
+
isValidating: boolean;
|
|
38
|
+
/** Current validation error (if any) */
|
|
39
|
+
error?: FieldError;
|
|
40
|
+
/** Inverse of valid state for convenience */
|
|
41
|
+
invalid: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extended form state with Formality-specific additions
|
|
45
|
+
*
|
|
46
|
+
* Combines RHF's form state with proxy-wrapped field states
|
|
47
|
+
* and the original record for expression access.
|
|
48
|
+
*/
|
|
49
|
+
interface ExtendedFormState<TFieldValues extends FieldValues = FieldValues> extends UseFormStateReturn<TFieldValues> {
|
|
50
|
+
/** Proxy-wrapped field states for each field */
|
|
51
|
+
fields: Record<string, CustomFieldState>;
|
|
52
|
+
/** Original record passed to Form (for expression access) */
|
|
53
|
+
record: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Isolated form state for performance-critical subscriptions
|
|
57
|
+
*
|
|
58
|
+
* This is a lightweight version of ExtendedFormState that does NOT
|
|
59
|
+
* subscribe to the entire RHF form state. It only contains:
|
|
60
|
+
* - Fields you explicitly subscribed to
|
|
61
|
+
* - The record object
|
|
62
|
+
*
|
|
63
|
+
* Use this when you need to watch specific fields without causing
|
|
64
|
+
* re-renders when other fields change.
|
|
65
|
+
*/
|
|
66
|
+
interface IsolatedFormState {
|
|
67
|
+
/** Proxy-wrapped field states for watched fields only */
|
|
68
|
+
fields: Record<string, CustomFieldState>;
|
|
69
|
+
/** Original record passed to Form (for expression access) */
|
|
70
|
+
record: Record<string, unknown>;
|
|
71
|
+
/** Minimal form-level flags (not reactive) */
|
|
72
|
+
isDirty: boolean;
|
|
73
|
+
isTouched: boolean;
|
|
74
|
+
isValid: boolean;
|
|
75
|
+
isSubmitting: boolean;
|
|
76
|
+
errors: Record<string, unknown>;
|
|
77
|
+
touchedFields: Record<string, unknown>;
|
|
78
|
+
dirtyFields: Record<string, unknown>;
|
|
79
|
+
defaultValues: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Watcher setter function type
|
|
83
|
+
*
|
|
84
|
+
* Used by fields to receive updates about which other fields are watching them.
|
|
85
|
+
*/
|
|
86
|
+
type WatcherSetterFn = React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
|
87
|
+
/**
|
|
88
|
+
* Debounced function interface
|
|
89
|
+
*
|
|
90
|
+
* Used for debounced submission and validation.
|
|
91
|
+
*/
|
|
92
|
+
interface DebouncedFunction {
|
|
93
|
+
(): void;
|
|
94
|
+
cancel: () => void;
|
|
95
|
+
flush: () => void;
|
|
96
|
+
pending: () => boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ConfigContextValue - Global configuration provided by FormalityProvider
|
|
101
|
+
*
|
|
102
|
+
* Contains all input types, transformers, validators, and global defaults
|
|
103
|
+
* that apply across all forms in the application.
|
|
104
|
+
*/
|
|
105
|
+
interface ConfigContextValue {
|
|
106
|
+
/** Input type definitions (e.g., textField, switch, autocomplete) */
|
|
107
|
+
inputs: Record<string, InputConfig>;
|
|
108
|
+
/** Named formatters for value → display transformation */
|
|
109
|
+
formatters: Record<string, (value: unknown) => unknown>;
|
|
110
|
+
/** Named parsers for input → value transformation */
|
|
111
|
+
parsers: Record<string, (value: unknown) => unknown>;
|
|
112
|
+
/** Named validators and validator factories */
|
|
113
|
+
validators: ValidatorsConfig;
|
|
114
|
+
/** Error message templates by type key */
|
|
115
|
+
errorMessages: ErrorMessagesConfig;
|
|
116
|
+
/** Default template component for all inputs */
|
|
117
|
+
defaultInputTemplate?: ComponentType<InputTemplateProps>;
|
|
118
|
+
/** Named template components for specific input types */
|
|
119
|
+
inputTemplates: Record<string, ComponentType<InputTemplateProps>>;
|
|
120
|
+
/** Default prop name for passSubscriptions (default: 'state') */
|
|
121
|
+
defaultSubscriptionPropName: string;
|
|
122
|
+
/** Static default props for all fields */
|
|
123
|
+
defaultFieldProps: Record<string, unknown>;
|
|
124
|
+
/** Dynamic default props evaluated per-field */
|
|
125
|
+
selectDefaultFieldProps?: SelectValue;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* ConfigContext - React context for global Formality configuration
|
|
129
|
+
*
|
|
130
|
+
* Provides access to input types, transformers, validators, and
|
|
131
|
+
* global defaults to all forms and fields in the application.
|
|
132
|
+
*/
|
|
133
|
+
declare const ConfigContext: react.Context<ConfigContextValue>;
|
|
134
|
+
/**
|
|
135
|
+
* useConfigContext - Hook to access global configuration
|
|
136
|
+
*
|
|
137
|
+
* @returns The current ConfigContextValue from the nearest FormalityProvider
|
|
138
|
+
*/
|
|
139
|
+
declare function useConfigContext(): ConfigContextValue;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* FormContextValue - Form-level configuration and operations
|
|
143
|
+
*
|
|
144
|
+
* Provides field registration, subscription management, and access
|
|
145
|
+
* to form state for all fields within a Form component.
|
|
146
|
+
*/
|
|
147
|
+
interface FormContextValue<TFieldValues extends FieldValues = FieldValues> {
|
|
148
|
+
/** Field configurations for this form */
|
|
149
|
+
config: FormFieldsConfig;
|
|
150
|
+
/** Form-level configuration (title, groups, input overrides) */
|
|
151
|
+
formConfig: FormConfig;
|
|
152
|
+
/** Original record passed to Form (for expression access) */
|
|
153
|
+
record?: Record<string, unknown>;
|
|
154
|
+
/**
|
|
155
|
+
* Register a field when it mounts
|
|
156
|
+
* @param name - Field name
|
|
157
|
+
*/
|
|
158
|
+
registerField: (name: string) => void;
|
|
159
|
+
/**
|
|
160
|
+
* Unregister a field when it unmounts
|
|
161
|
+
* @param name - Field name
|
|
162
|
+
*/
|
|
163
|
+
unregisterField: (name: string) => void;
|
|
164
|
+
/**
|
|
165
|
+
* Add a subscription from subscriber to target field
|
|
166
|
+
* @param target - The field being watched
|
|
167
|
+
* @param subscriber - The field watching
|
|
168
|
+
*/
|
|
169
|
+
addSubscription: (target: string, subscriber: string) => void;
|
|
170
|
+
/**
|
|
171
|
+
* Remove a subscription from subscriber to target field
|
|
172
|
+
* @param target - The field being watched
|
|
173
|
+
* @param subscriber - The field watching
|
|
174
|
+
*/
|
|
175
|
+
removeSubscription: (target: string, subscriber: string) => void;
|
|
176
|
+
/**
|
|
177
|
+
* Register a setter for a field's watchers state
|
|
178
|
+
* @param name - Field name
|
|
179
|
+
* @param setter - React state setter for watchers
|
|
180
|
+
*/
|
|
181
|
+
registerWatcherSetter: (name: string, setter: WatcherSetterFn) => void;
|
|
182
|
+
/**
|
|
183
|
+
* Unregister a watcher setter when field unmounts
|
|
184
|
+
* @param name - Field name
|
|
185
|
+
*/
|
|
186
|
+
unregisterWatcherSetter: (name: string) => void;
|
|
187
|
+
/**
|
|
188
|
+
* Programmatically change a field's value
|
|
189
|
+
* @param name - Field name
|
|
190
|
+
* @param value - New value
|
|
191
|
+
*/
|
|
192
|
+
changeField: (name: string, value: unknown) => void;
|
|
193
|
+
/**
|
|
194
|
+
* Set a field's validating state
|
|
195
|
+
* @param name - Field name
|
|
196
|
+
* @param isValidating - Whether validation is in progress
|
|
197
|
+
*/
|
|
198
|
+
setFieldValidating: (name: string, isValidating: boolean) => void;
|
|
199
|
+
/**
|
|
200
|
+
* Get the current complete form state
|
|
201
|
+
* Used for expression evaluation and condition checking
|
|
202
|
+
*/
|
|
203
|
+
getFormState: () => FormState;
|
|
204
|
+
/** Optional submit handler passed to Form */
|
|
205
|
+
onSubmit?: (values: Partial<TFieldValues>) => void | Promise<void>;
|
|
206
|
+
/** Debounced submit for auto-save forms */
|
|
207
|
+
debouncedSubmit: DebouncedFunction;
|
|
208
|
+
/** Immediate submit bypassing debounce */
|
|
209
|
+
submitImmediate: () => void;
|
|
210
|
+
/** Fields in config but not rendered (for config-driven forms) */
|
|
211
|
+
unusedFields: string[];
|
|
212
|
+
/** React Hook Form methods passthrough */
|
|
213
|
+
methods: UseFormReturn<TFieldValues>;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* FormContext - React context for form-level state and operations
|
|
217
|
+
*
|
|
218
|
+
* Must be provided by a Form component. No default value as
|
|
219
|
+
* fields cannot function without a parent Form.
|
|
220
|
+
*/
|
|
221
|
+
declare const FormContext: react.Context<FormContextValue<FieldValues> | null>;
|
|
222
|
+
/**
|
|
223
|
+
* useFormContext - Hook to access form-level context
|
|
224
|
+
*
|
|
225
|
+
* @throws Error if used outside a Form component
|
|
226
|
+
* @returns The FormContextValue from the nearest Form component
|
|
227
|
+
*/
|
|
228
|
+
declare function useFormContext<TFieldValues extends FieldValues = FieldValues>(): FormContextValue<TFieldValues>;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* GroupState - Current computed state for a FieldGroup
|
|
232
|
+
*
|
|
233
|
+
* Contains the resolved disabled/visible states from condition
|
|
234
|
+
* evaluation and the conditions/subscriptions used.
|
|
235
|
+
*/
|
|
236
|
+
interface GroupState {
|
|
237
|
+
/** Is the group disabled (propagates to all child fields) */
|
|
238
|
+
isDisabled: boolean;
|
|
239
|
+
/** Is the group visible (propagates to all child fields) */
|
|
240
|
+
isVisible: boolean;
|
|
241
|
+
/** Whether the group has a setValue condition */
|
|
242
|
+
hasSetCondition: boolean;
|
|
243
|
+
/** Value to set on all child fields (from group-level set/selectSet) */
|
|
244
|
+
setValue: unknown;
|
|
245
|
+
/** Conditions from group configuration */
|
|
246
|
+
conditions: ConditionDescriptor[];
|
|
247
|
+
/** Fields this group subscribes to */
|
|
248
|
+
subscriptions: string[];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* GroupContextValue - Group-level configuration and state
|
|
252
|
+
*
|
|
253
|
+
* Provides inherited disabled/visible state and subscription
|
|
254
|
+
* information to nested fields and groups.
|
|
255
|
+
*/
|
|
256
|
+
interface GroupContextValue {
|
|
257
|
+
/** Current computed state for the group */
|
|
258
|
+
state: GroupState;
|
|
259
|
+
/** Inherited subscriptions from parent groups */
|
|
260
|
+
subscriptions: string[];
|
|
261
|
+
/** Field names inferred from group conditions */
|
|
262
|
+
inferredInputs: string[];
|
|
263
|
+
/** Group configuration */
|
|
264
|
+
config: GroupConfig;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* GroupContext - React context for group-level state
|
|
268
|
+
*
|
|
269
|
+
* Provides inherited disabled/visible state from parent groups
|
|
270
|
+
* to child fields and nested groups.
|
|
271
|
+
*/
|
|
272
|
+
declare const GroupContext: react.Context<GroupContextValue>;
|
|
273
|
+
/**
|
|
274
|
+
* useGroupContext - Hook to access group-level context
|
|
275
|
+
*
|
|
276
|
+
* @returns The GroupContextValue from the nearest FieldGroup or default
|
|
277
|
+
*/
|
|
278
|
+
declare function useGroupContext(): GroupContextValue;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* FormalityProviderProps - Props for the FormalityProvider component
|
|
282
|
+
*
|
|
283
|
+
* Configures input types, transformers, validators, and global defaults
|
|
284
|
+
* for all forms in the application.
|
|
285
|
+
*/
|
|
286
|
+
interface FormalityProviderProps {
|
|
287
|
+
/** Child components */
|
|
288
|
+
children: ReactNode;
|
|
289
|
+
/**
|
|
290
|
+
* Input type definitions (REQUIRED)
|
|
291
|
+
*
|
|
292
|
+
* Defines how each input type (e.g., textField, switch, autocomplete)
|
|
293
|
+
* behaves across all forms.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```tsx
|
|
297
|
+
* inputs={{
|
|
298
|
+
* textField: { component: TextField, defaultValue: '' },
|
|
299
|
+
* switch: { component: Switch, defaultValue: false },
|
|
300
|
+
* }}
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
inputs: Record<string, InputConfig>;
|
|
304
|
+
/**
|
|
305
|
+
* Named formatters for value → display transformation
|
|
306
|
+
*
|
|
307
|
+
* Transform form values before displaying in inputs.
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```tsx
|
|
311
|
+
* formatters={{
|
|
312
|
+
* currency: (v) => `$${Number(v).toFixed(2)}`,
|
|
313
|
+
* uppercase: (v) => String(v).toUpperCase(),
|
|
314
|
+
* }}
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
formatters?: Record<string, (value: unknown) => unknown>;
|
|
318
|
+
/**
|
|
319
|
+
* Named parsers for input → value transformation
|
|
320
|
+
*
|
|
321
|
+
* Transform user input before storing in form state.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```tsx
|
|
325
|
+
* parsers={{
|
|
326
|
+
* number: (v) => Number(v) || 0,
|
|
327
|
+
* trim: (v) => String(v).trim(),
|
|
328
|
+
* }}
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
parsers?: Record<string, (value: unknown) => unknown>;
|
|
332
|
+
/**
|
|
333
|
+
* Named validators and validator factories
|
|
334
|
+
*
|
|
335
|
+
* Define reusable validation logic across forms.
|
|
336
|
+
*/
|
|
337
|
+
validators?: ValidatorsConfig;
|
|
338
|
+
/**
|
|
339
|
+
* Error message templates by type key
|
|
340
|
+
*
|
|
341
|
+
* Map validation error types to human-readable messages.
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```tsx
|
|
345
|
+
* errorMessages={{
|
|
346
|
+
* required: 'This field is required',
|
|
347
|
+
* minLength: 'Must be at least {min} characters',
|
|
348
|
+
* }}
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
errorMessages?: ErrorMessagesConfig;
|
|
352
|
+
/**
|
|
353
|
+
* Default template component for all inputs
|
|
354
|
+
*
|
|
355
|
+
* Wraps all inputs for consistent styling, labels, error display.
|
|
356
|
+
*/
|
|
357
|
+
defaultInputTemplate?: ComponentType<InputTemplateProps>;
|
|
358
|
+
/**
|
|
359
|
+
* Named template components for specific input types
|
|
360
|
+
*
|
|
361
|
+
* Override the default template for specific input types.
|
|
362
|
+
*/
|
|
363
|
+
inputTemplates?: Record<string, ComponentType<InputTemplateProps>>;
|
|
364
|
+
/**
|
|
365
|
+
* Default prop name for passSubscriptions (default: 'state')
|
|
366
|
+
*
|
|
367
|
+
* When a field has passSubscriptions: true, subscribed field states
|
|
368
|
+
* are passed using this prop name.
|
|
369
|
+
*/
|
|
370
|
+
defaultSubscriptionPropName?: string;
|
|
371
|
+
/**
|
|
372
|
+
* Static default props for all fields
|
|
373
|
+
*
|
|
374
|
+
* Props applied to every field unless overridden.
|
|
375
|
+
*/
|
|
376
|
+
defaultFieldProps?: Record<string, unknown>;
|
|
377
|
+
/**
|
|
378
|
+
* Dynamic default props evaluated per-field
|
|
379
|
+
*
|
|
380
|
+
* Expression or function that computes default props.
|
|
381
|
+
*/
|
|
382
|
+
selectDefaultFieldProps?: SelectValue;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* FormalityProvider - Global configuration provider for Formality
|
|
386
|
+
*
|
|
387
|
+
* Wraps your application to provide input types, transformers, validators,
|
|
388
|
+
* and global defaults to all Formality forms.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```tsx
|
|
392
|
+
* import { FormalityProvider } from '@formality-ui/react';
|
|
393
|
+
*
|
|
394
|
+
* const inputs = {
|
|
395
|
+
* textField: { component: TextInput, defaultValue: '' },
|
|
396
|
+
* switch: { component: Toggle, defaultValue: false },
|
|
397
|
+
* };
|
|
398
|
+
*
|
|
399
|
+
* function App() {
|
|
400
|
+
* return (
|
|
401
|
+
* <FormalityProvider
|
|
402
|
+
* inputs={inputs}
|
|
403
|
+
* formatters={{ currency: (v) => `$${v}` }}
|
|
404
|
+
* errorMessages={{ required: 'Required' }}
|
|
405
|
+
* >
|
|
406
|
+
* <MyApp />
|
|
407
|
+
* </FormalityProvider>
|
|
408
|
+
* );
|
|
409
|
+
* }
|
|
410
|
+
* ```
|
|
411
|
+
*/
|
|
412
|
+
declare function FormalityProvider({ children, inputs, formatters, parsers, validators, errorMessages, defaultInputTemplate, inputTemplates, defaultSubscriptionPropName, defaultFieldProps, selectDefaultFieldProps, }: FormalityProviderProps): JSX.Element;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Form component props
|
|
416
|
+
*/
|
|
417
|
+
interface FormProps<TFieldValues extends FieldValues = FieldValues> {
|
|
418
|
+
/** Form content - can be static children or render function */
|
|
419
|
+
children: ReactNode | ((api: FormRenderAPI<TFieldValues>) => ReactNode);
|
|
420
|
+
/** Field configurations */
|
|
421
|
+
config: FormFieldsConfig;
|
|
422
|
+
/** Form-level configuration (title, groups, input overrides) */
|
|
423
|
+
formConfig?: FormConfig;
|
|
424
|
+
/** Submit handler */
|
|
425
|
+
onSubmit?: (values: Partial<TFieldValues>) => void | Promise<void>;
|
|
426
|
+
/** Initial record data */
|
|
427
|
+
record?: Partial<TFieldValues>;
|
|
428
|
+
/** Enable auto-save on field changes */
|
|
429
|
+
autoSave?: boolean;
|
|
430
|
+
/** Debounce milliseconds for auto-save (default: 1000) */
|
|
431
|
+
debounce?: number;
|
|
432
|
+
/** Form-level validation */
|
|
433
|
+
validate?: (values: Partial<TFieldValues>) => Record<string, string> | Promise<Record<string, string>>;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* API passed to render function children
|
|
437
|
+
*/
|
|
438
|
+
interface FormRenderAPI<TFieldValues extends FieldValues = FieldValues> {
|
|
439
|
+
/** Fields in config but not rendered */
|
|
440
|
+
unusedFields: string[];
|
|
441
|
+
/** React Hook Form formState */
|
|
442
|
+
formState: UseFormReturn<TFieldValues>['formState'];
|
|
443
|
+
/** React Hook Form methods */
|
|
444
|
+
methods: UseFormReturn<TFieldValues>;
|
|
445
|
+
/** Resolved form title (static or evaluated) */
|
|
446
|
+
resolvedTitle?: string;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Form component - Core form wrapper with React Hook Form integration
|
|
450
|
+
*
|
|
451
|
+
* Provides:
|
|
452
|
+
* - FormContext for field registration and subscription management
|
|
453
|
+
* - React Hook Form integration with mode: 'onChange'
|
|
454
|
+
* - Auto-save with debounced submission
|
|
455
|
+
* - Config-driven default values
|
|
456
|
+
* - Unused fields tracking for config-driven rendering
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```tsx
|
|
460
|
+
* <Form
|
|
461
|
+
* config={{
|
|
462
|
+
* name: { type: 'textField', label: 'Name' },
|
|
463
|
+
* email: { type: 'textField', label: 'Email' },
|
|
464
|
+
* }}
|
|
465
|
+
* onSubmit={(values) => console.log(values)}
|
|
466
|
+
* >
|
|
467
|
+
* <Field name="name" />
|
|
468
|
+
* <Field name="email" />
|
|
469
|
+
* <button type="submit">Submit</button>
|
|
470
|
+
* </Form>
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
declare function Form<TFieldValues extends FieldValues = FieldValues>({ children, config, formConfig, onSubmit, record, autoSave, debounce: debounceMs, validate, }: FormProps<TFieldValues>): JSX.Element;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Field component props
|
|
477
|
+
*/
|
|
478
|
+
interface FieldProps {
|
|
479
|
+
/** Field name (must match a key in Form's config) */
|
|
480
|
+
name: string;
|
|
481
|
+
/** Override the input type from config */
|
|
482
|
+
type?: string;
|
|
483
|
+
/** Override disabled state */
|
|
484
|
+
disabled?: boolean;
|
|
485
|
+
/** Override hidden state (inverse of visible) */
|
|
486
|
+
hidden?: boolean;
|
|
487
|
+
/** Custom render function for advanced use cases */
|
|
488
|
+
children?: ReactNode | ((api: FieldRenderAPI) => ReactNode);
|
|
489
|
+
/** Whether to register this field in Form's field registry (default: true) */
|
|
490
|
+
shouldRegister?: boolean;
|
|
491
|
+
/** Additional props to pass to the input component */
|
|
492
|
+
[key: string]: unknown;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* API passed to render function children
|
|
496
|
+
*/
|
|
497
|
+
interface FieldRenderAPI {
|
|
498
|
+
/** React Hook Form field state */
|
|
499
|
+
fieldState: ControllerFieldState;
|
|
500
|
+
/** The rendered input component */
|
|
501
|
+
renderedField: ReactNode;
|
|
502
|
+
/** Final merged props passed to input */
|
|
503
|
+
fieldProps: Record<string, unknown>;
|
|
504
|
+
/** Map of fields watching this field */
|
|
505
|
+
watchers: Record<string, boolean>;
|
|
506
|
+
/** React Hook Form form state */
|
|
507
|
+
formState: UseFormStateReturn<FieldValues>;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Field component - Renders a form field with full Formality integration
|
|
511
|
+
*
|
|
512
|
+
* Provides:
|
|
513
|
+
* - React Hook Form Controller integration
|
|
514
|
+
* - Props resolution (8-layer merge)
|
|
515
|
+
* - Condition evaluation (disabled/visible/setValue)
|
|
516
|
+
* - Value transformation (parse/format)
|
|
517
|
+
* - Validation (field + type validators)
|
|
518
|
+
* - Subscription management
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* ```tsx
|
|
522
|
+
* // Basic usage
|
|
523
|
+
* <Field name="email" />
|
|
524
|
+
*
|
|
525
|
+
* // Override type
|
|
526
|
+
* <Field name="status" type="select" />
|
|
527
|
+
*
|
|
528
|
+
* // Custom render
|
|
529
|
+
* <Field name="name">
|
|
530
|
+
* {({ renderedField, fieldState }) => (
|
|
531
|
+
* <div className={fieldState.error ? 'has-error' : ''}>
|
|
532
|
+
* {renderedField}
|
|
533
|
+
* </div>
|
|
534
|
+
* )}
|
|
535
|
+
* </Field>
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
declare function Field({ name, type: typeProp, disabled: disabledProp, hidden: hiddenProp, children, shouldRegister, ...restProps }: FieldProps): JSX.Element | null;
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* FieldGroup component props
|
|
542
|
+
*/
|
|
543
|
+
interface FieldGroupProps {
|
|
544
|
+
/** Group name (must match a key in FormConfig.groups) */
|
|
545
|
+
name: string;
|
|
546
|
+
/** Fields to render within this group */
|
|
547
|
+
children: ReactNode;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* FieldGroup component - Groups fields with shared conditions
|
|
551
|
+
*
|
|
552
|
+
* Provides:
|
|
553
|
+
* - Group-level condition evaluation (disabled/visible)
|
|
554
|
+
* - State propagation to nested fields:
|
|
555
|
+
* - disabled: OR logic (child || parent)
|
|
556
|
+
* - visible: AND logic (child && parent)
|
|
557
|
+
* - Subscription accumulation from parent groups
|
|
558
|
+
*
|
|
559
|
+
* Groups can be nested, with state merging at each level.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```tsx
|
|
563
|
+
* // FormConfig
|
|
564
|
+
* formConfig={{
|
|
565
|
+
* groups: {
|
|
566
|
+
* clientSection: {
|
|
567
|
+
* conditions: [
|
|
568
|
+
* { when: 'hasClient', is: false, visible: false }
|
|
569
|
+
* ]
|
|
570
|
+
* }
|
|
571
|
+
* }
|
|
572
|
+
* }}
|
|
573
|
+
*
|
|
574
|
+
* // Usage
|
|
575
|
+
* <Form config={config} formConfig={formConfig}>
|
|
576
|
+
* <Field name="hasClient" />
|
|
577
|
+
* <FieldGroup name="clientSection">
|
|
578
|
+
* <Field name="clientName" />
|
|
579
|
+
* <Field name="clientEmail" />
|
|
580
|
+
* </FieldGroup>
|
|
581
|
+
* </Form>
|
|
582
|
+
* ```
|
|
583
|
+
*/
|
|
584
|
+
declare function FieldGroup({ name, children }: FieldGroupProps): JSX.Element;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* UnusedFields component props
|
|
588
|
+
*/
|
|
589
|
+
interface UnusedFieldsProps {
|
|
590
|
+
/** Custom render function for each unused field */
|
|
591
|
+
children?: (field: {
|
|
592
|
+
name: string;
|
|
593
|
+
component: ReactNode;
|
|
594
|
+
}) => ReactNode;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* UnusedFields component - Renders config-driven fields
|
|
598
|
+
*
|
|
599
|
+
* Renders fields that are declared in the Form's config but have not
|
|
600
|
+
* been explicitly rendered using <Field> components. This enables
|
|
601
|
+
* config-driven forms where not all fields need to be declared in JSX.
|
|
602
|
+
*
|
|
603
|
+
* Fields are tracked via the Form's field registry. When a <Field>
|
|
604
|
+
* component mounts, it registers itself. UnusedFields renders all
|
|
605
|
+
* fields that haven't been registered.
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```tsx
|
|
609
|
+
* // All fields from config rendered automatically
|
|
610
|
+
* <Form config={config}>
|
|
611
|
+
* <UnusedFields />
|
|
612
|
+
* </Form>
|
|
613
|
+
*
|
|
614
|
+
* // Mix explicit and automatic fields
|
|
615
|
+
* <Form config={config}>
|
|
616
|
+
* <Field name="name" /> {/* Explicit with custom props *\/}
|
|
617
|
+
* <Field name="email" /> {/* Explicit with custom props *\/}
|
|
618
|
+
* <UnusedFields /> {/* Renders remaining fields *\/}
|
|
619
|
+
* </Form>
|
|
620
|
+
*
|
|
621
|
+
* // Custom wrapper for unused fields
|
|
622
|
+
* <Form config={config}>
|
|
623
|
+
* <UnusedFields>
|
|
624
|
+
* {({ name, component }) => (
|
|
625
|
+
* <div key={name} className="auto-field">
|
|
626
|
+
* {component}
|
|
627
|
+
* </div>
|
|
628
|
+
* )}
|
|
629
|
+
* </UnusedFields>
|
|
630
|
+
* </Form>
|
|
631
|
+
* ```
|
|
632
|
+
*/
|
|
633
|
+
declare function UnusedFields({ children }: UnusedFieldsProps): JSX.Element;
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Creates a proxy object with lazy property access via Object.defineProperty
|
|
637
|
+
*
|
|
638
|
+
* CRITICAL: This enables React's dependency tracking to only register
|
|
639
|
+
* dependencies on actually accessed properties, not the entire object.
|
|
640
|
+
*
|
|
641
|
+
* Without proxy: accessing fieldState.value creates dependency on entire fieldState
|
|
642
|
+
* With proxy: accessing fieldState.value only creates dependency on value property
|
|
643
|
+
*
|
|
644
|
+
* Performance Impact:
|
|
645
|
+
* - Reduces unnecessary re-renders in condition-heavy forms
|
|
646
|
+
* - Expression evaluation only subscribes to properties actually used
|
|
647
|
+
* - Field states can change without triggering re-renders in unrelated fields
|
|
648
|
+
*
|
|
649
|
+
* Implementation Note:
|
|
650
|
+
* Uses Object.defineProperty (NOT Proxy) because:
|
|
651
|
+
* - Better performance for simple property access
|
|
652
|
+
* - Works with Object.keys() iteration when enumerable: true
|
|
653
|
+
* - Simpler debugging and stack traces
|
|
654
|
+
*
|
|
655
|
+
* @param source - The object to wrap with lazy getters
|
|
656
|
+
* @returns A new object with getter properties that lazily access source
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* ```typescript
|
|
660
|
+
* const source = { value: 5, isTouched: false };
|
|
661
|
+
* const proxy = makeProxyState(source);
|
|
662
|
+
*
|
|
663
|
+
* // Properties are accessed lazily
|
|
664
|
+
* console.log(proxy.value); // 5
|
|
665
|
+
*
|
|
666
|
+
* // Changes to source reflect in proxy (lazy access)
|
|
667
|
+
* source.value = 10;
|
|
668
|
+
* console.log(proxy.value); // 10
|
|
669
|
+
*
|
|
670
|
+
* // Object.keys still works
|
|
671
|
+
* console.log(Object.keys(proxy)); // ['value', 'isTouched']
|
|
672
|
+
* ```
|
|
673
|
+
*/
|
|
674
|
+
declare function makeProxyState<T extends object>(source: T): T;
|
|
675
|
+
/**
|
|
676
|
+
* Creates a nested proxy state, recursively wrapping objects
|
|
677
|
+
*
|
|
678
|
+
* Use this for deeply nested state objects where you want
|
|
679
|
+
* lazy access at multiple levels.
|
|
680
|
+
*
|
|
681
|
+
* @param source - The object to wrap with lazy getters (recursively)
|
|
682
|
+
* @returns A new object with recursive getter properties
|
|
683
|
+
*/
|
|
684
|
+
declare function makeDeepProxyState<T extends object>(source: T): T;
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Options for useFormState hook
|
|
688
|
+
*/
|
|
689
|
+
interface UseFormStateOptions {
|
|
690
|
+
/**
|
|
691
|
+
* Specific field name(s) to watch.
|
|
692
|
+
* REQUIRED for performance - you must specify which fields you need.
|
|
693
|
+
* Watching all fields defeats the purpose of isolation.
|
|
694
|
+
*/
|
|
695
|
+
name: string | string[];
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Custom useFormState that provides ISOLATED field subscriptions
|
|
699
|
+
*
|
|
700
|
+
* CRITICAL PERFORMANCE REQUIREMENT:
|
|
701
|
+
* You MUST specify which fields to watch via the `name` option.
|
|
702
|
+
* This hook is designed to prevent the "subscribe to everything" anti-pattern.
|
|
703
|
+
*
|
|
704
|
+
* This hook provides:
|
|
705
|
+
* - Proxy-wrapped field states to prevent unnecessary re-renders
|
|
706
|
+
* - Record property access via lazy getter
|
|
707
|
+
* - Integration with Formality's FormContext
|
|
708
|
+
*
|
|
709
|
+
* Performance Benefits:
|
|
710
|
+
* - Only re-renders when specified fields change
|
|
711
|
+
* - Expression evaluation only creates dependencies on used properties
|
|
712
|
+
* - Field states can update independently without cross-field re-renders
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```tsx
|
|
716
|
+
* function MyField() {
|
|
717
|
+
* // Watch specific fields only
|
|
718
|
+
* const formState = useFormState({ name: ['firstName', 'lastName'] });
|
|
719
|
+
*
|
|
720
|
+
* // Only re-renders when firstName or lastName change
|
|
721
|
+
* const value = formState.fields.firstName?.value;
|
|
722
|
+
*
|
|
723
|
+
* // Access original record for expressions
|
|
724
|
+
* const recordId = formState.record.id;
|
|
725
|
+
* }
|
|
726
|
+
* ```
|
|
727
|
+
*/
|
|
728
|
+
declare function useFormState<TFieldValues extends FieldValues = FieldValues>(options: UseFormStateOptions): IsolatedFormState;
|
|
729
|
+
|
|
730
|
+
interface UseConditionsOptions {
|
|
731
|
+
/** Conditions to evaluate */
|
|
732
|
+
conditions: ConditionDescriptor[];
|
|
733
|
+
/** Explicit field subscriptions */
|
|
734
|
+
subscribesTo?: string[];
|
|
735
|
+
/** Additional props for expression context */
|
|
736
|
+
props?: Record<string, unknown>;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Evaluates conditions against current field values
|
|
740
|
+
*
|
|
741
|
+
* This hook:
|
|
742
|
+
* 1. Infers which fields to watch from conditions
|
|
743
|
+
* 2. Subscribes to those fields via useWatch
|
|
744
|
+
* 3. Evaluates conditions whenever watched values change
|
|
745
|
+
*
|
|
746
|
+
* Implements the condition evaluation rules:
|
|
747
|
+
* - disabled: OR logic (any true = disabled)
|
|
748
|
+
* - visible: AND logic (any false = hidden)
|
|
749
|
+
* - setValue: last matching condition wins
|
|
750
|
+
*
|
|
751
|
+
* @param options - Conditions and subscription config
|
|
752
|
+
* @returns Evaluation result with disabled, visible, and setValue states
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* ```tsx
|
|
756
|
+
* const result = useConditions({
|
|
757
|
+
* conditions: [
|
|
758
|
+
* { when: 'signed', is: false, disabled: true },
|
|
759
|
+
* { when: 'archived', truthy: true, visible: false },
|
|
760
|
+
* ],
|
|
761
|
+
* });
|
|
762
|
+
* // result.disabled === true when signed is false
|
|
763
|
+
* // result.visible === false when archived is truthy
|
|
764
|
+
* ```
|
|
765
|
+
*/
|
|
766
|
+
declare function useConditions(options: UseConditionsOptions): ConditionResult;
|
|
767
|
+
|
|
768
|
+
interface UsePropsEvaluationOptions {
|
|
769
|
+
/** Dynamic props descriptor to evaluate */
|
|
770
|
+
selectProps?: SelectValue;
|
|
771
|
+
/** Explicit field subscriptions */
|
|
772
|
+
subscribesTo?: string[];
|
|
773
|
+
/** Current field name */
|
|
774
|
+
fieldName: string;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Evaluates selectProps against current field values
|
|
778
|
+
*
|
|
779
|
+
* This hook:
|
|
780
|
+
* 1. Infers which fields to watch from selectProps expressions
|
|
781
|
+
* 2. Subscribes to those fields via useWatch
|
|
782
|
+
* 3. Evaluates selectProps whenever watched values change
|
|
783
|
+
*
|
|
784
|
+
* Handles both expression-based selectProps and function-based selectProps.
|
|
785
|
+
*
|
|
786
|
+
* @param options - selectProps and subscription config
|
|
787
|
+
* @returns Evaluated props object
|
|
788
|
+
*
|
|
789
|
+
* @example
|
|
790
|
+
* ```tsx
|
|
791
|
+
* const props = usePropsEvaluation({
|
|
792
|
+
* selectProps: {
|
|
793
|
+
* options: "client.id ? clientOptions : allOptions",
|
|
794
|
+
* disabled: "!signed",
|
|
795
|
+
* },
|
|
796
|
+
* fieldName: 'contact',
|
|
797
|
+
* });
|
|
798
|
+
* // props.options and props.disabled are evaluated against current state
|
|
799
|
+
* ```
|
|
800
|
+
*/
|
|
801
|
+
declare function usePropsEvaluation(options: UsePropsEvaluationOptions): Record<string, unknown>;
|
|
802
|
+
|
|
803
|
+
interface UseInferredInputsOptions {
|
|
804
|
+
/** Dynamic props descriptor to analyze for field references */
|
|
805
|
+
selectProps?: SelectValue;
|
|
806
|
+
/** Conditions to analyze for field references */
|
|
807
|
+
conditions?: ConditionDescriptor[];
|
|
808
|
+
/** Explicit field subscriptions */
|
|
809
|
+
subscribesTo?: string[];
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Infers field dependencies from selectProps, conditions, and explicit subscriptions
|
|
813
|
+
*
|
|
814
|
+
* This hook analyzes:
|
|
815
|
+
* - selectProps expressions for field references
|
|
816
|
+
* - condition 'when' fields and 'selectWhen' expressions
|
|
817
|
+
* - explicit subscribesTo declarations
|
|
818
|
+
*
|
|
819
|
+
* Used to automatically determine which fields a Field or FieldGroup
|
|
820
|
+
* should subscribe to for reactive updates.
|
|
821
|
+
*
|
|
822
|
+
* @param options - Sources to analyze for field references
|
|
823
|
+
* @returns Array of unique field names to subscribe to
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```tsx
|
|
827
|
+
* const subscriptions = useInferredInputs({
|
|
828
|
+
* selectProps: "client.id",
|
|
829
|
+
* conditions: [{ when: 'signed', disabled: true }],
|
|
830
|
+
* subscribesTo: ['contact'],
|
|
831
|
+
* });
|
|
832
|
+
* // → ['client', 'signed', 'contact']
|
|
833
|
+
* ```
|
|
834
|
+
*/
|
|
835
|
+
declare function useInferredInputs(options: UseInferredInputsOptions): string[];
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Manages field subscriptions
|
|
839
|
+
*
|
|
840
|
+
* This hook registers the current field as a subscriber to the target fields,
|
|
841
|
+
* and cleans up subscriptions when the component unmounts or subscriptions change.
|
|
842
|
+
*
|
|
843
|
+
* Subscriptions are stored in the Form's inverted index (target → subscribers),
|
|
844
|
+
* which allows target fields to know who is watching them (for optimization).
|
|
845
|
+
*
|
|
846
|
+
* @param fieldName - The subscribing field's name
|
|
847
|
+
* @param subscriptions - Array of field names to subscribe to
|
|
848
|
+
*
|
|
849
|
+
* @example
|
|
850
|
+
* ```tsx
|
|
851
|
+
* // Contact field subscribes to client field
|
|
852
|
+
* useSubscriptions('contact', ['client']);
|
|
853
|
+
*
|
|
854
|
+
* // When client changes, its watchers can be notified
|
|
855
|
+
* // This enables features like passSubscriptions
|
|
856
|
+
* ```
|
|
857
|
+
*/
|
|
858
|
+
declare function useSubscriptions(fieldName: string, subscriptions: string[]): void;
|
|
859
|
+
|
|
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 };
|