@connect-soft/form-generator 1.0.0-alpha → 1.1.0-alpha10

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.
Files changed (35) hide show
  1. package/README.md +1102 -28
  2. package/dist/index.js +1184 -391
  3. package/dist/index.js.map +1 -1
  4. package/dist/{index.m.js → index.mjs} +1163 -389
  5. package/dist/index.mjs.map +1 -0
  6. package/dist/types/components/form/array-field-renderer.d.ts +39 -0
  7. package/dist/types/components/form/array-field-renderer.d.ts.map +1 -0
  8. package/dist/types/components/form/create-template-fields.d.ts +3 -0
  9. package/dist/types/components/form/create-template-fields.d.ts.map +1 -0
  10. package/dist/types/components/form/field-renderer.d.ts +1 -6
  11. package/dist/types/components/form/field-renderer.d.ts.map +1 -1
  12. package/dist/types/components/form/fields-context.d.ts +26 -0
  13. package/dist/types/components/form/fields-context.d.ts.map +1 -0
  14. package/dist/types/components/form/form-generator-typed.d.ts +47 -0
  15. package/dist/types/components/form/form-generator-typed.d.ts.map +1 -0
  16. package/dist/types/components/form/form-generator.d.ts +43 -6
  17. package/dist/types/components/form/form-generator.d.ts.map +1 -1
  18. package/dist/types/components/form/form-utils.d.ts +47 -0
  19. package/dist/types/components/form/form-utils.d.ts.map +1 -0
  20. package/dist/types/components/form/index.d.ts +5 -3
  21. package/dist/types/components/form/index.d.ts.map +1 -1
  22. package/dist/types/index.d.ts +13 -6
  23. package/dist/types/index.d.ts.map +1 -1
  24. package/dist/types/lib/field-registry.d.ts +19 -59
  25. package/dist/types/lib/field-registry.d.ts.map +1 -1
  26. package/dist/types/lib/field-types.d.ts +127 -26
  27. package/dist/types/lib/field-types.d.ts.map +1 -1
  28. package/dist/types/lib/index.d.ts +3 -4
  29. package/dist/types/lib/index.d.ts.map +1 -1
  30. package/dist/types/lib/template-types.d.ts +55 -0
  31. package/dist/types/lib/template-types.d.ts.map +1 -0
  32. package/package.json +11 -25
  33. package/MIGRATION.md +0 -503
  34. package/dist/index.m.js.map +0 -1
  35. package/dist/styles/globals.css +0 -2
package/README.md CHANGED
@@ -1,61 +1,1135 @@
1
1
  # @connect-soft/form-generator
2
2
 
3
- > Type-safe form generator with Radix UI, Tailwind CSS, and react-hook-form
3
+ > Headless, type-safe form generator with react-hook-form and Zod validation
4
4
 
5
- **v2.0.0** - Complete rewrite with Radix UI + Tailwind CSS
5
+ [![Version](https://img.shields.io/npm/v/@connect-soft/form-generator)](https://www.npmjs.com/package/@connect-soft/form-generator)
6
+ [![License](https://img.shields.io/npm/l/@connect-soft/form-generator)](./LICENSE)
7
+
8
+ ---
6
9
 
7
10
  ## Features
8
11
 
9
- - Modern UI with Radix primitives + Tailwind CSS
10
- - 📦 50-60% smaller bundle (<80 KB gzipped)
11
- - 🎯 Full TypeScript type inference
12
- - 🔧 10+ field types with extensible API
13
- - Built-in accessibility
14
- - 🎨 Easy Tailwind customization
15
- - 🚀 Zero runtime CSS overhead
16
- - 🔄 v1→v2 migration adapter included
12
+ - **Headless**: Bring your own UI components (Radix, MUI, Chakra, or plain HTML)
13
+ - **Type-Safe**: Full TypeScript inference for form values and field types
14
+ - **Field Type Checking**: Compile-time validation of `field.type` with autocomplete
15
+ - **Extensible Types**: Add custom field types via module augmentation
16
+ - **Imperative API**: Control form via ref (`setValues`, `reset`, `submit`, etc.)
17
+ - **Flexible**: Register custom field components with a simple API
18
+ - **Validation**: Built-in Zod validation support
19
+ - **Array Fields**: Repeatable field groups with `useFieldArray` integration
20
+ - **Custom Layouts**: Full control over form layout via render props
21
+ - **Lightweight**: No UI dependencies, minimal footprint
22
+ - **HTML Fallbacks**: Works out of the box with native HTML inputs
23
+
24
+ ---
17
25
 
18
26
  ## Installation
19
27
 
20
28
  ```bash
21
- pnpm add @connect-soft/form-generator react-hook-form zod
29
+ npm install @connect-soft/form-generator
22
30
  ```
23
31
 
32
+ ### Peer Dependencies
33
+
34
+ - `react` ^19.0.0
35
+ - `react-dom` ^19.0.0
36
+ - `zod` ^4.0.0
37
+
38
+ ---
39
+
24
40
  ## Quick Start
25
41
 
42
+ The library works immediately with HTML fallback components:
43
+
26
44
  ```typescript
27
45
  import { FormGenerator } from '@connect-soft/form-generator';
28
46
 
29
47
  const fields = [
48
+ { type: 'text', name: 'email', label: 'Email', required: true },
49
+ { type: 'number', name: 'age', label: 'Age', min: 18, max: 120 },
50
+ { type: 'select', name: 'country', label: 'Country', options: [
51
+ { label: 'United States', value: 'us' },
52
+ { label: 'Germany', value: 'de' },
53
+ ]},
54
+ { type: 'checkbox', name: 'subscribe', label: 'Subscribe to newsletter' },
55
+ ] as const;
56
+
57
+ function MyForm() {
58
+ return (
59
+ <FormGenerator
60
+ fields={fields}
61
+ onSubmit={(values) => {
62
+ console.log(values); // Fully typed!
63
+ }}
64
+ />
65
+ );
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Registering Custom Components
72
+
73
+ Register your own UI components to replace the HTML fallbacks. Field components are fully responsible for rendering their own label, description, and error messages:
74
+
75
+ ```typescript
76
+ import { registerFields, registerFormComponent } from '@connect-soft/form-generator';
77
+ import { Input } from './ui/input';
78
+ import { Label } from './ui/label';
79
+ import { Checkbox } from './ui/checkbox';
80
+ import { Button } from './ui/button';
81
+
82
+ // Register field components - they handle their own rendering
83
+ registerFields({
84
+ text: ({ field, formField, fieldState }) => (
85
+ <div className="form-field">
86
+ {field.label && <Label>{field.label}{field.required && ' *'}</Label>}
87
+ <Input
88
+ {...formField}
89
+ type={field.fieldType || 'text'}
90
+ placeholder={field.placeholder}
91
+ disabled={field.disabled}
92
+ />
93
+ {field.description && <p className="text-sm text-muted">{field.description}</p>}
94
+ {fieldState.error && <span className="text-red-500 text-sm">{fieldState.error.message}</span>}
95
+ </div>
96
+ ),
97
+ number: ({ field, formField, fieldState }) => (
98
+ <div className="form-field">
99
+ {field.label && <Label>{field.label}</Label>}
100
+ <Input
101
+ {...formField}
102
+ type="number"
103
+ min={field.min}
104
+ max={field.max}
105
+ />
106
+ {fieldState.error && <span className="text-red-500 text-sm">{fieldState.error.message}</span>}
107
+ </div>
108
+ ),
109
+ checkbox: ({ field, formField }) => (
110
+ <div className="flex items-center gap-2">
111
+ <Checkbox
112
+ checked={formField.value}
113
+ onCheckedChange={formField.onChange}
114
+ disabled={field.disabled}
115
+ />
116
+ {field.label && <Label>{field.label}</Label>}
117
+ </div>
118
+ ),
119
+ });
120
+
121
+ // Register the submit button component
122
+ registerFormComponent('SubmitButton', Button);
123
+
124
+ // Optional: Register field wrappers for custom styling
125
+ registerFormComponent('FieldWrapper', ({ children, name, type, className }) => (
126
+ <div className={`field-wrapper field-${type}`} data-field={name}>
127
+ {children}
128
+ </div>
129
+ ));
130
+
131
+ registerFormComponent('FieldsWrapper', ({ children }) => (
132
+ <div className="form-fields">
133
+ {children}
134
+ </div>
135
+ ));
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Field Wrappers
141
+
142
+ Customize how fields are wrapped without modifying individual field components:
143
+
144
+ ### FieldWrapper
145
+
146
+ Wraps each individual field. Receives the field's `name`, `type`, and `className`:
147
+
148
+ ```typescript
149
+ registerFormComponent('FieldWrapper', ({ children, name, type, className }) => (
150
+ <div className={`form-group ${className || ''}`} data-field-type={type}>
151
+ {children}
152
+ </div>
153
+ ));
154
+ ```
155
+
156
+ ### FieldsWrapper
157
+
158
+ Wraps all fields together (excludes the submit button). Useful for grid layouts:
159
+
160
+ ```typescript
161
+ registerFormComponent('FieldsWrapper', ({ children, className }) => (
162
+ <div className={`grid grid-cols-2 gap-4 ${className || ''}`}>
163
+ {children}
164
+ </div>
165
+ ));
166
+ ```
167
+
168
+ Both default to `React.Fragment`, adding zero DOM overhead when not customized.
169
+
170
+ ---
171
+
172
+ ## Field Types
173
+
174
+ Built-in HTML fallback types:
175
+
176
+ | Type | Description | Value Type |
177
+ |------|-------------|------------|
178
+ | `text` | Text input | `string` |
179
+ | `email` | Email input | `string` |
180
+ | `password` | Password input | `string` |
181
+ | `number` | Number input with min/max | `number` |
182
+ | `textarea` | Multi-line text | `string` |
183
+ | `checkbox` | Checkbox | `boolean` |
184
+ | `select` | Dropdown select | `string` |
185
+ | `radio` | Radio button group | `string` |
186
+ | `date` | Date input | `Date` |
187
+ | `time` | Time input | `string` |
188
+ | `file` | File input | `File` |
189
+ | `hidden` | Hidden input | `string` |
190
+
191
+ ### Adding Custom Field Types (TypeScript)
192
+
193
+ Extend the `FieldTypeRegistry` interface to add type checking for custom fields:
194
+
195
+ ```typescript
196
+ // types/form-generator.d.ts
197
+ import { CreateFieldType } from '@connect-soft/form-generator';
198
+
199
+ declare module '@connect-soft/form-generator' {
200
+ interface FieldTypeRegistry {
201
+ // Add your custom field types
202
+ 'color-picker': CreateFieldType<'color-picker', string, {
203
+ swatches?: string[];
204
+ showAlpha?: boolean;
205
+ }>;
206
+ 'rich-text': CreateFieldType<'rich-text', string, {
207
+ toolbar?: ('bold' | 'italic' | 'link')[];
208
+ maxLength?: number;
209
+ }>;
210
+ }
211
+ }
212
+ ```
213
+
214
+ Now TypeScript will recognize your custom field types:
215
+
216
+ ```typescript
217
+ const fields = [
218
+ { type: 'color-picker', name: 'theme', swatches: ['#fff', '#000'] }, // ✅ Valid
219
+ { type: 'unknown-type', name: 'test' }, // ❌ Type error
220
+ ] as const;
221
+ ```
222
+
223
+ Then register the component for your custom field:
224
+
225
+ ```typescript
226
+ import { registerField } from '@connect-soft/form-generator';
227
+ import { ColorPicker } from './components/ColorPicker';
228
+
229
+ // Type-safe: 'color-picker' must exist in FieldTypeRegistry
230
+ registerField('color-picker', ({ field, formField }) => (
231
+ <ColorPicker
232
+ value={formField.value}
233
+ onChange={formField.onChange}
234
+ swatches={field.swatches}
235
+ showAlpha={field.showAlpha}
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 });
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Custom Validation
308
+
309
+ Use Zod for field-level or form-level validation:
310
+
311
+ ```typescript
312
+ import { z } from 'zod';
313
+
314
+ // Field-level validation
315
+ const fields = [
316
+ {
317
+ type: 'text',
318
+ name: 'username',
319
+ label: 'Username',
320
+ validation: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
321
+ },
30
322
  {
31
323
  type: 'text',
32
324
  name: 'email',
33
325
  label: 'Email',
34
- fieldType: 'email',
35
- required: true,
326
+ validation: z.string().email(),
36
327
  },
328
+ ] as const;
329
+
330
+ // Or use a full schema for type inference
331
+ const schema = z.object({
332
+ username: z.string().min(3),
333
+ email: z.string().email(),
334
+ });
335
+
336
+ <FormGenerator
337
+ fields={fields}
338
+ schema={schema}
339
+ onSubmit={(values) => {
340
+ // values is inferred from schema
341
+ }}
342
+ />
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Array Fields
348
+
349
+ Create repeatable field groups with `useFieldArray` integration:
350
+
351
+ ```typescript
352
+ const fields = [
353
+ { type: 'text', name: 'name', label: 'Name' },
37
354
  {
38
- type: 'number',
39
- name: 'age',
40
- label: 'Age',
41
- min: 18,
355
+ type: 'array',
356
+ name: 'contacts',
357
+ label: 'Contacts',
358
+ fields: [
359
+ { type: 'text', name: 'email', label: 'Email' },
360
+ { type: 'text', name: 'phone', label: 'Phone' },
361
+ ],
362
+ minItems: 1,
363
+ maxItems: 5,
42
364
  },
43
365
  ] as const;
366
+ ```
44
367
 
45
- export const MyForm = () => (
46
- <FormGenerator
47
- fields={fields}
48
- onSubmit={(values) => {
49
- // values is fully typed!
50
- console.log(values);
51
- }}
52
- />
53
- );
368
+ ### Default Array Rendering
369
+
370
+ Array fields render automatically with add/remove functionality:
371
+
372
+ ```typescript
373
+ <FormGenerator
374
+ fields={fields}
375
+ onSubmit={(values) => {
376
+ console.log(values.contacts); // Array<{ email: string, phone: string }>
377
+ }}
378
+ />
379
+ ```
380
+
381
+ ### Custom Array Rendering with useArrayField
382
+
383
+ For full control, use the `useArrayField` hook in a custom layout:
384
+
385
+ ```typescript
386
+ import { FormGenerator, useArrayField } from '@connect-soft/form-generator';
387
+
388
+ <FormGenerator fields={fields} onSubmit={handleSubmit}>
389
+ {({ fields, arrays, buttons }) => {
390
+ const contacts = useArrayField(arrays.contacts.field);
391
+
392
+ return (
393
+ <div>
394
+ {fields.name}
395
+
396
+ <h3>Contacts</h3>
397
+ {contacts.items.map(({ id, index, remove, fields: itemFields }) => (
398
+ <div key={id} className="contact-row">
399
+ {itemFields.email}
400
+ {itemFields.phone}
401
+ {contacts.canRemove && (
402
+ <button type="button" onClick={remove}>Remove</button>
403
+ )}
404
+ </div>
405
+ ))}
406
+
407
+ {contacts.canAppend && (
408
+ <button type="button" onClick={contacts.append}>
409
+ Add Contact
410
+ </button>
411
+ )}
412
+
413
+ {buttons.submit}
414
+ </div>
415
+ );
416
+ }}
417
+ </FormGenerator>
418
+ ```
419
+
420
+ ### useArrayField Return Values
421
+
422
+ | Property | Type | Description |
423
+ |----------|------|-------------|
424
+ | `items` | `Array<{ id, index }>` | Array items with unique ids |
425
+ | `append` | `() => void` | Add new empty item |
426
+ | `appendWith` | `(values) => void` | Add item with values |
427
+ | `prepend` | `() => void` | Add item at beginning |
428
+ | `remove` | `(index) => void` | Remove item at index |
429
+ | `move` | `(from, to) => void` | Move item |
430
+ | `swap` | `(a, b) => void` | Swap two items |
431
+ | `insert` | `(index, values?) => void` | Insert at index |
432
+ | `canAppend` | `boolean` | Can add more items (respects maxItems) |
433
+ | `canRemove` | `boolean` | Can remove items (respects minItems) |
434
+ | `renderField` | `(index, name) => ReactElement` | Render single field |
435
+ | `renderItem` | `(index) => Record<string, ReactElement>` | Render all fields for item |
436
+
437
+ ---
438
+
439
+ ## Custom Layouts
440
+
441
+ For full control over form layout, pass a render function as `children`:
442
+
443
+ ```typescript
444
+ <FormGenerator
445
+ fields={[
446
+ { type: 'text', name: 'email', label: 'Email' },
447
+ { type: 'password', name: 'password', label: 'Password' },
448
+ ] as const}
449
+ title="Login"
450
+ onSubmit={handleSubmit}
451
+ >
452
+ {({ fields, buttons, title }) => (
453
+ <div className="login-form">
454
+ <h1>{title}</h1>
455
+ <div className="field-row">{fields.email}</div>
456
+ <div className="field-row">{fields.password}</div>
457
+ <div className="actions">{buttons.submit}</div>
458
+ </div>
459
+ )}
460
+ </FormGenerator>
461
+ ```
462
+
463
+ ### Render Props API
464
+
465
+ The render function receives:
466
+
467
+ | Property | Type | Description |
468
+ |----------|------|-------------|
469
+ | `fields` | `TemplateFields` | Pre-rendered fields (see below) |
470
+ | `arrays` | `Record<string, TemplateArrayField>` | Array field definitions (use with `useArrayField`) |
471
+ | `buttons` | `{ submit, reset? }` | Pre-rendered buttons |
472
+ | `title` | `string` | Form title prop |
473
+ | `description` | `string` | Form description prop |
474
+ | `form` | `UseFormReturn` | react-hook-form instance |
475
+ | `isSubmitting` | `boolean` | Form submission state |
476
+ | `isValid` | `boolean` | Form validity state |
477
+ | `isDirty` | `boolean` | Form dirty state |
478
+ | `renderField` | `function` | Manual field renderer |
479
+ | `FieldWrapper` | `ComponentType` | Registered FieldWrapper component |
480
+ | `FieldsWrapper` | `ComponentType` | Registered FieldsWrapper component |
481
+
482
+ ### Fields Object
483
+
484
+ Access fields by name or use helper methods:
485
+
486
+ ```typescript
487
+ {({ fields }) => (
488
+ <div>
489
+ {/* Access individual fields */}
490
+ {fields.email}
491
+ {fields.password}
492
+
493
+ {/* Render all fields */}
494
+ {fields.all}
495
+
496
+ {/* Render only fields not yet accessed */}
497
+ {fields.remaining}
498
+
499
+ {/* Check if field exists */}
500
+ {fields.has('email') && fields.email}
501
+
502
+ {/* Get all field names */}
503
+ {fields.names.map(name => <div key={name}>{fields[name]}</div>)}
504
+
505
+ {/* Render specific fields */}
506
+ {fields.render('email', 'password')}
507
+ </div>
508
+ )}
509
+ ```
510
+
511
+ ### Mixed Layout Example
512
+
513
+ Highlight specific fields while rendering the rest normally:
514
+
515
+ ```typescript
516
+ <FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
517
+ {({ fields, buttons }) => (
518
+ <div>
519
+ <div className="highlighted">{fields.email}</div>
520
+ <div className="other-fields">{fields.remaining}</div>
521
+ {buttons.submit}
522
+ </div>
523
+ )}
524
+ </FormGenerator>
525
+ ```
526
+
527
+ ### Form State Access
528
+
529
+ Use form state for conditional rendering:
530
+
531
+ ```typescript
532
+ <FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
533
+ {({ fields, buttons, isSubmitting, isValid, isDirty }) => (
534
+ <div>
535
+ {fields.all}
536
+ <button type="submit" disabled={isSubmitting || !isValid}>
537
+ {isSubmitting ? 'Saving...' : 'Submit'}
538
+ </button>
539
+ {isDirty && <span>You have unsaved changes</span>}
540
+ </div>
541
+ )}
542
+ </FormGenerator>
543
+ ```
544
+
545
+ ---
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
+ | Constraint detection | No | Yes (automatic) |
755
+ | Use case | Quick prototyping | Production apps |
756
+
757
+ ### Automatic Schema Constraint Detection
758
+
759
+ `StrictFormGenerator` automatically extracts constraints from your Zod schema and propagates them to field components. This means you don't need to duplicate constraints in both your schema and field definitions.
760
+
761
+ #### Supported Constraints
762
+
763
+ **Number fields** (`z.number()`):
764
+ | Zod Method | Field Property | Example |
765
+ |------------|----------------|---------|
766
+ | `.min(n)` | `min` | `z.number().min(0)` → `{ min: 0 }` |
767
+ | `.max(n)` | `max` | `z.number().max(100)` → `{ max: 100 }` |
768
+ | `.int()` | `step: 1` | `z.number().int()` → `{ step: 1 }` |
769
+ | `.multipleOf(n)` | `step` | `z.number().multipleOf(0.01)` → `{ step: 0.01 }` |
770
+ | `.positive()` | `min` | `z.number().positive()` → `{ min: 0 }` (exclusive) |
771
+ | `.nonnegative()` | `min: 0` | `z.number().nonnegative()` → `{ min: 0 }` |
772
+
773
+ **String fields** (`z.string()`):
774
+ | Zod Method | Field Property | Example |
775
+ |------------|----------------|---------|
776
+ | `.min(n)` | `minLength` | `z.string().min(3)` → `{ minLength: 3 }` |
777
+ | `.max(n)` | `maxLength` | `z.string().max(100)` → `{ maxLength: 100 }` |
778
+ | `.length(n)` | `minLength` + `maxLength` | `z.string().length(6)` → `{ minLength: 6, maxLength: 6 }` |
779
+ | `.regex(pattern)` | `pattern` | `z.string().regex(/^[A-Z]+$/)` → `{ pattern: '^[A-Z]+$' }` |
780
+
781
+ **Date fields** (`z.date()`):
782
+ | Zod Method | Field Property | Example |
783
+ |------------|----------------|---------|
784
+ | `.min(date)` | `min` (ISO string) | `z.date().min(new Date('2020-01-01'))` → `{ min: '2020-01-01' }` |
785
+ | `.max(date)` | `max` (ISO string) | `z.date().max(new Date('2030-12-31'))` → `{ max: '2030-12-31' }` |
786
+
787
+ #### Example
788
+
789
+ ```typescript
790
+ import { StrictFormGenerator } from '@connect-soft/form-generator';
791
+ import { z } from 'zod';
792
+
793
+ const userSchema = z.object({
794
+ username: z.string().min(3).max(20).regex(/^[a-z0-9_]+$/),
795
+ age: z.number().int().min(18).max(120),
796
+ price: z.number().multipleOf(0.01).min(0),
797
+ birthDate: z.date().min(new Date('1900-01-01')).max(new Date()),
798
+ });
799
+
800
+ // No need to specify min/max/minLength/maxLength in fields!
801
+ // They are automatically extracted from the schema
802
+ <StrictFormGenerator
803
+ schema={userSchema}
804
+ fields={[
805
+ { type: 'text', name: 'username', label: 'Username' },
806
+ { type: 'number', name: 'age', label: 'Age' },
807
+ { type: 'number', name: 'price', label: 'Price' },
808
+ { type: 'date', name: 'birthDate', label: 'Birth Date' },
809
+ ]}
810
+ onSubmit={handleSubmit}
811
+ />
812
+
813
+ // Field components receive:
814
+ // username: { minLength: 3, maxLength: 20, pattern: '^[a-z0-9_]+$', required: true }
815
+ // age: { min: 18, max: 120, step: 1, required: true }
816
+ // price: { min: 0, step: 0.01, required: true }
817
+ // birthDate: { min: '1900-01-01', max: '2026-02-02', required: true }
818
+ ```
819
+
820
+ #### Using Constraints in Field Components
821
+
822
+ Your registered field components can use these constraints directly:
823
+
824
+ ```typescript
825
+ registerField('number', ({ field, formField, fieldState }) => (
826
+ <div>
827
+ <label>{field.label}</label>
828
+ <input
829
+ type="number"
830
+ {...formField}
831
+ min={field.min}
832
+ max={field.max}
833
+ step={field.step}
834
+ />
835
+ {fieldState.error && <span>{fieldState.error.message}</span>}
836
+ </div>
837
+ ));
838
+
839
+ registerField('text', ({ field, formField, fieldState }) => (
840
+ <div>
841
+ <label>{field.label}</label>
842
+ <input
843
+ type="text"
844
+ {...formField}
845
+ minLength={field.minLength}
846
+ maxLength={field.maxLength}
847
+ pattern={field.pattern}
848
+ />
849
+ {fieldState.error && <span>{fieldState.error.message}</span>}
850
+ </div>
851
+ ));
852
+ ```
853
+
854
+ #### Manual Constraint Merging
855
+
856
+ You can also manually merge constraints using the utility functions:
857
+
858
+ ```typescript
859
+ import { mergeSchemaConstraints, analyzeSchema } from '@connect-soft/form-generator';
860
+
861
+ const schema = z.object({
862
+ age: z.number().min(0).max(120),
863
+ name: z.string().min(1).max(100),
864
+ });
865
+
866
+ // Analyze schema to get field info
867
+ const fieldInfo = analyzeSchema(schema);
868
+ // => [
869
+ // { name: 'age', type: 'number', required: true, min: 0, max: 120 },
870
+ // { name: 'name', type: 'string', required: true, minLength: 1, maxLength: 100 }
871
+ // ]
872
+
873
+ // Or merge constraints into existing fields
874
+ const fields = [
875
+ { type: 'number', name: 'age', label: 'Age' },
876
+ { type: 'text', name: 'name', label: 'Name' },
877
+ ];
878
+
879
+ const fieldsWithConstraints = mergeSchemaConstraints(schema, fields);
880
+ // => [
881
+ // { type: 'number', name: 'age', label: 'Age', required: true, min: 0, max: 120 },
882
+ // { type: 'text', name: 'name', label: 'Name', required: true, minLength: 1, maxLength: 100 }
883
+ // ]
54
884
  ```
55
885
 
56
- ## Documentation
886
+ ### Available Helpers
887
+
888
+ | Helper | Description |
889
+ |--------|-------------|
890
+ | `createFieldFactory(schema)` | Create a field factory for a schema |
891
+ | `typedField<typeof schema>()` | Create a single typed field |
892
+ | `typedFields<typeof schema>([...])` | Create an array of typed fields |
893
+
894
+ ---
895
+
896
+ ## TypeScript Type Inference
897
+
898
+ Get full type inference from field definitions:
899
+
900
+ ```typescript
901
+ const fields = [
902
+ { type: 'text', name: 'email', required: true },
903
+ { type: 'number', name: 'age', required: true },
904
+ { type: 'checkbox', name: 'terms' },
905
+ ] as const;
906
+
907
+ <FormGenerator
908
+ fields={fields}
909
+ onSubmit={(values) => {
910
+ values.email; // string
911
+ values.age; // number
912
+ values.terms; // boolean | undefined
913
+ }}
914
+ />
915
+ ```
916
+
917
+ Or provide an explicit Zod schema:
918
+
919
+ ```typescript
920
+ const schema = z.object({
921
+ email: z.string().email(),
922
+ age: z.number().min(18),
923
+ terms: z.boolean(),
924
+ });
925
+
926
+ <FormGenerator
927
+ fields={fields}
928
+ schema={schema}
929
+ onSubmit={(values) => {
930
+ // values: { email: string; age: number; terms: boolean }
931
+ }}
932
+ />
933
+ ```
934
+
935
+ ---
936
+
937
+ ## Imperative API (Ref)
938
+
939
+ Access form methods programmatically using a ref:
940
+
941
+ ```typescript
942
+ import { useRef } from 'react';
943
+ import { FormGenerator, FormGeneratorRef } from '@connect-soft/form-generator';
944
+
945
+ function MyForm() {
946
+ const formRef = useRef<FormGeneratorRef>(null);
947
+
948
+ const handleExternalSubmit = async () => {
949
+ await formRef.current?.submit();
950
+ };
951
+
952
+ const handleReset = () => {
953
+ formRef.current?.reset();
954
+ };
955
+
956
+ const handleSetValues = () => {
957
+ formRef.current?.setValues({
958
+ email: 'test@example.com',
959
+ age: 25,
960
+ });
961
+ };
962
+
963
+ return (
964
+ <>
965
+ <FormGenerator
966
+ ref={formRef}
967
+ fields={fields}
968
+ onSubmit={(values) => console.log(values)}
969
+ />
970
+ <button type="button" onClick={handleExternalSubmit}>Submit Externally</button>
971
+ <button type="button" onClick={handleReset}>Reset Form</button>
972
+ <button type="button" onClick={handleSetValues}>Set Values</button>
973
+ </>
974
+ );
975
+ }
976
+ ```
977
+
978
+ ### Available Ref Methods
979
+
980
+ | Method | Description |
981
+ |--------|-------------|
982
+ | `setValues(values)` | Set form values (partial update) |
983
+ | `getValues()` | Get current form values |
984
+ | `reset(values?)` | Reset to default or provided values |
985
+ | `submit()` | Programmatically submit the form |
986
+ | `clearErrors()` | Clear all validation errors |
987
+ | `setError(name, error)` | Set error for a specific field |
988
+ | `isValid()` | Check if form passes validation |
989
+ | `isDirty()` | Check if form has unsaved changes |
990
+ | `form` | Access underlying react-hook-form instance |
991
+
992
+ ### Watching Form Values
993
+
994
+ Detect when field values change from the parent component:
995
+
996
+ ```typescript
997
+ import { useRef, useEffect } from 'react';
998
+ import { FormGenerator, FormGeneratorRef } from '@connect-soft/form-generator';
999
+
1000
+ function MyForm() {
1001
+ const formRef = useRef<FormGeneratorRef>(null);
1002
+
1003
+ useEffect(() => {
1004
+ // Watch all fields for changes
1005
+ const subscription = formRef.current?.form.watch((values, { name, type }) => {
1006
+ console.log('Changed field:', name);
1007
+ console.log('New values:', values);
1008
+ });
1009
+
1010
+ return () => subscription?.unsubscribe();
1011
+ }, []);
1012
+
1013
+ return (
1014
+ <FormGenerator
1015
+ ref={formRef}
1016
+ fields={fields}
1017
+ onSubmit={handleSubmit}
1018
+ />
1019
+ );
1020
+ }
1021
+ ```
1022
+
1023
+ Or use `useWatch` inside a custom layout:
1024
+
1025
+ ```typescript
1026
+ import { FormGenerator, useWatch } from '@connect-soft/form-generator';
1027
+
1028
+ function ValueWatcher() {
1029
+ const email = useWatch({ name: 'email' }); // Watch specific field
1030
+
1031
+ useEffect(() => {
1032
+ console.log('Email changed:', email);
1033
+ }, [email]);
1034
+
1035
+ return null;
1036
+ }
1037
+
1038
+ <FormGenerator fields={fields} onSubmit={handleSubmit}>
1039
+ {({ fields, buttons }) => (
1040
+ <>
1041
+ {fields.all}
1042
+ <ValueWatcher />
1043
+ {buttons.submit}
1044
+ </>
1045
+ )}
1046
+ </FormGenerator>
1047
+ ```
1048
+
1049
+ ---
1050
+
1051
+ ## API Reference
1052
+
1053
+ ### FormGenerator Props
1054
+
1055
+ | Prop | Type | Default | Description |
1056
+ |------|------|---------|-------------|
1057
+ | `fields` | `FormItem[]` | **required** | Array of field definitions |
1058
+ | `onSubmit` | `(values) => void \| Promise<void>` | **required** | Form submission handler |
1059
+ | `schema` | `ZodType` | - | Optional Zod schema for validation |
1060
+ | `defaultValues` | `object` | `{}` | Initial form values |
1061
+ | `className` | `string` | - | CSS class for form element |
1062
+ | `submitText` | `string` | `'Submit'` | Submit button text |
1063
+ | `disabled` | `boolean` | `false` | Disable entire form |
1064
+ | `mode` | `'onChange' \| 'onBlur' \| 'onSubmit' \| 'onTouched' \| 'all'` | `'onChange'` | Validation trigger mode |
1065
+ | `children` | `TemplateRenderFn` | - | Render function for custom layout |
1066
+ | `title` | `string` | - | Form title (available in render props) |
1067
+ | `description` | `string` | - | Form description (available in render props) |
1068
+ | `showReset` | `boolean` | `false` | Include reset button in `buttons.reset` |
1069
+ | `resetText` | `string` | `'Reset'` | Reset button text |
1070
+ | `validateTypes` | `boolean \| ValidateTypesOptions` | `false` | Runtime validation of field types |
1071
+
1072
+ ### Field Base Properties
1073
+
1074
+ | Property | Type | Description |
1075
+ |----------|------|-------------|
1076
+ | `type` | `string` | Field type (text, number, select, etc.) |
1077
+ | `name` | `string` | Field name (must be unique) |
1078
+ | `label` | `string` | Field label |
1079
+ | `description` | `string` | Helper text below field |
1080
+ | `required` | `boolean` | Mark field as required |
1081
+ | `disabled` | `boolean` | Disable field |
1082
+ | `hidden` | `boolean` | Hide field |
1083
+ | `defaultValue` | `any` | Default field value |
1084
+ | `validation` | `ZodType` | Zod validation schema |
1085
+ | `className` | `string` | CSS class for field wrapper |
1086
+
1087
+ ### Field Type-Specific Properties
1088
+
1089
+ **Text fields** (`text`, `email`, `password`, `tel`, `url`, `search`):
1090
+ | Property | Type | Description |
1091
+ |----------|------|-------------|
1092
+ | `placeholder` | `string` | Placeholder text |
1093
+ | `minLength` | `number` | Minimum character length |
1094
+ | `maxLength` | `number` | Maximum character length |
1095
+ | `pattern` | `string` | HTML5 validation pattern |
1096
+
1097
+ **Number fields** (`number`, `range`):
1098
+ | Property | Type | Description |
1099
+ |----------|------|-------------|
1100
+ | `placeholder` | `string` | Placeholder text |
1101
+ | `min` | `number` | Minimum value |
1102
+ | `max` | `number` | Maximum value |
1103
+ | `step` | `number` | Step increment |
1104
+
1105
+ **Date fields** (`date`, `datetime`, `datetime-local`):
1106
+ | Property | Type | Description |
1107
+ |----------|------|-------------|
1108
+ | `min` | `string` | Minimum date (ISO format: YYYY-MM-DD) |
1109
+ | `max` | `string` | Maximum date (ISO format: YYYY-MM-DD) |
1110
+
1111
+ ### Schema Analysis Utilities
1112
+
1113
+ | Function | Description |
1114
+ |----------|-------------|
1115
+ | `analyzeSchema(schema)` | Extract detailed field info from Zod schema |
1116
+ | `mergeSchemaConstraints(schema, fields)` | Merge schema constraints into field definitions |
1117
+ | `mergeSchemaRequirements(schema, fields)` | Merge only required status (legacy) |
1118
+ | `getNumberConstraints(schema)` | Extract min/max/step from number schema |
1119
+ | `getStringConstraints(schema)` | Extract minLength/maxLength/pattern from string schema |
1120
+ | `getDateConstraints(schema)` | Extract min/max dates from date schema |
1121
+ | `isSchemaRequired(schema)` | Check if schema field is required |
1122
+ | `unwrapSchema(schema)` | Unwrap optional/nullable wrappers |
1123
+ | `getSchemaTypeName(schema)` | Get base type name (string, number, etc.) |
1124
+
1125
+ ---
1126
+
1127
+ ## Links
1128
+
1129
+ - [GitLab Repository](https://gitlab.com/connect-soft/components/form-generator)
1130
+ - [Issues](https://gitlab.com/connect-soft/components/form-generator/issues)
57
1131
 
58
- See [main README](../../README.md) for full documentation.
1132
+ ---
59
1133
 
60
1134
  ## License
61
1135