@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 +344 -1
- package/dist/index.js +478 -70
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +468 -72
- package/dist/index.mjs.map +1 -1
- package/dist/types/components/form/fields-context.d.ts +26 -0
- package/dist/types/components/form/fields-context.d.ts.map +1 -0
- package/dist/types/components/form/form-generator-typed.d.ts +49 -0
- package/dist/types/components/form/form-generator-typed.d.ts.map +1 -0
- package/dist/types/components/form/form-generator.d.ts +12 -20
- package/dist/types/components/form/form-generator.d.ts.map +1 -1
- package/dist/types/components/form/form-utils.d.ts +4 -2
- package/dist/types/components/form/form-utils.d.ts.map +1 -1
- package/dist/types/components/form/index.d.ts +1 -1
- package/dist/types/components/form/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/field-registry.d.ts +10 -10
- package/dist/types/lib/field-registry.d.ts.map +1 -1
- package/dist/types/lib/field-types.d.ts +25 -0
- package/dist/types/lib/field-types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -220,12 +220,13 @@ const fields = [
|
|
|
220
220
|
] as const;
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
|
|
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
|
|