@rilaykit/forms 4.0.0 → 5.1.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.mts CHANGED
@@ -1,9 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ril, ValidationConfig, ConditionalConfig, FormFieldConfig, FormFieldRow, FormConfiguration, RendererChildrenFunction, FormBodyRendererProps, ValidationError, ValidationResult, FormRowRendererProps, FormSubmitButtonRendererProps } from '@rilaykit/core';
2
+ import { ril, FieldValidationConfig, FormFieldConfig, FormFieldRow, FormValidationConfig, FormValidator, FormConfiguration, ComponentRendererBaseProps, FormBodyRendererProps, ValidationError, ValidationResult, FormRowRendererProps, FormSubmitButtonRendererProps } from '@rilaykit/core';
3
3
  export * from '@rilaykit/core';
4
- export { createZodValidator, ril } from '@rilaykit/core';
5
- import * as React$1 from 'react';
6
- import React__default from 'react';
4
+ import React$1 from 'react';
7
5
 
8
6
  /**
9
7
  * Configuration for a form field with type safety
@@ -16,7 +14,11 @@ import React__default from 'react';
16
14
  * const fieldConfig: FieldConfig<MyComponents, 'text'> = {
17
15
  * type: 'text',
18
16
  * props: { placeholder: 'Enter your name' },
19
- * validation: { required: true }
17
+ * validation: {
18
+ * validators: [required(), minLength(2)],
19
+ * validateOnChange: true,
20
+ * validateOnBlur: true
21
+ * }
20
22
  * };
21
23
  * ```
22
24
  */
@@ -27,28 +29,9 @@ type FieldConfig<C extends Record<string, any>, T extends keyof C> = {
27
29
  type: T;
28
30
  /** Component-specific properties */
29
31
  props?: Partial<C[T]>;
30
- /** Validation rules for the field */
31
- validation?: ValidationConfig;
32
- /** Conditional display logic */
33
- conditional?: ConditionalConfig;
32
+ /** Validation configuration for this field */
33
+ validation?: FieldValidationConfig;
34
34
  };
35
- /**
36
- * Options for configuring row layout and appearance
37
- *
38
- * @example
39
- * ```typescript
40
- * const rowOptions: RowOptions = {
41
- * spacing: 'loose',
42
- * alignment: 'center'
43
- * };
44
- * ```
45
- */
46
- interface RowOptions {
47
- /** Spacing between fields in the row */
48
- spacing?: 'tight' | 'normal' | 'loose';
49
- /** Vertical alignment of fields in the row */
50
- alignment?: 'start' | 'center' | 'end' | 'stretch';
51
- }
52
35
  /**
53
36
  * Form builder class for creating type-safe form configurations
54
37
  *
@@ -73,7 +56,6 @@ interface RowOptions {
73
56
  * @remarks
74
57
  * - Supports up to 3 fields per row for optimal layout
75
58
  * - Automatically generates unique IDs for fields and rows
76
- * - Provides comprehensive validation before building
77
59
  * - Maintains type safety throughout the building process
78
60
  */
79
61
  declare class form<C extends Record<string, any> = Record<string, never>> {
@@ -85,6 +67,8 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
85
67
  private formId;
86
68
  /** Generator for creating unique IDs */
87
69
  private idGenerator;
70
+ /** Form-level validation configuration */
71
+ private formValidation?;
88
72
  /**
89
73
  * Creates a new form builder instance
90
74
  *
@@ -112,11 +96,15 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
112
96
  */
113
97
  static create<Cm extends Record<string, any> = Record<string, never>>(config: ril<Cm>, formId?: string): form<Cm>;
114
98
  /**
115
- * Converts a FieldConfig to a FormFieldConfig with proper validation
99
+ * Converts a FieldConfig to a FormFieldConfig
116
100
  *
117
101
  * This internal method handles the transformation from the builder's field
118
102
  * configuration format to the final form field configuration, including
119
- * component lookup, prop merging, and ID generation.
103
+ * component lookup, prop merging, ID generation, and validation setup.
104
+ *
105
+ * The validation system combines component-level validation (defined in the component config)
106
+ * with field-level validation (defined in the field config). Component validators are
107
+ * applied first, followed by field validators.
120
108
  *
121
109
  * @template T - The component type
122
110
  * @param fieldConfig - The field configuration to convert
@@ -129,7 +117,7 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
129
117
  /**
130
118
  * Creates a form row with the specified fields and options
131
119
  *
132
- * This internal method handles row creation with validation of field limits,
120
+ * This internal method handles row creation,
133
121
  * proper spacing, and alignment configuration.
134
122
  *
135
123
  * @template T - The component type
@@ -176,7 +164,7 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
176
164
  * ```
177
165
  */
178
166
  add<T extends keyof C & string>(...fields: FieldConfig<C, T>[]): this;
179
- add<T extends keyof C & string>(fields: FieldConfig<C, T>[], rowOptions?: RowOptions): this;
167
+ add<T extends keyof C & string>(fields: FieldConfig<C, T>[]): this;
180
168
  /**
181
169
  * Adds multiple fields on separate rows
182
170
  *
@@ -196,10 +184,10 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
196
184
  * { type: 'text', props: { label: 'Field 1' } },
197
185
  * { type: 'text', props: { label: 'Field 2' } },
198
186
  * { type: 'text', props: { label: 'Field 3' } }
199
- * ], { spacing: 'loose' });
187
+ * ]);
200
188
  * ```
201
189
  */
202
- addSeparateRows<T extends keyof C & string>(fieldConfigs: FieldConfig<C, T>[], rowOptions?: RowOptions): this;
190
+ addSeparateRows<T extends keyof C & string>(fieldConfigs: FieldConfig<C, T>[]): this;
203
191
  /**
204
192
  * Sets the form identifier
205
193
  *
@@ -215,8 +203,7 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
215
203
  /**
216
204
  * Updates an existing field's configuration
217
205
  *
218
- * This method allows you to modify field properties, validation rules,
219
- * or conditional logic after the field has been added to the form.
206
+ * This method allows you to modify field properties after the field has been added to the form.
220
207
  *
221
208
  * @param fieldId - The unique identifier of the field to update
222
209
  * @param updates - Partial field configuration with updates to apply
@@ -227,7 +214,6 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
227
214
  * ```typescript
228
215
  * builder.updateField('email-field', {
229
216
  * props: { placeholder: 'Enter your email address' },
230
- * validation: { required: true, email: true }
231
217
  * });
232
218
  * ```
233
219
  */
@@ -319,6 +305,70 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
319
305
  * ```
320
306
  */
321
307
  clear(): this;
308
+ /**
309
+ * Configures validation for the entire form
310
+ *
311
+ * This method sets up form-level validation that will be applied when the
312
+ * form is submitted or when validation is explicitly triggered. Form validators
313
+ * receive all form data and can perform cross-field validation.
314
+ *
315
+ * @param validationConfig - Form validation configuration
316
+ * @returns The form builder instance for method chaining
317
+ *
318
+ * @example
319
+ * ```typescript
320
+ * builder.setValidation({
321
+ * validators: [
322
+ * (formData, context) => {
323
+ * if (!formData.email && !formData.phone) {
324
+ * return createErrorResult('Either email or phone is required');
325
+ * }
326
+ * return createSuccessResult();
327
+ * }
328
+ * ],
329
+ * validateOnSubmit: true
330
+ * });
331
+ * ```
332
+ */
333
+ setValidation(validationConfig: FormValidationConfig): this;
334
+ /**
335
+ * Adds validators to the form-level validation
336
+ *
337
+ * This method allows adding validators to an existing validation configuration
338
+ * without replacing the entire configuration.
339
+ *
340
+ * @param validators - Array of form validators to add
341
+ * @returns The form builder instance for method chaining
342
+ *
343
+ * @example
344
+ * ```typescript
345
+ * builder.addValidators([
346
+ * customFormValidator,
347
+ * anotherFormValidator
348
+ * ]);
349
+ * ```
350
+ */
351
+ addValidators(validators: FormValidator[]): this;
352
+ /**
353
+ * Adds validation to a specific field by ID
354
+ *
355
+ * This method allows adding validation to a field after it has been created,
356
+ * useful for dynamic validation requirements.
357
+ *
358
+ * @param fieldId - The ID of the field to add validation to
359
+ * @param validationConfig - Field validation configuration
360
+ * @returns The form builder instance for method chaining
361
+ * @throws Error if the field with the specified ID is not found
362
+ *
363
+ * @example
364
+ * ```typescript
365
+ * builder.addFieldValidation('email', {
366
+ * validators: [required(), email()],
367
+ * validateOnBlur: true
368
+ * });
369
+ * ```
370
+ */
371
+ addFieldValidation(fieldId: string, validationConfig: FieldValidationConfig): this;
322
372
  /**
323
373
  * Creates a deep copy of the current form builder
324
374
  *
@@ -338,47 +388,24 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
338
388
  */
339
389
  clone(newFormId?: string): form<C>;
340
390
  /**
341
- * Validates the current form configuration
342
- *
343
- * This method performs comprehensive validation of the form structure,
344
- * checking for common issues like duplicate IDs, missing components,
345
- * and invalid row configurations.
391
+ * Checks the current form configuration for basic structural issues.
346
392
  *
347
- * @returns Array of validation error messages (empty if valid)
348
- *
349
- * @example
350
- * ```typescript
351
- * const errors = builder.validate();
352
- * if (errors.length > 0) {
353
- * console.error('Form validation failed:', errors);
354
- * }
355
- * ```
356
- *
357
- * @remarks
358
- * Validation checks include:
359
- * - Duplicate field IDs across the form
360
- * - Missing component definitions for referenced types
361
- * - Row constraints (max 3 fields, no empty rows)
393
+ * @returns Array of error messages (empty if valid)
362
394
  */
363
395
  validate(): string[];
364
396
  /**
365
397
  * Builds the final form configuration
366
398
  *
367
- * This method performs final validation and creates the complete form
399
+ * This method creates the complete form
368
400
  * configuration object ready for rendering. It includes all field
369
- * configurations, render settings, and metadata.
401
+ * configurations, render settings, validation configuration, and metadata.
370
402
  *
371
403
  * @returns Complete form configuration ready for use
372
- * @throws Error if validation fails
373
404
  *
374
405
  * @example
375
406
  * ```typescript
376
- * try {
377
- * const formConfig = builder.build();
378
- * // Use formConfig with your form renderer
379
- * } catch (error) {
380
- * console.error('Failed to build form:', error.message);
381
- * }
407
+ * const formConfig = builder.build();
408
+ * // Use formConfig with your form renderer
382
409
  * ```
383
410
  *
384
411
  * @remarks
@@ -388,6 +415,7 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
388
415
  * - Flattened array of all fields for easy access
389
416
  * - Component configuration reference
390
417
  * - Render configuration for customization
418
+ * - Form-level validation configuration
391
419
  */
392
420
  build(): FormConfiguration;
393
421
  /**
@@ -516,12 +544,7 @@ interface FormProps {
516
544
  }
517
545
  declare function Form({ formConfig, defaultValues, onSubmit, onFieldChange, children }: FormProps): react_jsx_runtime.JSX.Element;
518
546
 
519
- interface FormBodyProps {
520
- className?: string;
521
- children?: React.ReactNode | RendererChildrenFunction<FormBodyRendererProps>;
522
- renderAs?: 'default' | 'children' | boolean;
523
- }
524
- declare function FormBody({ className, children, renderAs }: FormBodyProps): string | number | boolean | React$1.ReactElement<any, string | React$1.JSXElementConstructor<any>> | Iterable<React$1.ReactNode> | null | undefined;
547
+ declare function FormBody({ className, ...props }: ComponentRendererBaseProps<FormBodyRendererProps>): react_jsx_runtime.JSX.Element;
525
548
 
526
549
  interface FormFieldProps {
527
550
  fieldId: string;
@@ -534,27 +557,27 @@ declare function FormField({ fieldId, disabled, customProps, className, }: FormF
534
557
  interface FormState {
535
558
  values: Record<string, any>;
536
559
  errors: Record<string, ValidationError[]>;
537
- touched: Set<string>;
538
- isValidating: Set<string>;
560
+ validationState: Record<string, 'idle' | 'validating' | 'valid' | 'invalid'>;
561
+ touched: Record<string, boolean>;
539
562
  isDirty: boolean;
540
- isValid: boolean;
541
563
  isSubmitting: boolean;
564
+ isValid: boolean;
542
565
  }
543
566
  interface FormContextValue {
544
567
  formState: FormState;
545
568
  formConfig: FormConfiguration;
546
569
  setValue: (fieldId: string, value: any) => void;
547
- setError: (fieldId: string, errors: ValidationError[]) => void;
548
- clearError: (fieldId: string) => void;
549
- markFieldTouched: (fieldId: string) => void;
550
- setFieldValidating: (fieldId: string, isValidating: boolean) => void;
570
+ setFieldTouched: (fieldId: string, touched?: boolean) => void;
551
571
  validateField: (fieldId: string, value?: any) => Promise<ValidationResult>;
552
- validateAllFields: () => Promise<boolean>;
572
+ validateForm: () => Promise<ValidationResult>;
573
+ isFormValid: () => boolean;
553
574
  reset: (values?: Record<string, any>) => void;
554
- submit: (event?: React__default.FormEvent) => Promise<boolean>;
575
+ submit: (event?: React$1.FormEvent) => Promise<boolean>;
576
+ setError: (fieldId: string, errors: ValidationError[]) => void;
577
+ clearError: (fieldId: string) => void;
555
578
  }
556
579
  interface FormProviderProps {
557
- children: React__default.ReactNode;
580
+ children: React$1.ReactNode;
558
581
  formConfig: FormConfiguration;
559
582
  defaultValues?: Record<string, any>;
560
583
  onSubmit?: (data: Record<string, any>) => void | Promise<void>;
@@ -564,19 +587,11 @@ interface FormProviderProps {
564
587
  declare function FormProvider({ children, formConfig, defaultValues, onSubmit, onFieldChange, className, }: FormProviderProps): react_jsx_runtime.JSX.Element;
565
588
  declare function useFormContext(): FormContextValue;
566
589
 
567
- interface FormRowProps {
590
+ interface FormRowProps extends ComponentRendererBaseProps<FormRowRendererProps> {
568
591
  row: FormFieldRow;
569
- className?: string;
570
- children?: React.ReactNode | RendererChildrenFunction<FormRowRendererProps>;
571
- renderAs?: 'default' | 'children' | boolean;
572
592
  }
573
- declare function FormRow({ row, className, children, renderAs }: FormRowProps): string | number | boolean | React$1.ReactElement<any, string | React$1.JSXElementConstructor<any>> | Iterable<React$1.ReactNode> | null | undefined;
593
+ declare function FormRow({ row, className, ...props }: FormRowProps): react_jsx_runtime.JSX.Element;
574
594
 
575
- interface FormSubmitButtonProps {
576
- className?: string;
577
- children?: React__default.ReactNode | RendererChildrenFunction<FormSubmitButtonRendererProps>;
578
- renderAs?: 'default' | 'children' | boolean;
579
- }
580
- declare function FormSubmitButton({ className, children, renderAs }: FormSubmitButtonProps): string | number | boolean | React__default.ReactElement<any, string | React__default.JSXElementConstructor<any>> | Iterable<React__default.ReactNode> | null | undefined;
595
+ declare function FormSubmitButton({ className, ...props }: ComponentRendererBaseProps<FormSubmitButtonRendererProps>): react_jsx_runtime.JSX.Element;
581
596
 
582
- export { type FieldConfig, Form, FormBody, type FormBodyProps, form as FormBuilder, type FormContextValue, FormField, type FormFieldProps, type FormProps, FormProvider, type FormProviderProps, FormRow, type FormRowProps, type FormState, FormSubmitButton, type FormSubmitButtonProps, createForm, form, useFormContext };
597
+ export { type FieldConfig, Form, FormBody, form as FormBuilder, FormField, FormProvider, FormRow, FormSubmitButton, createForm, form, useFormContext };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ril, ValidationConfig, ConditionalConfig, FormFieldConfig, FormFieldRow, FormConfiguration, RendererChildrenFunction, FormBodyRendererProps, ValidationError, ValidationResult, FormRowRendererProps, FormSubmitButtonRendererProps } from '@rilaykit/core';
2
+ import { ril, FieldValidationConfig, FormFieldConfig, FormFieldRow, FormValidationConfig, FormValidator, FormConfiguration, ComponentRendererBaseProps, FormBodyRendererProps, ValidationError, ValidationResult, FormRowRendererProps, FormSubmitButtonRendererProps } from '@rilaykit/core';
3
3
  export * from '@rilaykit/core';
4
- export { createZodValidator, ril } from '@rilaykit/core';
5
- import * as React$1 from 'react';
6
- import React__default from 'react';
4
+ import React$1 from 'react';
7
5
 
8
6
  /**
9
7
  * Configuration for a form field with type safety
@@ -16,7 +14,11 @@ import React__default from 'react';
16
14
  * const fieldConfig: FieldConfig<MyComponents, 'text'> = {
17
15
  * type: 'text',
18
16
  * props: { placeholder: 'Enter your name' },
19
- * validation: { required: true }
17
+ * validation: {
18
+ * validators: [required(), minLength(2)],
19
+ * validateOnChange: true,
20
+ * validateOnBlur: true
21
+ * }
20
22
  * };
21
23
  * ```
22
24
  */
@@ -27,28 +29,9 @@ type FieldConfig<C extends Record<string, any>, T extends keyof C> = {
27
29
  type: T;
28
30
  /** Component-specific properties */
29
31
  props?: Partial<C[T]>;
30
- /** Validation rules for the field */
31
- validation?: ValidationConfig;
32
- /** Conditional display logic */
33
- conditional?: ConditionalConfig;
32
+ /** Validation configuration for this field */
33
+ validation?: FieldValidationConfig;
34
34
  };
35
- /**
36
- * Options for configuring row layout and appearance
37
- *
38
- * @example
39
- * ```typescript
40
- * const rowOptions: RowOptions = {
41
- * spacing: 'loose',
42
- * alignment: 'center'
43
- * };
44
- * ```
45
- */
46
- interface RowOptions {
47
- /** Spacing between fields in the row */
48
- spacing?: 'tight' | 'normal' | 'loose';
49
- /** Vertical alignment of fields in the row */
50
- alignment?: 'start' | 'center' | 'end' | 'stretch';
51
- }
52
35
  /**
53
36
  * Form builder class for creating type-safe form configurations
54
37
  *
@@ -73,7 +56,6 @@ interface RowOptions {
73
56
  * @remarks
74
57
  * - Supports up to 3 fields per row for optimal layout
75
58
  * - Automatically generates unique IDs for fields and rows
76
- * - Provides comprehensive validation before building
77
59
  * - Maintains type safety throughout the building process
78
60
  */
79
61
  declare class form<C extends Record<string, any> = Record<string, never>> {
@@ -85,6 +67,8 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
85
67
  private formId;
86
68
  /** Generator for creating unique IDs */
87
69
  private idGenerator;
70
+ /** Form-level validation configuration */
71
+ private formValidation?;
88
72
  /**
89
73
  * Creates a new form builder instance
90
74
  *
@@ -112,11 +96,15 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
112
96
  */
113
97
  static create<Cm extends Record<string, any> = Record<string, never>>(config: ril<Cm>, formId?: string): form<Cm>;
114
98
  /**
115
- * Converts a FieldConfig to a FormFieldConfig with proper validation
99
+ * Converts a FieldConfig to a FormFieldConfig
116
100
  *
117
101
  * This internal method handles the transformation from the builder's field
118
102
  * configuration format to the final form field configuration, including
119
- * component lookup, prop merging, and ID generation.
103
+ * component lookup, prop merging, ID generation, and validation setup.
104
+ *
105
+ * The validation system combines component-level validation (defined in the component config)
106
+ * with field-level validation (defined in the field config). Component validators are
107
+ * applied first, followed by field validators.
120
108
  *
121
109
  * @template T - The component type
122
110
  * @param fieldConfig - The field configuration to convert
@@ -129,7 +117,7 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
129
117
  /**
130
118
  * Creates a form row with the specified fields and options
131
119
  *
132
- * This internal method handles row creation with validation of field limits,
120
+ * This internal method handles row creation,
133
121
  * proper spacing, and alignment configuration.
134
122
  *
135
123
  * @template T - The component type
@@ -176,7 +164,7 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
176
164
  * ```
177
165
  */
178
166
  add<T extends keyof C & string>(...fields: FieldConfig<C, T>[]): this;
179
- add<T extends keyof C & string>(fields: FieldConfig<C, T>[], rowOptions?: RowOptions): this;
167
+ add<T extends keyof C & string>(fields: FieldConfig<C, T>[]): this;
180
168
  /**
181
169
  * Adds multiple fields on separate rows
182
170
  *
@@ -196,10 +184,10 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
196
184
  * { type: 'text', props: { label: 'Field 1' } },
197
185
  * { type: 'text', props: { label: 'Field 2' } },
198
186
  * { type: 'text', props: { label: 'Field 3' } }
199
- * ], { spacing: 'loose' });
187
+ * ]);
200
188
  * ```
201
189
  */
202
- addSeparateRows<T extends keyof C & string>(fieldConfigs: FieldConfig<C, T>[], rowOptions?: RowOptions): this;
190
+ addSeparateRows<T extends keyof C & string>(fieldConfigs: FieldConfig<C, T>[]): this;
203
191
  /**
204
192
  * Sets the form identifier
205
193
  *
@@ -215,8 +203,7 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
215
203
  /**
216
204
  * Updates an existing field's configuration
217
205
  *
218
- * This method allows you to modify field properties, validation rules,
219
- * or conditional logic after the field has been added to the form.
206
+ * This method allows you to modify field properties after the field has been added to the form.
220
207
  *
221
208
  * @param fieldId - The unique identifier of the field to update
222
209
  * @param updates - Partial field configuration with updates to apply
@@ -227,7 +214,6 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
227
214
  * ```typescript
228
215
  * builder.updateField('email-field', {
229
216
  * props: { placeholder: 'Enter your email address' },
230
- * validation: { required: true, email: true }
231
217
  * });
232
218
  * ```
233
219
  */
@@ -319,6 +305,70 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
319
305
  * ```
320
306
  */
321
307
  clear(): this;
308
+ /**
309
+ * Configures validation for the entire form
310
+ *
311
+ * This method sets up form-level validation that will be applied when the
312
+ * form is submitted or when validation is explicitly triggered. Form validators
313
+ * receive all form data and can perform cross-field validation.
314
+ *
315
+ * @param validationConfig - Form validation configuration
316
+ * @returns The form builder instance for method chaining
317
+ *
318
+ * @example
319
+ * ```typescript
320
+ * builder.setValidation({
321
+ * validators: [
322
+ * (formData, context) => {
323
+ * if (!formData.email && !formData.phone) {
324
+ * return createErrorResult('Either email or phone is required');
325
+ * }
326
+ * return createSuccessResult();
327
+ * }
328
+ * ],
329
+ * validateOnSubmit: true
330
+ * });
331
+ * ```
332
+ */
333
+ setValidation(validationConfig: FormValidationConfig): this;
334
+ /**
335
+ * Adds validators to the form-level validation
336
+ *
337
+ * This method allows adding validators to an existing validation configuration
338
+ * without replacing the entire configuration.
339
+ *
340
+ * @param validators - Array of form validators to add
341
+ * @returns The form builder instance for method chaining
342
+ *
343
+ * @example
344
+ * ```typescript
345
+ * builder.addValidators([
346
+ * customFormValidator,
347
+ * anotherFormValidator
348
+ * ]);
349
+ * ```
350
+ */
351
+ addValidators(validators: FormValidator[]): this;
352
+ /**
353
+ * Adds validation to a specific field by ID
354
+ *
355
+ * This method allows adding validation to a field after it has been created,
356
+ * useful for dynamic validation requirements.
357
+ *
358
+ * @param fieldId - The ID of the field to add validation to
359
+ * @param validationConfig - Field validation configuration
360
+ * @returns The form builder instance for method chaining
361
+ * @throws Error if the field with the specified ID is not found
362
+ *
363
+ * @example
364
+ * ```typescript
365
+ * builder.addFieldValidation('email', {
366
+ * validators: [required(), email()],
367
+ * validateOnBlur: true
368
+ * });
369
+ * ```
370
+ */
371
+ addFieldValidation(fieldId: string, validationConfig: FieldValidationConfig): this;
322
372
  /**
323
373
  * Creates a deep copy of the current form builder
324
374
  *
@@ -338,47 +388,24 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
338
388
  */
339
389
  clone(newFormId?: string): form<C>;
340
390
  /**
341
- * Validates the current form configuration
342
- *
343
- * This method performs comprehensive validation of the form structure,
344
- * checking for common issues like duplicate IDs, missing components,
345
- * and invalid row configurations.
391
+ * Checks the current form configuration for basic structural issues.
346
392
  *
347
- * @returns Array of validation error messages (empty if valid)
348
- *
349
- * @example
350
- * ```typescript
351
- * const errors = builder.validate();
352
- * if (errors.length > 0) {
353
- * console.error('Form validation failed:', errors);
354
- * }
355
- * ```
356
- *
357
- * @remarks
358
- * Validation checks include:
359
- * - Duplicate field IDs across the form
360
- * - Missing component definitions for referenced types
361
- * - Row constraints (max 3 fields, no empty rows)
393
+ * @returns Array of error messages (empty if valid)
362
394
  */
363
395
  validate(): string[];
364
396
  /**
365
397
  * Builds the final form configuration
366
398
  *
367
- * This method performs final validation and creates the complete form
399
+ * This method creates the complete form
368
400
  * configuration object ready for rendering. It includes all field
369
- * configurations, render settings, and metadata.
401
+ * configurations, render settings, validation configuration, and metadata.
370
402
  *
371
403
  * @returns Complete form configuration ready for use
372
- * @throws Error if validation fails
373
404
  *
374
405
  * @example
375
406
  * ```typescript
376
- * try {
377
- * const formConfig = builder.build();
378
- * // Use formConfig with your form renderer
379
- * } catch (error) {
380
- * console.error('Failed to build form:', error.message);
381
- * }
407
+ * const formConfig = builder.build();
408
+ * // Use formConfig with your form renderer
382
409
  * ```
383
410
  *
384
411
  * @remarks
@@ -388,6 +415,7 @@ declare class form<C extends Record<string, any> = Record<string, never>> {
388
415
  * - Flattened array of all fields for easy access
389
416
  * - Component configuration reference
390
417
  * - Render configuration for customization
418
+ * - Form-level validation configuration
391
419
  */
392
420
  build(): FormConfiguration;
393
421
  /**
@@ -516,12 +544,7 @@ interface FormProps {
516
544
  }
517
545
  declare function Form({ formConfig, defaultValues, onSubmit, onFieldChange, children }: FormProps): react_jsx_runtime.JSX.Element;
518
546
 
519
- interface FormBodyProps {
520
- className?: string;
521
- children?: React.ReactNode | RendererChildrenFunction<FormBodyRendererProps>;
522
- renderAs?: 'default' | 'children' | boolean;
523
- }
524
- declare function FormBody({ className, children, renderAs }: FormBodyProps): string | number | boolean | React$1.ReactElement<any, string | React$1.JSXElementConstructor<any>> | Iterable<React$1.ReactNode> | null | undefined;
547
+ declare function FormBody({ className, ...props }: ComponentRendererBaseProps<FormBodyRendererProps>): react_jsx_runtime.JSX.Element;
525
548
 
526
549
  interface FormFieldProps {
527
550
  fieldId: string;
@@ -534,27 +557,27 @@ declare function FormField({ fieldId, disabled, customProps, className, }: FormF
534
557
  interface FormState {
535
558
  values: Record<string, any>;
536
559
  errors: Record<string, ValidationError[]>;
537
- touched: Set<string>;
538
- isValidating: Set<string>;
560
+ validationState: Record<string, 'idle' | 'validating' | 'valid' | 'invalid'>;
561
+ touched: Record<string, boolean>;
539
562
  isDirty: boolean;
540
- isValid: boolean;
541
563
  isSubmitting: boolean;
564
+ isValid: boolean;
542
565
  }
543
566
  interface FormContextValue {
544
567
  formState: FormState;
545
568
  formConfig: FormConfiguration;
546
569
  setValue: (fieldId: string, value: any) => void;
547
- setError: (fieldId: string, errors: ValidationError[]) => void;
548
- clearError: (fieldId: string) => void;
549
- markFieldTouched: (fieldId: string) => void;
550
- setFieldValidating: (fieldId: string, isValidating: boolean) => void;
570
+ setFieldTouched: (fieldId: string, touched?: boolean) => void;
551
571
  validateField: (fieldId: string, value?: any) => Promise<ValidationResult>;
552
- validateAllFields: () => Promise<boolean>;
572
+ validateForm: () => Promise<ValidationResult>;
573
+ isFormValid: () => boolean;
553
574
  reset: (values?: Record<string, any>) => void;
554
- submit: (event?: React__default.FormEvent) => Promise<boolean>;
575
+ submit: (event?: React$1.FormEvent) => Promise<boolean>;
576
+ setError: (fieldId: string, errors: ValidationError[]) => void;
577
+ clearError: (fieldId: string) => void;
555
578
  }
556
579
  interface FormProviderProps {
557
- children: React__default.ReactNode;
580
+ children: React$1.ReactNode;
558
581
  formConfig: FormConfiguration;
559
582
  defaultValues?: Record<string, any>;
560
583
  onSubmit?: (data: Record<string, any>) => void | Promise<void>;
@@ -564,19 +587,11 @@ interface FormProviderProps {
564
587
  declare function FormProvider({ children, formConfig, defaultValues, onSubmit, onFieldChange, className, }: FormProviderProps): react_jsx_runtime.JSX.Element;
565
588
  declare function useFormContext(): FormContextValue;
566
589
 
567
- interface FormRowProps {
590
+ interface FormRowProps extends ComponentRendererBaseProps<FormRowRendererProps> {
568
591
  row: FormFieldRow;
569
- className?: string;
570
- children?: React.ReactNode | RendererChildrenFunction<FormRowRendererProps>;
571
- renderAs?: 'default' | 'children' | boolean;
572
592
  }
573
- declare function FormRow({ row, className, children, renderAs }: FormRowProps): string | number | boolean | React$1.ReactElement<any, string | React$1.JSXElementConstructor<any>> | Iterable<React$1.ReactNode> | null | undefined;
593
+ declare function FormRow({ row, className, ...props }: FormRowProps): react_jsx_runtime.JSX.Element;
574
594
 
575
- interface FormSubmitButtonProps {
576
- className?: string;
577
- children?: React__default.ReactNode | RendererChildrenFunction<FormSubmitButtonRendererProps>;
578
- renderAs?: 'default' | 'children' | boolean;
579
- }
580
- declare function FormSubmitButton({ className, children, renderAs }: FormSubmitButtonProps): string | number | boolean | React__default.ReactElement<any, string | React__default.JSXElementConstructor<any>> | Iterable<React__default.ReactNode> | null | undefined;
595
+ declare function FormSubmitButton({ className, ...props }: ComponentRendererBaseProps<FormSubmitButtonRendererProps>): react_jsx_runtime.JSX.Element;
581
596
 
582
- export { type FieldConfig, Form, FormBody, type FormBodyProps, form as FormBuilder, type FormContextValue, FormField, type FormFieldProps, type FormProps, FormProvider, type FormProviderProps, FormRow, type FormRowProps, type FormState, FormSubmitButton, type FormSubmitButtonProps, createForm, form, useFormContext };
597
+ export { type FieldConfig, Form, FormBody, form as FormBuilder, FormField, FormProvider, FormRow, FormSubmitButton, createForm, form, useFormContext };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- 'use strict';var core=require('@rilaykit/core'),se=require('react'),jsxRuntime=require('react/jsx-runtime');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var se__default=/*#__PURE__*/_interopDefault(se);var w=class t{constructor(e,r){this.rows=[];this.idGenerator=new core.IdGenerator;this.config=e,this.formId=r||`form-${Date.now()}`;}static create(e,r){return new t(e,r)}createFormField(e){let r=this.config.getComponent(e.type);if(!r)throw new Error(`No component found with type "${e.type}"`);return {id:e.id||this.idGenerator.next("field"),componentId:r.id,props:{...r.defaultProps,...e.props},validation:e.validation,conditional:e.conditional}}createRow(e,r){if(e.length===0)throw new Error("At least one field is required");if(e.length>3)throw new Error("Maximum 3 fields per row");let n=e.map(o=>this.createFormField(o));return {id:this.idGenerator.next("row"),fields:n,maxColumns:e.length,spacing:r?.spacing||"normal",alignment:r?.alignment||"stretch"}}add(...e){let r,n,o=false;if(e.length===1&&Array.isArray(e[0])?(r=e[0],o=true):e.length===2&&Array.isArray(e[0])?(r=e[0],n=e[1],o=true):r=e,r.length===0)throw new Error("At least one field is required");if(o&&r.length>3)throw new Error("Maximum 3 fields per row");if(r.length===1){let c=this.createRow(r,n);return this.rows.push(c),this}if(r.length<=3){let c=this.createRow(r,n);return this.rows.push(c),this}for(let c of r){let f=this.createRow([c],n);this.rows.push(f);}return this}addSeparateRows(e,r){for(let n of e)this.add([n],r);return this}setId(e){return this.formId=e,this}updateField(e,r){let n=this.findField(e);if(!n)throw new Error(`Field with ID "${e}" not found`);return Object.assign(n,{...r,props:{...n.props,...r.props}}),this}findField(e){for(let r of this.rows){let n=r.fields.find(o=>o.id===e);if(n)return n}return null}removeField(e){return this.rows=this.rows.map(r=>({...r,fields:r.fields.filter(n=>n.id!==e)})).filter(r=>r.fields.length>0),this}getField(e){return this.findField(e)||void 0}getFields(){return this.rows.flatMap(e=>e.fields)}getRows(){return [...this.rows]}clear(){return this.rows=[],this.idGenerator.reset(),this}clone(e){let r=new t(this.config,e||`${this.formId}-clone`);return r.rows=core.deepClone(this.rows),r}validate(){let e=new core.ValidationErrorBuilder,r=this.getFields(),n=r.map(o=>o.id);try{core.ensureUnique(n,"field");}catch(o){e.add("DUPLICATE_FIELD_IDS",o instanceof Error?o.message:String(o));}for(let o of r)e.addIf(!this.config.hasComponent(o.componentId),"MISSING_COMPONENT",`Component "${o.componentId}" not found for field "${o.id}"`);for(let o of this.rows)e.addIf(o.fields.length>3,"TOO_MANY_FIELDS_IN_ROW",`Row "${o.id}" has ${o.fields.length} fields, maximum is 3`),e.addIf(o.fields.length===0,"EMPTY_ROW",`Row "${o.id}" is empty`);return e.build().map(o=>o.message)}build(){let e=this.validate();if(e.length>0)throw new Error(`Form validation failed: ${e.join(", ")}`);return {id:this.formId,rows:[...this.rows],allFields:this.getFields(),config:this.config,renderConfig:this.config.getFormRenderConfig()}}toJSON(){return {id:this.formId,rows:this.rows}}fromJSON(e){return e.id&&(this.formId=e.id),e.rows&&(this.rows=e.rows),this}getStats(){let e=this.getFields(),r=this.rows.map(n=>n.fields.length);return {totalFields:e.length,totalRows:this.rows.length,averageFieldsPerRow:this.rows.length>0?e.length/this.rows.length:0,maxFieldsInRow:r.length>0?Math.max(...r):0,minFieldsInRow:r.length>0?Math.min(...r):0}}};function j(t,e){return w.create(t,e)}core.ril.prototype.form=function(t){return w.create(this,t)};function te(t,e){switch(e.type){case "SET_VALUE":{let r={...t.values,[e.fieldId]:e.value};return {...t,values:r,isDirty:true}}case "SET_ERROR":return {...t,errors:{...t.errors,[e.fieldId]:e.errors},isValid:false};case "CLEAR_ERROR":{let r={...t.errors};return delete r[e.fieldId],{...t,errors:r}}case "MARK_TOUCHED":return {...t,touched:new Set([...t.touched,e.fieldId])};case "SET_VALIDATING":{let r=new Set(t.isValidating);return e.isValidating?r.add(e.fieldId):r.delete(e.fieldId),{...t,isValidating:r}}case "SET_SUBMITTING":return {...t,isSubmitting:e.isSubmitting};case "RESET":return {values:e.values||{},errors:{},touched:new Set,isValidating:new Set,isDirty:false,isValid:true,isSubmitting:false};case "UPDATE_VALIDATION_STATE":{let r=Object.keys(t.errors).some(n=>t.errors[n].length>0);return {...t,isValid:!r}}default:return t}}var L=se.createContext(null);function D({children:t,formConfig:e,defaultValues:r={},onSubmit:n,onFieldChange:o,className:c}){let f={values:r,errors:{},touched:new Set,isValidating:new Set,isDirty:false,isValid:true,isSubmitting:false},[d,a]=se.useReducer(te,f),i=se.useRef(new Map),m=se.useRef(n),l=se.useRef(o);m.current=n,l.current=o;let C=se.useCallback((s,u)=>{a({type:"SET_ERROR",fieldId:s,errors:u}),a({type:"UPDATE_VALIDATION_STATE"});},[]),v=se.useCallback(s=>{a({type:"CLEAR_ERROR",fieldId:s}),a({type:"UPDATE_VALIDATION_STATE"});},[]),S=se.useCallback(s=>{a({type:"MARK_TOUCHED",fieldId:s});},[]),p=se.useCallback((s,u)=>{a({type:"SET_VALIDATING",fieldId:s,isValidating:u});},[]),V=se.useCallback((s,u)=>{if(a({type:"SET_VALUE",fieldId:s,value:u}),d.errors[s]&&d.errors[s].length>0&&(a({type:"CLEAR_ERROR",fieldId:s}),a({type:"UPDATE_VALIDATION_STATE"})),l.current){let g={...d.values,[s]:u};l.current(s,u,g);}},[d.errors,d.values]),h=se.useMemo(()=>e,[e]),T=se.useCallback(async(s,u)=>{let g=h.allFields.find(I=>I.id===s);if(!g?.validation?.validator)return {isValid:true,errors:[]};let J=u!==void 0?u:d.values[s],O=i.current.get(s);O&&clearTimeout(O);let K={fieldId:s,formData:d.values,fieldProps:g.props,touched:d.touched.has(s),dirty:d.isDirty},N=g.validation.debounceMs||0;return new Promise(I=>{let B=async()=>{p(s,true);try{let y=await g.validation.validator(J,K,g.props);y.errors.length>0?C(s,y.errors):v(s),I(y);}catch(y){let k={isValid:false,errors:[{code:"validation_error",message:y instanceof Error?y.message:"Validation error"}]};C(s,k.errors),I(k);}finally{p(s,false);}};if(N>0){let y=setTimeout(B,N);i.current.set(s,y);}else B();})},[h,d.values,d.touched,d.isDirty,C,v,p]),E=se.useCallback(async()=>{let s=h.allFields.map(g=>T(g.id));return (await Promise.all(s)).every(g=>g.isValid)},[h,T]),b=se.useCallback(s=>{a({type:"RESET",values:s});},[]),x=se.useCallback(async s=>{if(s?.preventDefault(),!m.current)return true;a({type:"SET_SUBMITTING",isSubmitting:true});try{return await E()?(await m.current(d.values),!0):!1}catch(u){return console.error("Error during form submission:",u),false}finally{a({type:"SET_SUBMITTING",isSubmitting:false});}},[d.values,E]),A=se.useMemo(()=>({formState:d,formConfig:h,setValue:V,setError:C,clearError:v,markFieldTouched:S,setFieldValidating:p,validateField:T,validateAllFields:E,reset:b,submit:x}),[d,h,V,C,v,S,p,T,E,b,x]);return jsxRuntime.jsx(L.Provider,{value:A,children:jsxRuntime.jsx("form",{onSubmit:x,className:c,noValidate:true,children:t})})}function R(){let t=se.useContext(L);if(!t)throw new Error("useFormContext must be used within a FormProvider");return t}function ie({formConfig:t,defaultValues:e,onSubmit:r,onFieldChange:n,children:o}){let c=t instanceof w?t.build():t;return jsxRuntime.jsx(D,{formConfig:c,defaultValues:e,onSubmit:r,onFieldChange:n,children:o})}function G({fieldId:t,disabled:e=false,customProps:r={},className:n}){let{formState:o,formConfig:c,setValue:f,markFieldTouched:d,validateField:a}=R(),i=se.useMemo(()=>c.allFields.find(s=>s.id===t),[c.allFields,t]);if(!i)throw new Error(`Field with ID "${t}" not found`);let m=se.useMemo(()=>c.config.getComponent(i.componentId),[c.config,i.componentId]);if(!m)throw new Error(`Component with ID "${i.componentId}" not found`);let l=se.useMemo(()=>({value:o.values[i.id],errors:o.errors[i.id]||[],touched:o.touched.has(i.id),validating:o.isValidating.has(i.id)}),[o.values,o.errors,o.touched,o.isValidating,i.id]),C=se.useCallback(s=>{let u=l.errors.length>0;f(i.id,s),(i.validation?.validateOnChange||u&&i.validation?.validator||l.touched&&i.validation?.validator)&&a(i.id,s);},[i.id,i.validation,f,a,l.errors.length,l.touched]),v=se.useCallback(()=>{d(i.id),(i.validation?.validateOnBlur||i.validation?.validator)&&a(i.id);},[i.id,i.validation,d,a]),S=se.useMemo(()=>{if(!i.conditional)return true;try{return i.conditional.condition(o.values)}catch(s){return console.warn(`Conditional evaluation failed for field "${i.id}":`,s),true}},[i.conditional,o.values,i.id]),p=se.useMemo(()=>{if(!i.conditional||!S)return {};switch(i.conditional.action){case "disable":return {disabled:true};case "require":return {required:true};default:return {}}},[i.conditional,S]),V=!S&&i.conditional?.action==="hide",h=se.useMemo(()=>({...m.defaultProps??{},...i.props,...r,...p}),[m.defaultProps,i.props,r,p]),T=se.useMemo(()=>({id:i.id,props:h,value:l.value,onChange:C,onBlur:v,error:l.errors,touched:l.touched,disabled:e||p.disabled,isValidating:l.validating}),[i.id,h,l.value,C,v,l.errors,l.touched,e,p.disabled,l.validating]),E=c.renderConfig?.fieldRenderer,b=m.renderer(T),x=m.useFieldRenderer!==false,A=E&&x?E({children:b,id:i.id,error:l.errors,touched:l.touched,disabled:e||p.disabled,isValidating:l.validating,...h}):b;return jsxRuntime.jsx("div",{className:n,"data-field-id":i.id,"data-field-type":m.type,style:V?{display:"none !important"}:void 0,children:A})}var q=se__default.default.memo(G);function W({row:t,className:e,children:r,renderAs:n}){let{formConfig:o}=R(),c=t.fields.map(l=>jsxRuntime.jsx(q,{fieldId:l.id},l.id));if(n==="children"||n===true){if(typeof r!="function")throw new Error('When renderAs="children" is used, children must be a function that returns React elements');let l={row:t,children:c,className:e,spacing:t.spacing,alignment:t.alignment};return r(l)}let d=o.renderConfig?.rowRenderer;if(!d)throw new Error(`No rowRenderer configured for form "${o.id}". Please configure a rowRenderer using config.setRowRenderer() or config.setFormRenderConfig().`);let a={row:t,children:c,className:e,spacing:t.spacing,alignment:t.alignment},i=core.resolveRendererChildren(r,a),m={...a,children:i||c};return d(m)}var H=W;function ue({className:t,children:e,renderAs:r}){let{formConfig:n}=R(),o=se.useMemo(()=>n.rows.map(m=>jsxRuntime.jsx(H,{row:m},m.id)),[n.rows]);if(r==="children"||r===true){if(typeof e!="function")throw new Error('When renderAs="children" is used, children must be a function that returns React elements');return e({formConfig:n,children:o,className:t})}let f=n.renderConfig?.bodyRenderer;if(!f)throw new Error(`No bodyRenderer configured for form "${n.id}". Please configure a bodyRenderer using config.setBodyRenderer() or config.setFormRenderConfig().`);let d={formConfig:n,children:o,className:t},a=core.resolveRendererChildren(e,d),i={...d,children:a||o};return f(i)}function ge({className:t,children:e,renderAs:r}){let{formState:n,submit:o,formConfig:c}=R(),f={isSubmitting:n.isSubmitting,isValid:n.isValid,isDirty:n.isDirty,onSubmit:o,className:t};if(r==="children"||r===true){if(typeof e!="function")throw new Error('When renderAs="children" is used, children must be a function that returns React elements');return e(f)}let a=c.renderConfig?.submitButtonRenderer;if(!a)throw new Error(`No submitButtonRenderer configured for form "${c.id}". Please configure a submitButtonRenderer using config.setSubmitButtonRenderer() or config.setFormRenderConfig().`);let i=core.resolveRendererChildren(e,f),m={...f,children:i};return a(m)}Object.defineProperty(exports,"createZodValidator",{enumerable:true,get:function(){return core.createZodValidator}});Object.defineProperty(exports,"ril",{enumerable:true,get:function(){return core.ril}});exports.Form=ie;exports.FormBody=ue;exports.FormBuilder=w;exports.FormField=G;exports.FormProvider=D;exports.FormRow=W;exports.FormSubmitButton=ge;exports.createForm=j;exports.form=w;exports.useFormContext=R;
1
+ 'use strict';var se=require('react'),core=require('@rilaykit/core'),jsxRuntime=require('react/jsx-runtime');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var se__default=/*#__PURE__*/_interopDefault(se);var v=class t{constructor(e,r){this.rows=[];this.idGenerator=new core.IdGenerator;this.config=e,this.formId=r||`form-${Date.now()}`;}static create(e,r){return new t(e,r)}createFormField(e){let r=this.config.getComponent(e.type);if(!r)throw new Error(`No component found with type "${e.type}"`);let i;return (r.validation||e.validation)&&(i={validateOnChange:e.validation?.validateOnChange??r.validation?.validateOnChange,validateOnBlur:e.validation?.validateOnBlur??r.validation?.validateOnBlur,debounceMs:e.validation?.debounceMs??r.validation?.debounceMs,validators:[...r.validation?.validators||[],...e.validation?.validators||[]]}),{id:e.id||this.idGenerator.next("field"),componentId:r.id,props:{...r.defaultProps,...e.props},validation:i}}createRow(e){if(e.length===0)throw new Error("At least one field is required");if(e.length>3)throw new Error("Maximum 3 fields per row");let r=e.map(i=>this.createFormField(i));return {id:this.idGenerator.next("row"),fields:r,maxColumns:e.length}}add(...e){let r,i=false;if(e.length===1&&Array.isArray(e[0])?(r=e[0],i=true):r=e,r.length===0)throw new Error("At least one field is required");if(i&&r.length>3)throw new Error("Maximum 3 fields per row");if(r.length===1){let o=this.createRow(r);return this.rows.push(o),this}if(r.length<=3){let o=this.createRow(r);return this.rows.push(o),this}for(let o of r){let m=this.createRow([o]);this.rows.push(m);}return this}addSeparateRows(e){for(let r of e)this.add(r);return this}setId(e){return this.formId=e,this}updateField(e,r){let i=this.findField(e);if(!i)throw new Error(`Field with ID "${e}" not found`);return Object.assign(i,{...r,props:{...i.props,...r.props}}),this}findField(e){for(let r of this.rows){let i=r.fields.find(o=>o.id===e);if(i)return i}return null}removeField(e){return this.rows=this.rows.map(r=>({...r,fields:r.fields.filter(i=>i.id!==e)})).filter(r=>r.fields.length>0),this}getField(e){return this.findField(e)||void 0}getFields(){return this.rows.flatMap(e=>e.fields)}getRows(){return [...this.rows]}clear(){return this.rows=[],this.idGenerator.reset(),this}setValidation(e){return this.formValidation=e,this}addValidators(e){return this.formValidation||(this.formValidation={validators:[]}),this.formValidation={...this.formValidation,validators:[...this.formValidation.validators||[],...e]},this}addFieldValidation(e,r){let i=this.findField(e);if(!i)throw new Error(`Field with ID "${e}" not found`);let o={...i.validation,...r,validators:[...i.validation?.validators||[],...r.validators||[]]};return this.updateField(e,{validation:o})}clone(e){let r=new t(this.config,e||`${this.formId}-clone`);return r.rows=core.deepClone(this.rows),r}validate(){let e=[],r=this.getFields(),i=r.map(o=>o.id);try{core.ensureUnique(i,"field");}catch(o){e.push(o instanceof Error?o.message:String(o));}for(let o of r)this.config.hasComponent(o.componentId)||e.push(`Component "${o.componentId}" not found for field "${o.id}"`);for(let o of this.rows)o.fields.length>3&&e.push(`Row "${o.id}" has ${o.fields.length} fields, maximum is 3`),o.fields.length===0&&e.push(`Row "${o.id}" is empty`);return e}build(){let e=this.validate();if(e.length>0)throw new Error(`Form validation failed: ${e.join(", ")}`);return {id:this.formId,rows:[...this.rows],allFields:this.getFields(),config:this.config,renderConfig:this.config.getFormRenderConfig(),validation:this.formValidation}}toJSON(){return {id:this.formId,rows:this.rows}}fromJSON(e){return e.id&&(this.formId=e.id),e.rows&&(this.rows=e.rows),this}getStats(){let e=this.getFields(),r=this.rows.map(i=>i.fields.length);return {totalFields:e.length,totalRows:this.rows.length,averageFieldsPerRow:this.rows.length>0?e.length/this.rows.length:0,maxFieldsInRow:r.length>0?Math.max(...r):0,minFieldsInRow:r.length>0?Math.min(...r):0}}};function Z(t,e){return v.create(t,e)}core.ril.prototype.form=function(t){return v.create(this,t)};function k(){return {isValid:true,errors:[]}}function te(t,e){switch(e.type){case "SET_VALUE":{let r={...t.values,[e.fieldId]:e.value};return {...t,values:r,isDirty:true}}case "SET_FIELD_ERRORS":return {...t,errors:{...t.errors,[e.fieldId]:e.errors},touched:{...t.touched,[e.fieldId]:true}};case "SET_FIELD_VALIDATION_STATE":return {...t,validationState:{...t.validationState,[e.fieldId]:e.state}};case "SET_FIELD_TOUCHED":return {...t,touched:{...t.touched,[e.fieldId]:e.touched}};case "SET_SUBMITTING":return {...t,isSubmitting:e.isSubmitting};case "SET_FORM_VALIDITY":return {...t,isValid:e.isValid};case "RESET":return {values:e.values||{},errors:{},validationState:{},touched:{},isDirty:false,isSubmitting:false,isValid:true};default:return t}}var U=se.createContext(null);function B({children:t,formConfig:e,defaultValues:r={},onSubmit:i,onFieldChange:o,className:m}){let R={values:r,errors:{},validationState:{},touched:{},isDirty:false,isSubmitting:false,isValid:true},[s,d]=se.useReducer(te,R),a=se.useRef(i),c=se.useRef(o),f=se.useRef(r);a.current=i,c.current=o,se.useEffect(()=>{JSON.stringify(f.current)!==JSON.stringify(r)&&(f.current=r,d({type:"RESET",values:r}));},[r]);let E=se.useCallback((n,l)=>{d({type:"SET_VALUE",fieldId:n,value:l}),c.current?.(n,l,{...s.values,[n]:l});},[s.values]),S=se.useCallback((n,l=true)=>{d({type:"SET_FIELD_TOUCHED",fieldId:n,touched:l});},[]),g=se.useCallback(async(n,l)=>{let p=e.allFields.find(u=>u.id===n);if(!p||!p.validation?.validators)return k();let y=l!==void 0?l:s.values[n],_=core.createValidationContext({fieldId:n,formId:e.id,allFormData:{...s.values,[n]:y}});d({type:"SET_FIELD_VALIDATION_STATE",fieldId:n,state:"validating"});try{let u=await core.runValidatorsAsync(p.validation.validators,y,_);return d({type:"SET_FIELD_ERRORS",fieldId:n,errors:u.errors}),d({type:"SET_FIELD_VALIDATION_STATE",fieldId:n,state:u.isValid?"valid":"invalid"}),u}catch(u){let x={isValid:false,errors:[{message:u instanceof Error?u.message:"Validation failed",code:"VALIDATION_ERROR"}]};return d({type:"SET_FIELD_ERRORS",fieldId:n,errors:x.errors}),d({type:"SET_FIELD_VALIDATION_STATE",fieldId:n,state:"invalid"}),x}},[e,s.values]),w=se.useCallback(async()=>{let n=e.allFields.filter(u=>u.validation?.validators).map(u=>g(u.id)),l=await Promise.all(n),p=l.some(u=>!u.isValid),y=k();if(e.validation?.validators){let u=core.createValidationContext({formId:e.id,allFormData:s.values});try{y=await core.runValidatorsAsync(e.validation.validators,s.values,u);}catch(x){y={isValid:false,errors:[{message:x instanceof Error?x.message:"Form validation failed",code:"FORM_VALIDATION_ERROR"}]};}}let _={isValid:!p&&y.isValid,errors:[...l.flatMap(u=>u.errors),...y.errors]};return d({type:"SET_FORM_VALIDITY",isValid:_.isValid}),_},[e,s.values,g]),T=se.useCallback(()=>{let n=Object.values(s.errors).some(p=>p.length>0),l=Object.values(s.validationState).some(p=>p==="invalid");return s.isValid&&!n&&!l},[s.isValid,s.errors,s.validationState]),V=se.useMemo(()=>e,[e]),b=se.useCallback(n=>{d({type:"RESET",values:n});},[]),P=se.useCallback((n,l)=>{d({type:"SET_FIELD_ERRORS",fieldId:n,errors:l}),l.length>0&&d({type:"SET_FORM_VALIDITY",isValid:false});},[]),C=se.useCallback(n=>{d({type:"SET_FIELD_ERRORS",fieldId:n,errors:[]});},[]),D=se.useCallback(async n=>{n?.preventDefault();try{return d({type:"SET_SUBMITTING",isSubmitting:!0}),(await w()).isValid?(a.current&&await a.current(s.values),!0):!1}catch(l){return console.error("Error during form submission:",l),false}finally{d({type:"SET_SUBMITTING",isSubmitting:false});}},[s.values,w]),H=se.useMemo(()=>({formState:s,formConfig:V,setValue:E,setFieldTouched:S,validateField:g,validateForm:w,isFormValid:T,reset:b,submit:D,setError:P,clearError:C}),[s,V,E,S,g,w,T,b,D,P,C]);return jsxRuntime.jsx(U.Provider,{value:H,children:jsxRuntime.jsx("form",{onSubmit:D,className:m,noValidate:true,children:t})})}function h(){let t=se.useContext(U);if(!t)throw new Error("useFormContext must be used within a FormProvider");return t}function ne({formConfig:t,defaultValues:e,onSubmit:r,onFieldChange:i,children:o}){let m=se.useMemo(()=>t instanceof v?t.build():t,[t]);return jsxRuntime.jsx(B,{formConfig:m,defaultValues:e,onSubmit:r,onFieldChange:i,children:o})}function O({fieldId:t,disabled:e=false,customProps:r={},className:i}){let{formState:o,formConfig:m,setValue:R,setFieldTouched:s,validateField:d}=h(),a=se.useMemo(()=>m.allFields.find(C=>C.id===t),[m.allFields,t]);if(!a)throw new Error(`Field with ID "${t}" not found`);let c=se.useMemo(()=>m.config.getComponent(a.componentId),[m.config,a.componentId]);if(!c)throw new Error(`Component with ID "${a.componentId}" not found`);let f=se.useMemo(()=>({value:o.values[t],errors:o.errors[t]||[],validationState:o.validationState[t]||"idle",isValidating:o.validationState[t]==="validating",isTouched:o.touched[t]||false}),[o.values,o.errors,o.validationState,o.touched,t]),E=se.useCallback(async C=>{R(a.id,C),(a.validation?.validateOnChange||f.isTouched)&&await d(a.id,C);},[a.id,a.validation?.validateOnChange,f.isTouched,R,d]),S=se.useCallback(async()=>{f.isTouched||s(a.id,true),(a.validation?.validateOnBlur||!a.validation?.validateOnChange)&&await d(a.id);},[a.id,a.validation?.validateOnBlur,a.validation?.validateOnChange,f.isTouched,s,d]),g=se.useMemo(()=>({...c.defaultProps??{},...a.props,...r}),[c.defaultProps,a.props,r]),w=se.useMemo(()=>({id:a.id,props:g,value:f.value,onChange:E,onBlur:S,disabled:e,error:f.errors,isValidating:f.isValidating,touched:f.isTouched}),[a.id,g,f.value,f.errors,f.isValidating,f.isTouched,E,S,e]),T=m.renderConfig?.fieldRenderer,V=c.renderer(w),b=c.useFieldRenderer!==false,P=T&&b?T({children:V,id:a.id,disabled:e,...g,...f}):V;return jsxRuntime.jsx("div",{className:i,"data-field-id":a.id,"data-field-type":c.type,children:P})}se__default.default.memo(O);function J({row:t,className:e,...r}){let{formConfig:i}=h(),o=t.fields.map(R=>jsxRuntime.jsx(O,{fieldId:R.id},R.id)),m={row:t,children:o,className:e};return jsxRuntime.jsx(core.ComponentRendererWrapper,{name:"FormRow",renderer:i.renderConfig?.rowRenderer,props:m,...r,children:o})}var Y=J;function fe({className:t,...e}){let{formConfig:r}=h(),i=se.useMemo(()=>r.rows.map(m=>jsxRuntime.jsx(Y,{row:m},m.id)),[r.rows]),o={formConfig:r,children:i,className:t};return jsxRuntime.jsx(core.ComponentRendererWrapper,{name:"FormBody",renderer:r.renderConfig?.bodyRenderer,props:o,...e,children:i})}function pe({className:t,...e}){let{formState:r,submit:i,formConfig:o}=h(),m={isSubmitting:r.isSubmitting,onSubmit:i,className:t};return jsxRuntime.jsx(core.ComponentRendererWrapper,{name:"FormSubmitButton",renderer:o.renderConfig?.submitButtonRenderer,props:m,...e})}exports.Form=ne;exports.FormBody=fe;exports.FormBuilder=v;exports.FormField=O;exports.FormProvider=B;exports.FormRow=J;exports.FormSubmitButton=pe;exports.createForm=Z;exports.form=v;exports.useFormContext=h;Object.keys(core).forEach(function(k){if(k!=='default'&&!Object.prototype.hasOwnProperty.call(exports,k))Object.defineProperty(exports,k,{enumerable:true,get:function(){return core[k]}})});
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import {ril,IdGenerator,deepClone,ValidationErrorBuilder,ensureUnique,resolveRendererChildren}from'@rilaykit/core';export{createZodValidator,ril}from'@rilaykit/core';import se,{createContext,useMemo,useCallback,useContext,useReducer,useRef}from'react';import {jsx}from'react/jsx-runtime';var w=class t{constructor(e,r){this.rows=[];this.idGenerator=new IdGenerator;this.config=e,this.formId=r||`form-${Date.now()}`;}static create(e,r){return new t(e,r)}createFormField(e){let r=this.config.getComponent(e.type);if(!r)throw new Error(`No component found with type "${e.type}"`);return {id:e.id||this.idGenerator.next("field"),componentId:r.id,props:{...r.defaultProps,...e.props},validation:e.validation,conditional:e.conditional}}createRow(e,r){if(e.length===0)throw new Error("At least one field is required");if(e.length>3)throw new Error("Maximum 3 fields per row");let n=e.map(o=>this.createFormField(o));return {id:this.idGenerator.next("row"),fields:n,maxColumns:e.length,spacing:r?.spacing||"normal",alignment:r?.alignment||"stretch"}}add(...e){let r,n,o=false;if(e.length===1&&Array.isArray(e[0])?(r=e[0],o=true):e.length===2&&Array.isArray(e[0])?(r=e[0],n=e[1],o=true):r=e,r.length===0)throw new Error("At least one field is required");if(o&&r.length>3)throw new Error("Maximum 3 fields per row");if(r.length===1){let c=this.createRow(r,n);return this.rows.push(c),this}if(r.length<=3){let c=this.createRow(r,n);return this.rows.push(c),this}for(let c of r){let f=this.createRow([c],n);this.rows.push(f);}return this}addSeparateRows(e,r){for(let n of e)this.add([n],r);return this}setId(e){return this.formId=e,this}updateField(e,r){let n=this.findField(e);if(!n)throw new Error(`Field with ID "${e}" not found`);return Object.assign(n,{...r,props:{...n.props,...r.props}}),this}findField(e){for(let r of this.rows){let n=r.fields.find(o=>o.id===e);if(n)return n}return null}removeField(e){return this.rows=this.rows.map(r=>({...r,fields:r.fields.filter(n=>n.id!==e)})).filter(r=>r.fields.length>0),this}getField(e){return this.findField(e)||void 0}getFields(){return this.rows.flatMap(e=>e.fields)}getRows(){return [...this.rows]}clear(){return this.rows=[],this.idGenerator.reset(),this}clone(e){let r=new t(this.config,e||`${this.formId}-clone`);return r.rows=deepClone(this.rows),r}validate(){let e=new ValidationErrorBuilder,r=this.getFields(),n=r.map(o=>o.id);try{ensureUnique(n,"field");}catch(o){e.add("DUPLICATE_FIELD_IDS",o instanceof Error?o.message:String(o));}for(let o of r)e.addIf(!this.config.hasComponent(o.componentId),"MISSING_COMPONENT",`Component "${o.componentId}" not found for field "${o.id}"`);for(let o of this.rows)e.addIf(o.fields.length>3,"TOO_MANY_FIELDS_IN_ROW",`Row "${o.id}" has ${o.fields.length} fields, maximum is 3`),e.addIf(o.fields.length===0,"EMPTY_ROW",`Row "${o.id}" is empty`);return e.build().map(o=>o.message)}build(){let e=this.validate();if(e.length>0)throw new Error(`Form validation failed: ${e.join(", ")}`);return {id:this.formId,rows:[...this.rows],allFields:this.getFields(),config:this.config,renderConfig:this.config.getFormRenderConfig()}}toJSON(){return {id:this.formId,rows:this.rows}}fromJSON(e){return e.id&&(this.formId=e.id),e.rows&&(this.rows=e.rows),this}getStats(){let e=this.getFields(),r=this.rows.map(n=>n.fields.length);return {totalFields:e.length,totalRows:this.rows.length,averageFieldsPerRow:this.rows.length>0?e.length/this.rows.length:0,maxFieldsInRow:r.length>0?Math.max(...r):0,minFieldsInRow:r.length>0?Math.min(...r):0}}};function j(t,e){return w.create(t,e)}ril.prototype.form=function(t){return w.create(this,t)};function te(t,e){switch(e.type){case "SET_VALUE":{let r={...t.values,[e.fieldId]:e.value};return {...t,values:r,isDirty:true}}case "SET_ERROR":return {...t,errors:{...t.errors,[e.fieldId]:e.errors},isValid:false};case "CLEAR_ERROR":{let r={...t.errors};return delete r[e.fieldId],{...t,errors:r}}case "MARK_TOUCHED":return {...t,touched:new Set([...t.touched,e.fieldId])};case "SET_VALIDATING":{let r=new Set(t.isValidating);return e.isValidating?r.add(e.fieldId):r.delete(e.fieldId),{...t,isValidating:r}}case "SET_SUBMITTING":return {...t,isSubmitting:e.isSubmitting};case "RESET":return {values:e.values||{},errors:{},touched:new Set,isValidating:new Set,isDirty:false,isValid:true,isSubmitting:false};case "UPDATE_VALIDATION_STATE":{let r=Object.keys(t.errors).some(n=>t.errors[n].length>0);return {...t,isValid:!r}}default:return t}}var L=createContext(null);function D({children:t,formConfig:e,defaultValues:r={},onSubmit:n,onFieldChange:o,className:c}){let f={values:r,errors:{},touched:new Set,isValidating:new Set,isDirty:false,isValid:true,isSubmitting:false},[d,a]=useReducer(te,f),i=useRef(new Map),m=useRef(n),l=useRef(o);m.current=n,l.current=o;let C=useCallback((s,u)=>{a({type:"SET_ERROR",fieldId:s,errors:u}),a({type:"UPDATE_VALIDATION_STATE"});},[]),v=useCallback(s=>{a({type:"CLEAR_ERROR",fieldId:s}),a({type:"UPDATE_VALIDATION_STATE"});},[]),S=useCallback(s=>{a({type:"MARK_TOUCHED",fieldId:s});},[]),p=useCallback((s,u)=>{a({type:"SET_VALIDATING",fieldId:s,isValidating:u});},[]),V=useCallback((s,u)=>{if(a({type:"SET_VALUE",fieldId:s,value:u}),d.errors[s]&&d.errors[s].length>0&&(a({type:"CLEAR_ERROR",fieldId:s}),a({type:"UPDATE_VALIDATION_STATE"})),l.current){let g={...d.values,[s]:u};l.current(s,u,g);}},[d.errors,d.values]),h=useMemo(()=>e,[e]),T=useCallback(async(s,u)=>{let g=h.allFields.find(I=>I.id===s);if(!g?.validation?.validator)return {isValid:true,errors:[]};let J=u!==void 0?u:d.values[s],O=i.current.get(s);O&&clearTimeout(O);let K={fieldId:s,formData:d.values,fieldProps:g.props,touched:d.touched.has(s),dirty:d.isDirty},N=g.validation.debounceMs||0;return new Promise(I=>{let B=async()=>{p(s,true);try{let y=await g.validation.validator(J,K,g.props);y.errors.length>0?C(s,y.errors):v(s),I(y);}catch(y){let k={isValid:false,errors:[{code:"validation_error",message:y instanceof Error?y.message:"Validation error"}]};C(s,k.errors),I(k);}finally{p(s,false);}};if(N>0){let y=setTimeout(B,N);i.current.set(s,y);}else B();})},[h,d.values,d.touched,d.isDirty,C,v,p]),E=useCallback(async()=>{let s=h.allFields.map(g=>T(g.id));return (await Promise.all(s)).every(g=>g.isValid)},[h,T]),b=useCallback(s=>{a({type:"RESET",values:s});},[]),x=useCallback(async s=>{if(s?.preventDefault(),!m.current)return true;a({type:"SET_SUBMITTING",isSubmitting:true});try{return await E()?(await m.current(d.values),!0):!1}catch(u){return console.error("Error during form submission:",u),false}finally{a({type:"SET_SUBMITTING",isSubmitting:false});}},[d.values,E]),A=useMemo(()=>({formState:d,formConfig:h,setValue:V,setError:C,clearError:v,markFieldTouched:S,setFieldValidating:p,validateField:T,validateAllFields:E,reset:b,submit:x}),[d,h,V,C,v,S,p,T,E,b,x]);return jsx(L.Provider,{value:A,children:jsx("form",{onSubmit:x,className:c,noValidate:true,children:t})})}function R(){let t=useContext(L);if(!t)throw new Error("useFormContext must be used within a FormProvider");return t}function ie({formConfig:t,defaultValues:e,onSubmit:r,onFieldChange:n,children:o}){let c=t instanceof w?t.build():t;return jsx(D,{formConfig:c,defaultValues:e,onSubmit:r,onFieldChange:n,children:o})}function G({fieldId:t,disabled:e=false,customProps:r={},className:n}){let{formState:o,formConfig:c,setValue:f,markFieldTouched:d,validateField:a}=R(),i=useMemo(()=>c.allFields.find(s=>s.id===t),[c.allFields,t]);if(!i)throw new Error(`Field with ID "${t}" not found`);let m=useMemo(()=>c.config.getComponent(i.componentId),[c.config,i.componentId]);if(!m)throw new Error(`Component with ID "${i.componentId}" not found`);let l=useMemo(()=>({value:o.values[i.id],errors:o.errors[i.id]||[],touched:o.touched.has(i.id),validating:o.isValidating.has(i.id)}),[o.values,o.errors,o.touched,o.isValidating,i.id]),C=useCallback(s=>{let u=l.errors.length>0;f(i.id,s),(i.validation?.validateOnChange||u&&i.validation?.validator||l.touched&&i.validation?.validator)&&a(i.id,s);},[i.id,i.validation,f,a,l.errors.length,l.touched]),v=useCallback(()=>{d(i.id),(i.validation?.validateOnBlur||i.validation?.validator)&&a(i.id);},[i.id,i.validation,d,a]),S=useMemo(()=>{if(!i.conditional)return true;try{return i.conditional.condition(o.values)}catch(s){return console.warn(`Conditional evaluation failed for field "${i.id}":`,s),true}},[i.conditional,o.values,i.id]),p=useMemo(()=>{if(!i.conditional||!S)return {};switch(i.conditional.action){case "disable":return {disabled:true};case "require":return {required:true};default:return {}}},[i.conditional,S]),V=!S&&i.conditional?.action==="hide",h=useMemo(()=>({...m.defaultProps??{},...i.props,...r,...p}),[m.defaultProps,i.props,r,p]),T=useMemo(()=>({id:i.id,props:h,value:l.value,onChange:C,onBlur:v,error:l.errors,touched:l.touched,disabled:e||p.disabled,isValidating:l.validating}),[i.id,h,l.value,C,v,l.errors,l.touched,e,p.disabled,l.validating]),E=c.renderConfig?.fieldRenderer,b=m.renderer(T),x=m.useFieldRenderer!==false,A=E&&x?E({children:b,id:i.id,error:l.errors,touched:l.touched,disabled:e||p.disabled,isValidating:l.validating,...h}):b;return jsx("div",{className:n,"data-field-id":i.id,"data-field-type":m.type,style:V?{display:"none !important"}:void 0,children:A})}var q=se.memo(G);function W({row:t,className:e,children:r,renderAs:n}){let{formConfig:o}=R(),c=t.fields.map(l=>jsx(q,{fieldId:l.id},l.id));if(n==="children"||n===true){if(typeof r!="function")throw new Error('When renderAs="children" is used, children must be a function that returns React elements');let l={row:t,children:c,className:e,spacing:t.spacing,alignment:t.alignment};return r(l)}let d=o.renderConfig?.rowRenderer;if(!d)throw new Error(`No rowRenderer configured for form "${o.id}". Please configure a rowRenderer using config.setRowRenderer() or config.setFormRenderConfig().`);let a={row:t,children:c,className:e,spacing:t.spacing,alignment:t.alignment},i=resolveRendererChildren(r,a),m={...a,children:i||c};return d(m)}var H=W;function ue({className:t,children:e,renderAs:r}){let{formConfig:n}=R(),o=useMemo(()=>n.rows.map(m=>jsx(H,{row:m},m.id)),[n.rows]);if(r==="children"||r===true){if(typeof e!="function")throw new Error('When renderAs="children" is used, children must be a function that returns React elements');return e({formConfig:n,children:o,className:t})}let f=n.renderConfig?.bodyRenderer;if(!f)throw new Error(`No bodyRenderer configured for form "${n.id}". Please configure a bodyRenderer using config.setBodyRenderer() or config.setFormRenderConfig().`);let d={formConfig:n,children:o,className:t},a=resolveRendererChildren(e,d),i={...d,children:a||o};return f(i)}function ge({className:t,children:e,renderAs:r}){let{formState:n,submit:o,formConfig:c}=R(),f={isSubmitting:n.isSubmitting,isValid:n.isValid,isDirty:n.isDirty,onSubmit:o,className:t};if(r==="children"||r===true){if(typeof e!="function")throw new Error('When renderAs="children" is used, children must be a function that returns React elements');return e(f)}let a=c.renderConfig?.submitButtonRenderer;if(!a)throw new Error(`No submitButtonRenderer configured for form "${c.id}". Please configure a submitButtonRenderer using config.setSubmitButtonRenderer() or config.setFormRenderConfig().`);let i=resolveRendererChildren(e,f),m={...f,children:i};return a(m)}export{ie as Form,ue as FormBody,w as FormBuilder,G as FormField,D as FormProvider,W as FormRow,ge as FormSubmitButton,j as createForm,w as form,R as useFormContext};
1
+ import se,{createContext,useMemo,useCallback,useContext,useReducer,useRef,useEffect}from'react';import {ril,IdGenerator,deepClone,ensureUnique,createValidationContext,runValidatorsAsync,ComponentRendererWrapper}from'@rilaykit/core';export*from'@rilaykit/core';import {jsx}from'react/jsx-runtime';var v=class t{constructor(e,r){this.rows=[];this.idGenerator=new IdGenerator;this.config=e,this.formId=r||`form-${Date.now()}`;}static create(e,r){return new t(e,r)}createFormField(e){let r=this.config.getComponent(e.type);if(!r)throw new Error(`No component found with type "${e.type}"`);let i;return (r.validation||e.validation)&&(i={validateOnChange:e.validation?.validateOnChange??r.validation?.validateOnChange,validateOnBlur:e.validation?.validateOnBlur??r.validation?.validateOnBlur,debounceMs:e.validation?.debounceMs??r.validation?.debounceMs,validators:[...r.validation?.validators||[],...e.validation?.validators||[]]}),{id:e.id||this.idGenerator.next("field"),componentId:r.id,props:{...r.defaultProps,...e.props},validation:i}}createRow(e){if(e.length===0)throw new Error("At least one field is required");if(e.length>3)throw new Error("Maximum 3 fields per row");let r=e.map(i=>this.createFormField(i));return {id:this.idGenerator.next("row"),fields:r,maxColumns:e.length}}add(...e){let r,i=false;if(e.length===1&&Array.isArray(e[0])?(r=e[0],i=true):r=e,r.length===0)throw new Error("At least one field is required");if(i&&r.length>3)throw new Error("Maximum 3 fields per row");if(r.length===1){let o=this.createRow(r);return this.rows.push(o),this}if(r.length<=3){let o=this.createRow(r);return this.rows.push(o),this}for(let o of r){let m=this.createRow([o]);this.rows.push(m);}return this}addSeparateRows(e){for(let r of e)this.add(r);return this}setId(e){return this.formId=e,this}updateField(e,r){let i=this.findField(e);if(!i)throw new Error(`Field with ID "${e}" not found`);return Object.assign(i,{...r,props:{...i.props,...r.props}}),this}findField(e){for(let r of this.rows){let i=r.fields.find(o=>o.id===e);if(i)return i}return null}removeField(e){return this.rows=this.rows.map(r=>({...r,fields:r.fields.filter(i=>i.id!==e)})).filter(r=>r.fields.length>0),this}getField(e){return this.findField(e)||void 0}getFields(){return this.rows.flatMap(e=>e.fields)}getRows(){return [...this.rows]}clear(){return this.rows=[],this.idGenerator.reset(),this}setValidation(e){return this.formValidation=e,this}addValidators(e){return this.formValidation||(this.formValidation={validators:[]}),this.formValidation={...this.formValidation,validators:[...this.formValidation.validators||[],...e]},this}addFieldValidation(e,r){let i=this.findField(e);if(!i)throw new Error(`Field with ID "${e}" not found`);let o={...i.validation,...r,validators:[...i.validation?.validators||[],...r.validators||[]]};return this.updateField(e,{validation:o})}clone(e){let r=new t(this.config,e||`${this.formId}-clone`);return r.rows=deepClone(this.rows),r}validate(){let e=[],r=this.getFields(),i=r.map(o=>o.id);try{ensureUnique(i,"field");}catch(o){e.push(o instanceof Error?o.message:String(o));}for(let o of r)this.config.hasComponent(o.componentId)||e.push(`Component "${o.componentId}" not found for field "${o.id}"`);for(let o of this.rows)o.fields.length>3&&e.push(`Row "${o.id}" has ${o.fields.length} fields, maximum is 3`),o.fields.length===0&&e.push(`Row "${o.id}" is empty`);return e}build(){let e=this.validate();if(e.length>0)throw new Error(`Form validation failed: ${e.join(", ")}`);return {id:this.formId,rows:[...this.rows],allFields:this.getFields(),config:this.config,renderConfig:this.config.getFormRenderConfig(),validation:this.formValidation}}toJSON(){return {id:this.formId,rows:this.rows}}fromJSON(e){return e.id&&(this.formId=e.id),e.rows&&(this.rows=e.rows),this}getStats(){let e=this.getFields(),r=this.rows.map(i=>i.fields.length);return {totalFields:e.length,totalRows:this.rows.length,averageFieldsPerRow:this.rows.length>0?e.length/this.rows.length:0,maxFieldsInRow:r.length>0?Math.max(...r):0,minFieldsInRow:r.length>0?Math.min(...r):0}}};function Z(t,e){return v.create(t,e)}ril.prototype.form=function(t){return v.create(this,t)};function k(){return {isValid:true,errors:[]}}function te(t,e){switch(e.type){case "SET_VALUE":{let r={...t.values,[e.fieldId]:e.value};return {...t,values:r,isDirty:true}}case "SET_FIELD_ERRORS":return {...t,errors:{...t.errors,[e.fieldId]:e.errors},touched:{...t.touched,[e.fieldId]:true}};case "SET_FIELD_VALIDATION_STATE":return {...t,validationState:{...t.validationState,[e.fieldId]:e.state}};case "SET_FIELD_TOUCHED":return {...t,touched:{...t.touched,[e.fieldId]:e.touched}};case "SET_SUBMITTING":return {...t,isSubmitting:e.isSubmitting};case "SET_FORM_VALIDITY":return {...t,isValid:e.isValid};case "RESET":return {values:e.values||{},errors:{},validationState:{},touched:{},isDirty:false,isSubmitting:false,isValid:true};default:return t}}var U=createContext(null);function B({children:t,formConfig:e,defaultValues:r={},onSubmit:i,onFieldChange:o,className:m}){let R={values:r,errors:{},validationState:{},touched:{},isDirty:false,isSubmitting:false,isValid:true},[s,d]=useReducer(te,R),a=useRef(i),c=useRef(o),f=useRef(r);a.current=i,c.current=o,useEffect(()=>{JSON.stringify(f.current)!==JSON.stringify(r)&&(f.current=r,d({type:"RESET",values:r}));},[r]);let E=useCallback((n,l)=>{d({type:"SET_VALUE",fieldId:n,value:l}),c.current?.(n,l,{...s.values,[n]:l});},[s.values]),S=useCallback((n,l=true)=>{d({type:"SET_FIELD_TOUCHED",fieldId:n,touched:l});},[]),g=useCallback(async(n,l)=>{let p=e.allFields.find(u=>u.id===n);if(!p||!p.validation?.validators)return k();let y=l!==void 0?l:s.values[n],_=createValidationContext({fieldId:n,formId:e.id,allFormData:{...s.values,[n]:y}});d({type:"SET_FIELD_VALIDATION_STATE",fieldId:n,state:"validating"});try{let u=await runValidatorsAsync(p.validation.validators,y,_);return d({type:"SET_FIELD_ERRORS",fieldId:n,errors:u.errors}),d({type:"SET_FIELD_VALIDATION_STATE",fieldId:n,state:u.isValid?"valid":"invalid"}),u}catch(u){let x={isValid:false,errors:[{message:u instanceof Error?u.message:"Validation failed",code:"VALIDATION_ERROR"}]};return d({type:"SET_FIELD_ERRORS",fieldId:n,errors:x.errors}),d({type:"SET_FIELD_VALIDATION_STATE",fieldId:n,state:"invalid"}),x}},[e,s.values]),w=useCallback(async()=>{let n=e.allFields.filter(u=>u.validation?.validators).map(u=>g(u.id)),l=await Promise.all(n),p=l.some(u=>!u.isValid),y=k();if(e.validation?.validators){let u=createValidationContext({formId:e.id,allFormData:s.values});try{y=await runValidatorsAsync(e.validation.validators,s.values,u);}catch(x){y={isValid:false,errors:[{message:x instanceof Error?x.message:"Form validation failed",code:"FORM_VALIDATION_ERROR"}]};}}let _={isValid:!p&&y.isValid,errors:[...l.flatMap(u=>u.errors),...y.errors]};return d({type:"SET_FORM_VALIDITY",isValid:_.isValid}),_},[e,s.values,g]),T=useCallback(()=>{let n=Object.values(s.errors).some(p=>p.length>0),l=Object.values(s.validationState).some(p=>p==="invalid");return s.isValid&&!n&&!l},[s.isValid,s.errors,s.validationState]),V=useMemo(()=>e,[e]),b=useCallback(n=>{d({type:"RESET",values:n});},[]),P=useCallback((n,l)=>{d({type:"SET_FIELD_ERRORS",fieldId:n,errors:l}),l.length>0&&d({type:"SET_FORM_VALIDITY",isValid:false});},[]),C=useCallback(n=>{d({type:"SET_FIELD_ERRORS",fieldId:n,errors:[]});},[]),D=useCallback(async n=>{n?.preventDefault();try{return d({type:"SET_SUBMITTING",isSubmitting:!0}),(await w()).isValid?(a.current&&await a.current(s.values),!0):!1}catch(l){return console.error("Error during form submission:",l),false}finally{d({type:"SET_SUBMITTING",isSubmitting:false});}},[s.values,w]),H=useMemo(()=>({formState:s,formConfig:V,setValue:E,setFieldTouched:S,validateField:g,validateForm:w,isFormValid:T,reset:b,submit:D,setError:P,clearError:C}),[s,V,E,S,g,w,T,b,D,P,C]);return jsx(U.Provider,{value:H,children:jsx("form",{onSubmit:D,className:m,noValidate:true,children:t})})}function h(){let t=useContext(U);if(!t)throw new Error("useFormContext must be used within a FormProvider");return t}function ne({formConfig:t,defaultValues:e,onSubmit:r,onFieldChange:i,children:o}){let m=useMemo(()=>t instanceof v?t.build():t,[t]);return jsx(B,{formConfig:m,defaultValues:e,onSubmit:r,onFieldChange:i,children:o})}function O({fieldId:t,disabled:e=false,customProps:r={},className:i}){let{formState:o,formConfig:m,setValue:R,setFieldTouched:s,validateField:d}=h(),a=useMemo(()=>m.allFields.find(C=>C.id===t),[m.allFields,t]);if(!a)throw new Error(`Field with ID "${t}" not found`);let c=useMemo(()=>m.config.getComponent(a.componentId),[m.config,a.componentId]);if(!c)throw new Error(`Component with ID "${a.componentId}" not found`);let f=useMemo(()=>({value:o.values[t],errors:o.errors[t]||[],validationState:o.validationState[t]||"idle",isValidating:o.validationState[t]==="validating",isTouched:o.touched[t]||false}),[o.values,o.errors,o.validationState,o.touched,t]),E=useCallback(async C=>{R(a.id,C),(a.validation?.validateOnChange||f.isTouched)&&await d(a.id,C);},[a.id,a.validation?.validateOnChange,f.isTouched,R,d]),S=useCallback(async()=>{f.isTouched||s(a.id,true),(a.validation?.validateOnBlur||!a.validation?.validateOnChange)&&await d(a.id);},[a.id,a.validation?.validateOnBlur,a.validation?.validateOnChange,f.isTouched,s,d]),g=useMemo(()=>({...c.defaultProps??{},...a.props,...r}),[c.defaultProps,a.props,r]),w=useMemo(()=>({id:a.id,props:g,value:f.value,onChange:E,onBlur:S,disabled:e,error:f.errors,isValidating:f.isValidating,touched:f.isTouched}),[a.id,g,f.value,f.errors,f.isValidating,f.isTouched,E,S,e]),T=m.renderConfig?.fieldRenderer,V=c.renderer(w),b=c.useFieldRenderer!==false,P=T&&b?T({children:V,id:a.id,disabled:e,...g,...f}):V;return jsx("div",{className:i,"data-field-id":a.id,"data-field-type":c.type,children:P})}se.memo(O);function J({row:t,className:e,...r}){let{formConfig:i}=h(),o=t.fields.map(R=>jsx(O,{fieldId:R.id},R.id)),m={row:t,children:o,className:e};return jsx(ComponentRendererWrapper,{name:"FormRow",renderer:i.renderConfig?.rowRenderer,props:m,...r,children:o})}var Y=J;function fe({className:t,...e}){let{formConfig:r}=h(),i=useMemo(()=>r.rows.map(m=>jsx(Y,{row:m},m.id)),[r.rows]),o={formConfig:r,children:i,className:t};return jsx(ComponentRendererWrapper,{name:"FormBody",renderer:r.renderConfig?.bodyRenderer,props:o,...e,children:i})}function pe({className:t,...e}){let{formState:r,submit:i,formConfig:o}=h(),m={isSubmitting:r.isSubmitting,onSubmit:i,className:t};return jsx(ComponentRendererWrapper,{name:"FormSubmitButton",renderer:o.renderConfig?.submitButtonRenderer,props:m,...e})}export{ne as Form,fe as FormBody,v as FormBuilder,O as FormField,B as FormProvider,J as FormRow,pe as FormSubmitButton,Z as createForm,v as form,h as useFormContext};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rilaykit/forms",
3
- "version": "4.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Form building utilities and components for RilayKit",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,7 +24,7 @@
24
24
  "url": "https://github.com/andyoucreate/rilay/issues"
25
25
  },
26
26
  "dependencies": {
27
- "@rilaykit/core": "4.0.0"
27
+ "@rilaykit/core": "5.1.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "react": ">=18.0.0",