@connect-soft/form-generator 1.1.0-alpha7 → 1.1.0-alpha9

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 CHANGED
@@ -220,12 +220,13 @@ const fields = [
220
220
  ] as const;
221
221
  ```
222
222
 
223
- Don't forget to register the component for your custom field:
223
+ Then register the component for your custom field:
224
224
 
225
225
  ```typescript
226
226
  import { registerField } from '@connect-soft/form-generator';
227
227
  import { ColorPicker } from './components/ColorPicker';
228
228
 
229
+ // Type-safe: 'color-picker' must exist in FieldTypeRegistry
229
230
  registerField('color-picker', ({ field, formField }) => (
230
231
  <ColorPicker
231
232
  value={formField.value}
@@ -234,6 +235,71 @@ registerField('color-picker', ({ field, formField }) => (
234
235
  showAlpha={field.showAlpha}
235
236
  />
236
237
  ));
238
+
239
+ // ❌ TypeScript error: 'unknown-type' is not in FieldTypeRegistry
240
+ // registerField('unknown-type', MyComponent);
241
+ ```
242
+
243
+ > **Note:** Both `registerField` and `registerFields` enforce that field types must be defined in `FieldTypeRegistry`. This ensures type safety between your type definitions and runtime registrations.
244
+
245
+ ### Field Type Validation Helpers
246
+
247
+ Use helper functions for strict type checking without `as const`:
248
+
249
+ ```typescript
250
+ import { createField, createArrayField, strictFields } from '@connect-soft/form-generator';
251
+
252
+ // Create a single field with full type checking
253
+ const emailField = createField({
254
+ type: 'email',
255
+ name: 'email',
256
+ label: 'Email',
257
+ placeholder: 'Enter your email' // TypeScript knows this is valid for email
258
+ });
259
+
260
+ // Create an array field
261
+ const contacts = createArrayField({
262
+ name: 'contacts',
263
+ fields: [
264
+ { type: 'text', name: 'name', label: 'Name' },
265
+ { type: 'email', name: 'email', label: 'Email' }
266
+ ],
267
+ minItems: 1,
268
+ maxItems: 5
269
+ });
270
+
271
+ // Create an array of fields with type checking
272
+ const fields = strictFields([
273
+ { type: 'text', name: 'username', label: 'Username' },
274
+ { type: 'email', name: 'email', label: 'Email' },
275
+ // { type: 'unknown', name: 'bad' } // TypeScript error!
276
+ ]);
277
+ ```
278
+
279
+ ### Runtime Field Type Validation
280
+
281
+ Enable runtime validation to catch unregistered field types during development:
282
+
283
+ ```typescript
284
+ // Warn in console for unregistered types (recommended for development)
285
+ <FormGenerator
286
+ fields={fields}
287
+ onSubmit={handleSubmit}
288
+ validateTypes
289
+ />
290
+
291
+ // Throw an error for unregistered types
292
+ <FormGenerator
293
+ fields={fields}
294
+ onSubmit={handleSubmit}
295
+ validateTypes={{ throwOnError: true }}
296
+ />
297
+
298
+ // Manual validation
299
+ import { validateFieldTypes, getRegisteredFieldTypes } from '@connect-soft/form-generator';
300
+
301
+ const registeredTypes = getRegisteredFieldTypes();
302
+ validateFieldTypes(fields, registeredTypes, { throwOnError: true });
237
303
  ```
238
304
 
239
305
  ---
@@ -478,6 +544,225 @@ Use form state for conditional rendering:
478
544
 
479
545
  ---
480
546
 
547
+ ## Raw Field Props with useFieldProps
548
+
549
+ For complete control over field rendering, use the `useFieldProps` hook to get raw form binding props. This is useful when you want to create custom field components without registering them globally.
550
+
551
+ ```typescript
552
+ import { FormGenerator, useFieldProps } from '@connect-soft/form-generator';
553
+
554
+ const fields = [
555
+ { type: 'text', name: 'email', label: 'Email', required: true },
556
+ { type: 'password', name: 'password', label: 'Password' },
557
+ ] as const;
558
+
559
+ // Custom component using the hook
560
+ function CustomEmailField() {
561
+ const { value, onChange, onBlur, ref, field, fieldState } = useFieldProps<string>('email');
562
+
563
+ return (
564
+ <div className="my-custom-field">
565
+ <label>{field.label}{field.required && ' *'}</label>
566
+ <input
567
+ ref={ref}
568
+ type="email"
569
+ value={value ?? ''}
570
+ onChange={(e) => onChange(e.target.value)}
571
+ onBlur={onBlur}
572
+ placeholder={field.placeholder}
573
+ />
574
+ {fieldState.error && (
575
+ <span className="error">{fieldState.error.message}</span>
576
+ )}
577
+ </div>
578
+ );
579
+ }
580
+
581
+ // Use in FormGenerator with custom layout
582
+ <FormGenerator fields={fields} onSubmit={handleSubmit}>
583
+ {({ fields, buttons }) => (
584
+ <div>
585
+ <CustomEmailField />
586
+ {fields.password} {/* Mix with pre-rendered fields */}
587
+ {buttons.submit}
588
+ </div>
589
+ )}
590
+ </FormGenerator>
591
+ ```
592
+
593
+ ### useFieldProps Return Value
594
+
595
+ | Property | Type | Description |
596
+ |----------|------|-------------|
597
+ | `name` | `string` | Field name (with any prefix applied) |
598
+ | `value` | `TValue` | Current field value |
599
+ | `onChange` | `(value: TValue) => void` | Change handler |
600
+ | `onBlur` | `() => void` | Blur handler |
601
+ | `ref` | `Ref<any>` | Ref to attach to input element |
602
+ | `field` | `BaseField` | Full field definition (type, label, required, etc.) |
603
+ | `fieldState` | `FieldState` | Validation state (see below) |
604
+
605
+ ### FieldState Properties
606
+
607
+ | Property | Type | Description |
608
+ |----------|------|-------------|
609
+ | `invalid` | `boolean` | Whether the field has validation errors |
610
+ | `error` | `{ type: string; message?: string }` | Error details if invalid |
611
+ | `isDirty` | `boolean` | Whether the value has changed from default |
612
+ | `isTouched` | `boolean` | Whether the field has been focused and blurred |
613
+
614
+ ### When to Use useFieldProps vs Registered Components
615
+
616
+ | Use Case | Approach |
617
+ |----------|----------|
618
+ | Reusable field component across forms | Register with `registerField` |
619
+ | One-off custom field in a specific form | Use `useFieldProps` |
620
+ | Need full control over a single field | Use `useFieldProps` |
621
+ | Consistent field styling across app | Register with `registerField` |
622
+
623
+ ### Example: Complete Custom Form
624
+
625
+ ```typescript
626
+ import { FormGenerator, useFieldProps } from '@connect-soft/form-generator';
627
+
628
+ const fields = [
629
+ { type: 'text', name: 'firstName', label: 'First Name', required: true },
630
+ { type: 'text', name: 'lastName', label: 'Last Name', required: true },
631
+ { type: 'email', name: 'email', label: 'Email', required: true },
632
+ ] as const;
633
+
634
+ function NameFields() {
635
+ const firstName = useFieldProps<string>('firstName');
636
+ const lastName = useFieldProps<string>('lastName');
637
+
638
+ return (
639
+ <div className="name-row">
640
+ <div className="field">
641
+ <label>{firstName.field.label}</label>
642
+ <input
643
+ ref={firstName.ref}
644
+ value={firstName.value ?? ''}
645
+ onChange={(e) => firstName.onChange(e.target.value)}
646
+ onBlur={firstName.onBlur}
647
+ />
648
+ {firstName.fieldState.error && (
649
+ <span className="error">{firstName.fieldState.error.message}</span>
650
+ )}
651
+ </div>
652
+ <div className="field">
653
+ <label>{lastName.field.label}</label>
654
+ <input
655
+ ref={lastName.ref}
656
+ value={lastName.value ?? ''}
657
+ onChange={(e) => lastName.onChange(e.target.value)}
658
+ onBlur={lastName.onBlur}
659
+ />
660
+ {lastName.fieldState.error && (
661
+ <span className="error">{lastName.fieldState.error.message}</span>
662
+ )}
663
+ </div>
664
+ </div>
665
+ );
666
+ }
667
+
668
+ function MyForm() {
669
+ return (
670
+ <FormGenerator fields={fields} onSubmit={handleSubmit}>
671
+ {({ fields, buttons, isSubmitting }) => (
672
+ <div className="custom-form">
673
+ <NameFields />
674
+ {fields.email}
675
+ <button type="submit" disabled={isSubmitting}>
676
+ {isSubmitting ? 'Submitting...' : 'Submit'}
677
+ </button>
678
+ </div>
679
+ )}
680
+ </FormGenerator>
681
+ );
682
+ }
683
+ ```
684
+
685
+ ---
686
+
687
+ ## Type-Safe Forms with StrictFormGenerator
688
+
689
+ For maximum type safety, use `StrictFormGenerator` which requires a Zod schema. This ensures field names match your schema and provides fully typed form values.
690
+
691
+ ```typescript
692
+ import { StrictFormGenerator } from '@connect-soft/form-generator';
693
+ import { z } from 'zod';
694
+
695
+ const userSchema = z.object({
696
+ email: z.string().email('Invalid email'),
697
+ password: z.string().min(8, 'Password must be at least 8 characters'),
698
+ age: z.number().min(18, 'Must be 18 or older'),
699
+ });
700
+
701
+ <StrictFormGenerator
702
+ schema={userSchema}
703
+ fields={[
704
+ { type: 'email', name: 'email', label: 'Email' }, // name must be keyof schema
705
+ { type: 'password', name: 'password', label: 'Password' },
706
+ { type: 'number', name: 'age', label: 'Age' },
707
+ // { type: 'text', name: 'invalid' } // TypeScript error: 'invalid' not in schema
708
+ ]}
709
+ onSubmit={(values) => {
710
+ // values: { email: string; password: string; age: number }
711
+ console.log(values.email); // Fully typed!
712
+ }}
713
+ />
714
+ ```
715
+
716
+ ### Typed Field Helpers
717
+
718
+ Use helper functions for even stricter type checking:
719
+
720
+ ```typescript
721
+ import { StrictFormGenerator, createFieldFactory } from '@connect-soft/form-generator';
722
+ import { z } from 'zod';
723
+
724
+ const loginSchema = z.object({
725
+ email: z.string().email(),
726
+ password: z.string(),
727
+ rememberMe: z.boolean(),
728
+ });
729
+
730
+ // Create a typed field factory from schema
731
+ const defineField = createFieldFactory(loginSchema);
732
+
733
+ // Each field is validated against the schema
734
+ const fields = [
735
+ defineField({ type: 'email', name: 'email', label: 'Email' }),
736
+ defineField({ type: 'password', name: 'password', label: 'Password' }),
737
+ defineField({ type: 'checkbox', name: 'rememberMe', label: 'Remember Me' }),
738
+ ];
739
+
740
+ <StrictFormGenerator
741
+ schema={loginSchema}
742
+ fields={fields}
743
+ onSubmit={handleSubmit}
744
+ />
745
+ ```
746
+
747
+ ### StrictFormGenerator vs FormGenerator
748
+
749
+ | Feature | FormGenerator | StrictFormGenerator |
750
+ |---------|---------------|---------------------|
751
+ | Schema | No (infers from fields) | Required (Zod) |
752
+ | Field name checking | Inferred from fields | Enforced at compile-time |
753
+ | Type inference | From field definitions | From Zod schema |
754
+ | Use case | Quick prototyping | Production apps |
755
+
756
+ ### Available Helpers
757
+
758
+ | Helper | Description |
759
+ |--------|-------------|
760
+ | `createFieldFactory(schema)` | Create a field factory for a schema |
761
+ | `typedField<typeof schema>()` | Create a single typed field |
762
+ | `typedFields<typeof schema>([...])` | Create an array of typed fields |
763
+
764
+ ---
765
+
481
766
  ## TypeScript Type Inference
482
767
 
483
768
  Get full type inference from field definitions:
@@ -574,6 +859,63 @@ function MyForm() {
574
859
  | `isDirty()` | Check if form has unsaved changes |
575
860
  | `form` | Access underlying react-hook-form instance |
576
861
 
862
+ ### Watching Form Values
863
+
864
+ Detect when field values change from the parent component:
865
+
866
+ ```typescript
867
+ import { useRef, useEffect } from 'react';
868
+ import { FormGenerator, FormGeneratorRef } from '@connect-soft/form-generator';
869
+
870
+ function MyForm() {
871
+ const formRef = useRef<FormGeneratorRef>(null);
872
+
873
+ useEffect(() => {
874
+ // Watch all fields for changes
875
+ const subscription = formRef.current?.form.watch((values, { name, type }) => {
876
+ console.log('Changed field:', name);
877
+ console.log('New values:', values);
878
+ });
879
+
880
+ return () => subscription?.unsubscribe();
881
+ }, []);
882
+
883
+ return (
884
+ <FormGenerator
885
+ ref={formRef}
886
+ fields={fields}
887
+ onSubmit={handleSubmit}
888
+ />
889
+ );
890
+ }
891
+ ```
892
+
893
+ Or use `useWatch` inside a custom layout:
894
+
895
+ ```typescript
896
+ import { FormGenerator, useWatch } from '@connect-soft/form-generator';
897
+
898
+ function ValueWatcher() {
899
+ const email = useWatch({ name: 'email' }); // Watch specific field
900
+
901
+ useEffect(() => {
902
+ console.log('Email changed:', email);
903
+ }, [email]);
904
+
905
+ return null;
906
+ }
907
+
908
+ <FormGenerator fields={fields} onSubmit={handleSubmit}>
909
+ {({ fields, buttons }) => (
910
+ <>
911
+ {fields.all}
912
+ <ValueWatcher />
913
+ {buttons.submit}
914
+ </>
915
+ )}
916
+ </FormGenerator>
917
+ ```
918
+
577
919
  ---
578
920
 
579
921
  ## API Reference
@@ -595,6 +937,7 @@ function MyForm() {
595
937
  | `description` | `string` | - | Form description (available in render props) |
596
938
  | `showReset` | `boolean` | `false` | Include reset button in `buttons.reset` |
597
939
  | `resetText` | `string` | `'Reset'` | Reset button text |
940
+ | `validateTypes` | `boolean \| ValidateTypesOptions` | `false` | Runtime validation of field types |
598
941
 
599
942
  ### Field Base Properties
600
943