@connect-soft/form-generator 1.1.0-alpha2 → 1.1.0-alpha3

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
@@ -1,61 +1,407 @@
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
+ - **Layouts**: Support for sections and multi-column layouts
20
+ - **Lightweight**: No UI dependencies, minimal footprint
21
+ - **HTML Fallbacks**: Works out of the box with native HTML inputs
22
+
23
+ ---
17
24
 
18
25
  ## Installation
19
26
 
20
27
  ```bash
21
- pnpm add @connect-soft/form-generator react-hook-form zod
28
+ npm install @connect-soft/form-generator
22
29
  ```
23
30
 
31
+ ### Peer Dependencies
32
+
33
+ - `react` ^19.0.0
34
+ - `react-dom` ^19.0.0
35
+ - `zod` ^4.0.0
36
+
37
+ ---
38
+
24
39
  ## Quick Start
25
40
 
41
+ The library works immediately with HTML fallback components:
42
+
26
43
  ```typescript
27
44
  import { FormGenerator } from '@connect-soft/form-generator';
28
45
 
29
46
  const fields = [
47
+ { type: 'text', name: 'email', label: 'Email', required: true },
48
+ { type: 'number', name: 'age', label: 'Age', min: 18, max: 120 },
49
+ { type: 'select', name: 'country', label: 'Country', options: [
50
+ { label: 'United States', value: 'us' },
51
+ { label: 'Germany', value: 'de' },
52
+ ]},
53
+ { type: 'checkbox', name: 'subscribe', label: 'Subscribe to newsletter' },
54
+ ] as const;
55
+
56
+ function MyForm() {
57
+ return (
58
+ <FormGenerator
59
+ fields={fields}
60
+ onSubmit={(values) => {
61
+ console.log(values); // Fully typed!
62
+ }}
63
+ />
64
+ );
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Registering Custom Components
71
+
72
+ Register your own UI components to replace the HTML fallbacks:
73
+
74
+ ```typescript
75
+ import { registerFields, registerFormComponents } from '@connect-soft/form-generator';
76
+ import { Input } from './ui/input';
77
+ import { Label } from './ui/label';
78
+ import { Checkbox } from './ui/checkbox';
79
+ import { Button } from './ui/button';
80
+
81
+ // Register field components
82
+ registerFields({
83
+ text: ({ field, formField }) => (
84
+ <Input
85
+ {...formField}
86
+ type={field.fieldType || 'text'}
87
+ placeholder={field.placeholder}
88
+ disabled={field.disabled}
89
+ />
90
+ ),
91
+ number: ({ field, formField }) => (
92
+ <Input
93
+ {...formField}
94
+ type="number"
95
+ min={field.min}
96
+ max={field.max}
97
+ />
98
+ ),
99
+ checkbox: {
100
+ component: ({ field, formField }) => (
101
+ <Checkbox
102
+ checked={formField.value}
103
+ onCheckedChange={formField.onChange}
104
+ disabled={field.disabled}
105
+ />
106
+ ),
107
+ options: {
108
+ className: 'flex items-center gap-2',
109
+ }
110
+ },
111
+ });
112
+
113
+ // Register form wrapper components
114
+ registerFormComponents({
115
+ FormItem: ({ children, className }) => <div className={className}>{children}</div>,
116
+ FormLabel: Label,
117
+ FormMessage: ({ children }) => <span className="text-red-500 text-sm">{children}</span>,
118
+ SubmitButton: Button,
119
+ });
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Field Types
125
+
126
+ Built-in HTML fallback types:
127
+
128
+ | Type | Description | Value Type |
129
+ |------|-------------|------------|
130
+ | `text` | Text input | `string` |
131
+ | `email` | Email input | `string` |
132
+ | `password` | Password input | `string` |
133
+ | `number` | Number input with min/max | `number` |
134
+ | `textarea` | Multi-line text | `string` |
135
+ | `checkbox` | Checkbox | `boolean` |
136
+ | `select` | Dropdown select | `string` |
137
+ | `radio` | Radio button group | `string` |
138
+ | `date` | Date input | `Date` |
139
+ | `time` | Time input | `string` |
140
+ | `file` | File input | `File` |
141
+ | `hidden` | Hidden input | `string` |
142
+
143
+ ### Adding Custom Field Types (TypeScript)
144
+
145
+ Extend the `FieldTypeRegistry` interface to add type checking for custom fields:
146
+
147
+ ```typescript
148
+ // types/form-generator.d.ts
149
+ import { CreateFieldType } from '@connect-soft/form-generator';
150
+
151
+ declare module '@connect-soft/form-generator' {
152
+ interface FieldTypeRegistry {
153
+ // Add your custom field types
154
+ 'color-picker': CreateFieldType<'color-picker', string, {
155
+ swatches?: string[];
156
+ showAlpha?: boolean;
157
+ }>;
158
+ 'rich-text': CreateFieldType<'rich-text', string, {
159
+ toolbar?: ('bold' | 'italic' | 'link')[];
160
+ maxLength?: number;
161
+ }>;
162
+ }
163
+ }
164
+ ```
165
+
166
+ Now TypeScript will recognize your custom field types:
167
+
168
+ ```typescript
169
+ const fields = [
170
+ { type: 'color-picker', name: 'theme', swatches: ['#fff', '#000'] }, // ✅ Valid
171
+ { type: 'unknown-type', name: 'test' }, // ❌ Type error
172
+ ] as const;
173
+ ```
174
+
175
+ Don't forget to register the component for your custom field:
176
+
177
+ ```typescript
178
+ import { registerField } from '@connect-soft/form-generator';
179
+ import { ColorPicker } from './components/ColorPicker';
180
+
181
+ registerField('color-picker', ({ field, formField }) => (
182
+ <ColorPicker
183
+ value={formField.value}
184
+ onChange={formField.onChange}
185
+ swatches={field.swatches}
186
+ showAlpha={field.showAlpha}
187
+ />
188
+ ));
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Custom Validation
194
+
195
+ Use Zod for field-level or form-level validation:
196
+
197
+ ```typescript
198
+ import { z } from 'zod';
199
+
200
+ // Field-level validation
201
+ const fields = [
202
+ {
203
+ type: 'text',
204
+ name: 'username',
205
+ label: 'Username',
206
+ validation: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
207
+ },
30
208
  {
31
209
  type: 'text',
32
210
  name: 'email',
33
211
  label: 'Email',
34
- fieldType: 'email',
35
- required: true,
212
+ validation: z.string().email(),
213
+ },
214
+ ] as const;
215
+
216
+ // Or use a full schema for type inference
217
+ const schema = z.object({
218
+ username: z.string().min(3),
219
+ email: z.string().email(),
220
+ });
221
+
222
+ <FormGenerator
223
+ fields={fields}
224
+ schema={schema}
225
+ onSubmit={(values) => {
226
+ // values is inferred from schema
227
+ }}
228
+ />
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Layouts
234
+
235
+ Organize fields with sections and columns:
236
+
237
+ ```typescript
238
+ const fields = [
239
+ {
240
+ type: 'section',
241
+ title: 'Personal Information',
242
+ children: [
243
+ { type: 'text', name: 'firstName', label: 'First Name' },
244
+ { type: 'text', name: 'lastName', label: 'Last Name' },
245
+ ],
36
246
  },
37
247
  {
38
- type: 'number',
39
- name: 'age',
40
- label: 'Age',
41
- min: 18,
248
+ type: 'columns',
249
+ columns: [
250
+ {
251
+ width: '1',
252
+ children: [
253
+ { type: 'text', name: 'city', label: 'City' },
254
+ ],
255
+ },
256
+ {
257
+ width: '1',
258
+ children: [
259
+ { type: 'text', name: 'zip', label: 'ZIP Code' },
260
+ ],
261
+ },
262
+ ],
42
263
  },
264
+ ];
265
+ ```
266
+
267
+ ---
268
+
269
+ ## TypeScript Type Inference
270
+
271
+ Get full type inference from field definitions:
272
+
273
+ ```typescript
274
+ const fields = [
275
+ { type: 'text', name: 'email', required: true },
276
+ { type: 'number', name: 'age', required: true },
277
+ { type: 'checkbox', name: 'terms' },
43
278
  ] as const;
44
279
 
45
- export const MyForm = () => (
46
- <FormGenerator
47
- fields={fields}
48
- onSubmit={(values) => {
49
- // values is fully typed!
50
- console.log(values);
51
- }}
52
- />
53
- );
280
+ <FormGenerator
281
+ fields={fields}
282
+ onSubmit={(values) => {
283
+ values.email; // string
284
+ values.age; // number
285
+ values.terms; // boolean | undefined
286
+ }}
287
+ />
288
+ ```
289
+
290
+ Or provide an explicit Zod schema:
291
+
292
+ ```typescript
293
+ const schema = z.object({
294
+ email: z.string().email(),
295
+ age: z.number().min(18),
296
+ terms: z.boolean(),
297
+ });
298
+
299
+ <FormGenerator
300
+ fields={fields}
301
+ schema={schema}
302
+ onSubmit={(values) => {
303
+ // values: { email: string; age: number; terms: boolean }
304
+ }}
305
+ />
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Imperative API (Ref)
311
+
312
+ Access form methods programmatically using a ref:
313
+
314
+ ```typescript
315
+ import { useRef } from 'react';
316
+ import { FormGenerator, FormGeneratorRef } from '@connect-soft/form-generator';
317
+
318
+ function MyForm() {
319
+ const formRef = useRef<FormGeneratorRef>(null);
320
+
321
+ const handleExternalSubmit = async () => {
322
+ await formRef.current?.submit();
323
+ };
324
+
325
+ const handleReset = () => {
326
+ formRef.current?.reset();
327
+ };
328
+
329
+ const handleSetValues = () => {
330
+ formRef.current?.setValues({
331
+ email: 'test@example.com',
332
+ age: 25,
333
+ });
334
+ };
335
+
336
+ return (
337
+ <>
338
+ <FormGenerator
339
+ ref={formRef}
340
+ fields={fields}
341
+ onSubmit={(values) => console.log(values)}
342
+ />
343
+ <button type="button" onClick={handleExternalSubmit}>Submit Externally</button>
344
+ <button type="button" onClick={handleReset}>Reset Form</button>
345
+ <button type="button" onClick={handleSetValues}>Set Values</button>
346
+ </>
347
+ );
348
+ }
54
349
  ```
55
350
 
56
- ## Documentation
351
+ ### Available Ref Methods
352
+
353
+ | Method | Description |
354
+ |--------|-------------|
355
+ | `setValues(values)` | Set form values (partial update) |
356
+ | `getValues()` | Get current form values |
357
+ | `reset(values?)` | Reset to default or provided values |
358
+ | `submit()` | Programmatically submit the form |
359
+ | `clearErrors()` | Clear all validation errors |
360
+ | `setError(name, error)` | Set error for a specific field |
361
+ | `isValid()` | Check if form passes validation |
362
+ | `isDirty()` | Check if form has unsaved changes |
363
+ | `form` | Access underlying react-hook-form instance |
364
+
365
+ ---
366
+
367
+ ## API Reference
368
+
369
+ ### FormGenerator Props
370
+
371
+ | Prop | Type | Default | Description |
372
+ |------|------|---------|-------------|
373
+ | `fields` | `FormItem[]` | **required** | Array of field definitions |
374
+ | `onSubmit` | `(values) => void \| Promise<void>` | **required** | Form submission handler |
375
+ | `schema` | `ZodType` | - | Optional Zod schema for validation |
376
+ | `defaultValues` | `object` | `{}` | Initial form values |
377
+ | `className` | `string` | - | CSS class for form element |
378
+ | `submitText` | `string` | `'Submit'` | Submit button text |
379
+ | `disabled` | `boolean` | `false` | Disable entire form |
380
+ | `mode` | `'onChange' \| 'onBlur' \| 'onSubmit' \| 'onTouched' \| 'all'` | `'onChange'` | Validation trigger mode |
381
+
382
+ ### Field Base Properties
383
+
384
+ | Property | Type | Description |
385
+ |----------|------|-------------|
386
+ | `type` | `string` | Field type (text, number, select, etc.) |
387
+ | `name` | `string` | Field name (must be unique) |
388
+ | `label` | `string` | Field label |
389
+ | `description` | `string` | Helper text below field |
390
+ | `required` | `boolean` | Mark field as required |
391
+ | `disabled` | `boolean` | Disable field |
392
+ | `hidden` | `boolean` | Hide field |
393
+ | `defaultValue` | `any` | Default field value |
394
+ | `validation` | `ZodType` | Zod validation schema |
395
+ | `className` | `string` | CSS class for field wrapper |
396
+
397
+ ---
398
+
399
+ ## Links
400
+
401
+ - [GitLab Repository](https://gitlab.com/connect-soft/components/form-generator)
402
+ - [Issues](https://gitlab.com/connect-soft/components/form-generator/issues)
57
403
 
58
- See [main README](../../README.md) for full documentation.
404
+ ---
59
405
 
60
406
  ## License
61
407
 
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var compilerRuntime = require('react/compiler-runtime');
4
3
  var jsxRuntime = require('react/jsx-runtime');
5
4
  var React = require('react');
6
5
  var n$1 = require('zod/v4/core');
7
6
  var zod = require('zod');
7
+ var compilerRuntime = require('react/compiler-runtime');
8
8
 
9
9
  function _interopNamespaceDefault(e) {
10
10
  var n = Object.create(null);
@@ -3465,114 +3465,81 @@ function getItemKey(item, index) {
3465
3465
  function hasSchema(props) {
3466
3466
  return props.schema !== undefined;
3467
3467
  }
3468
- function FormGenerator(props) {
3469
- const $ = compilerRuntime.c(20);
3468
+ function FormGeneratorImpl(props) {
3470
3469
  const {
3471
3470
  fields,
3472
3471
  defaultValues,
3473
3472
  className,
3474
- submitText: t0,
3473
+ submitText = 'Submit',
3475
3474
  disabled,
3476
- mode: t1
3475
+ mode = 'onChange',
3476
+ ref
3477
3477
  } = props;
3478
- const submitText = t0 === undefined ? "Submit" : t0;
3479
- const mode = t1 === undefined ? "onChange" : t1;
3480
3478
  const userSchema = hasSchema(props) ? props.schema : undefined;
3481
3479
  const onSubmit = props.onSubmit;
3482
- let t2;
3483
- bb0: {
3480
+ const formRef = React.useRef(null);
3481
+ const schema = React.useMemo(() => {
3484
3482
  if (userSchema) {
3485
- t2 = userSchema;
3486
- break bb0;
3483
+ return userSchema;
3487
3484
  }
3488
- let t3;
3489
- if ($[0] !== fields) {
3490
- t3 = buildSchema(fields);
3491
- $[0] = fields;
3492
- $[1] = t3;
3493
- } else {
3494
- t3 = $[1];
3495
- }
3496
- t2 = t3;
3497
- }
3498
- const schema = t2;
3499
- let t3;
3500
- if ($[2] !== defaultValues || $[3] !== fields) {
3485
+ return buildSchema(fields);
3486
+ }, [fields, userSchema]);
3487
+ const mergedDefaultValues = React.useMemo(() => {
3501
3488
  const builtDefaults = buildDefaultValues(fields);
3502
- t3 = defaultValues ? deepMerge(builtDefaults, defaultValues) : builtDefaults;
3503
- $[2] = defaultValues;
3504
- $[3] = fields;
3505
- $[4] = t3;
3506
- } else {
3507
- t3 = $[4];
3508
- }
3509
- const mergedDefaultValues = t3;
3510
- let t4;
3511
- if ($[5] !== schema) {
3512
- t4 = a(schema);
3513
- $[5] = schema;
3514
- $[6] = t4;
3515
- } else {
3516
- t4 = $[6];
3517
- }
3518
- let t5;
3519
- if ($[7] !== mergedDefaultValues || $[8] !== mode || $[9] !== t4) {
3520
- t5 = {
3521
- resolver: t4,
3522
- defaultValues: mergedDefaultValues,
3523
- mode
3524
- };
3525
- $[7] = mergedDefaultValues;
3526
- $[8] = mode;
3527
- $[9] = t4;
3528
- $[10] = t5;
3529
- } else {
3530
- t5 = $[10];
3531
- }
3532
- const form = useForm(t5);
3533
- let t6;
3534
- if ($[11] !== className || $[12] !== disabled || $[13] !== fields || $[14] !== form || $[15] !== onSubmit || $[16] !== submitText) {
3535
- let t7;
3536
- if ($[18] !== onSubmit) {
3537
- t7 = async data => {
3538
- await onSubmit(data);
3539
- };
3540
- $[18] = onSubmit;
3541
- $[19] = t7;
3542
- } else {
3543
- t7 = $[19];
3544
- }
3545
- const handleSubmit = form.handleSubmit(t7);
3546
- const SubmitButton = getFormComponent("SubmitButton");
3547
- t6 = jsxRuntime.jsx(FormProvider, {
3548
- ...form,
3549
- children: jsxRuntime.jsxs("form", {
3550
- onSubmit: handleSubmit,
3551
- className,
3552
- children: [fields.map(_temp), jsxRuntime.jsx(SubmitButton, {
3553
- disabled: disabled || form.formState.isSubmitting,
3554
- isSubmitting: form.formState.isSubmitting,
3555
- children: submitText
3556
- })]
3557
- })
3558
- });
3559
- $[11] = className;
3560
- $[12] = disabled;
3561
- $[13] = fields;
3562
- $[14] = form;
3563
- $[15] = onSubmit;
3564
- $[16] = submitText;
3565
- $[17] = t6;
3566
- } else {
3567
- t6 = $[17];
3568
- }
3569
- return t6;
3570
- }
3571
- function _temp(item, index) {
3572
- return jsxRuntime.jsx(FormItemRenderer, {
3573
- item
3574
- }, getItemKey(item, index));
3489
+ return defaultValues ? deepMerge(builtDefaults, defaultValues) : builtDefaults;
3490
+ }, [fields, defaultValues]);
3491
+ const form = useForm({
3492
+ resolver: a(schema),
3493
+ defaultValues: mergedDefaultValues,
3494
+ mode
3495
+ });
3496
+ const handleSubmit = form.handleSubmit(async data => {
3497
+ await onSubmit(data);
3498
+ });
3499
+ React.useImperativeHandle(ref, () => ({
3500
+ setValues: values => {
3501
+ Object.entries(values).forEach(([key, value]) => {
3502
+ form.setValue(key, value, {
3503
+ shouldValidate: true,
3504
+ shouldDirty: true
3505
+ });
3506
+ });
3507
+ },
3508
+ getValues: () => form.getValues(),
3509
+ reset: values_0 => {
3510
+ if (values_0) {
3511
+ form.reset(deepMerge(mergedDefaultValues, values_0));
3512
+ } else {
3513
+ form.reset(mergedDefaultValues);
3514
+ }
3515
+ },
3516
+ submit: async () => {
3517
+ await handleSubmit();
3518
+ },
3519
+ clearErrors: () => form.clearErrors(),
3520
+ setError: (name, error) => form.setError(name, error),
3521
+ isValid: () => form.formState.isValid,
3522
+ isDirty: () => form.formState.isDirty,
3523
+ form
3524
+ }), [form, handleSubmit, mergedDefaultValues]);
3525
+ const SubmitButton = getFormComponent('SubmitButton');
3526
+ return jsxRuntime.jsx(FormProvider, {
3527
+ ...form,
3528
+ children: jsxRuntime.jsxs("form", {
3529
+ ref: formRef,
3530
+ onSubmit: handleSubmit,
3531
+ className: className,
3532
+ children: [fields.map((item, index) => jsxRuntime.jsx(FormItemRenderer, {
3533
+ item: item
3534
+ }, getItemKey(item, index))), jsxRuntime.jsx(SubmitButton, {
3535
+ disabled: disabled || form.formState.isSubmitting,
3536
+ isSubmitting: form.formState.isSubmitting,
3537
+ children: submitText
3538
+ })]
3539
+ })
3540
+ });
3575
3541
  }
3542
+ const FormGenerator = FormGeneratorImpl;
3576
3543
 
3577
3544
  Object.defineProperty(exports, "z", {
3578
3545
  enumerable: true,