@connect-soft/form-generator 1.0.0 → 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.
- package/README.md +956 -188
- package/dist/index.js +4400 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4329 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types/components/form/array-field-renderer.d.ts +39 -0
- package/dist/types/components/form/array-field-renderer.d.ts.map +1 -0
- package/dist/types/components/form/create-template-fields.d.ts +3 -0
- package/dist/types/components/form/create-template-fields.d.ts.map +1 -0
- package/dist/types/components/form/field-renderer.d.ts +7 -0
- package/dist/types/components/form/field-renderer.d.ts.map +1 -0
- 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 +47 -0
- package/dist/types/components/form/form-generator-typed.d.ts.map +1 -0
- package/dist/types/components/form/form-generator.d.ts +51 -0
- package/dist/types/components/form/form-generator.d.ts.map +1 -0
- package/dist/types/components/form/form-utils.d.ts +47 -0
- package/dist/types/components/form/form-utils.d.ts.map +1 -0
- package/dist/types/components/form/index.d.ts +7 -0
- package/dist/types/components/form/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/lib/field-registry.d.ts +74 -0
- package/dist/types/lib/field-registry.d.ts.map +1 -0
- package/dist/types/lib/field-types.d.ts +151 -0
- package/dist/types/lib/field-types.d.ts.map +1 -0
- package/dist/types/lib/index.d.ts +7 -0
- package/dist/types/lib/index.d.ts.map +1 -0
- package/dist/types/lib/template-types.d.ts +55 -0
- package/dist/types/lib/template-types.d.ts.map +1 -0
- package/dist/types/setupTests.d.ts +2 -0
- package/dist/types/setupTests.d.ts.map +1 -0
- package/package.json +40 -131
package/README.md
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
# @connect-soft/form-generator
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Headless, type-safe form generator with react-hook-form and Zod validation
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@connect-soft/form-generator)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
-
[](https://bundlephobia.com/package/@connect-soft/form-generator)
|
|
8
|
-
|
|
9
|
-
**v2.0.0** - Complete rewrite with Radix UI + Tailwind CSS
|
|
10
|
-
**50-60% smaller bundle** | **No runtime CSS overhead** | **Modern, accessible UI**
|
|
11
7
|
|
|
12
8
|
---
|
|
13
9
|
|
|
14
10
|
## Features
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
24
23
|
|
|
25
24
|
---
|
|
26
25
|
|
|
27
26
|
## Installation
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
|
-
npm install @connect-soft/form-generator
|
|
29
|
+
npm install @connect-soft/form-generator
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
### Peer Dependencies
|
|
@@ -36,74 +35,23 @@ npm install @connect-soft/form-generator react-hook-form zod
|
|
|
36
35
|
- `react-dom` ^19.0.0
|
|
37
36
|
- `zod` ^4.0.0
|
|
38
37
|
|
|
39
|
-
### Tailwind CSS Setup
|
|
40
|
-
|
|
41
|
-
This library requires Tailwind CSS. If you don't have it set up:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npm install -D tailwindcss postcss autoprefixer
|
|
45
|
-
npx tailwindcss init -p
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
**tailwind.config.js:**
|
|
49
|
-
```javascript
|
|
50
|
-
module.exports = {
|
|
51
|
-
content: [
|
|
52
|
-
'./src/**/*.{js,ts,jsx,tsx}',
|
|
53
|
-
'./node_modules/@connect-soft/form-generator/dist/**/*.{js,mjs}',
|
|
54
|
-
],
|
|
55
|
-
theme: {
|
|
56
|
-
extend: {
|
|
57
|
-
// Your custom theme
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
plugins: [],
|
|
61
|
-
};
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**Import the styles in your main file:**
|
|
65
|
-
```typescript
|
|
66
|
-
import '@connect-soft/form-generator/dist/styles.css';
|
|
67
|
-
```
|
|
68
|
-
|
|
69
38
|
---
|
|
70
39
|
|
|
71
40
|
## Quick Start
|
|
72
41
|
|
|
42
|
+
The library works immediately with HTML fallback components:
|
|
43
|
+
|
|
73
44
|
```typescript
|
|
74
|
-
import { FormGenerator
|
|
45
|
+
import { FormGenerator } from '@connect-soft/form-generator';
|
|
75
46
|
|
|
76
|
-
const fields
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
label: '
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
type: 'number',
|
|
87
|
-
name: 'age',
|
|
88
|
-
label: 'Age',
|
|
89
|
-
min: 18,
|
|
90
|
-
max: 120,
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
type: 'select',
|
|
94
|
-
name: 'country',
|
|
95
|
-
label: 'Country',
|
|
96
|
-
options: [
|
|
97
|
-
{ label: 'United States', value: 'us' },
|
|
98
|
-
{ label: 'United Kingdom', value: 'uk' },
|
|
99
|
-
{ label: 'Germany', value: 'de' },
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
type: 'checkbox',
|
|
104
|
-
name: 'subscribe',
|
|
105
|
-
label: 'Subscribe to newsletter',
|
|
106
|
-
},
|
|
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' },
|
|
107
55
|
] as const;
|
|
108
56
|
|
|
109
57
|
function MyForm() {
|
|
@@ -111,13 +59,8 @@ function MyForm() {
|
|
|
111
59
|
<FormGenerator
|
|
112
60
|
fields={fields}
|
|
113
61
|
onSubmit={(values) => {
|
|
114
|
-
|
|
115
|
-
console.log(values.email); // string
|
|
116
|
-
console.log(values.age); // number
|
|
117
|
-
console.log(values.country); // string
|
|
118
|
-
console.log(values.subscribe);// boolean
|
|
62
|
+
console.log(values); // Fully typed!
|
|
119
63
|
}}
|
|
120
|
-
submitText="Create Account"
|
|
121
64
|
/>
|
|
122
65
|
);
|
|
123
66
|
}
|
|
@@ -125,77 +68,250 @@ function MyForm() {
|
|
|
125
68
|
|
|
126
69
|
---
|
|
127
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
|
+
|
|
128
172
|
## Field Types
|
|
129
173
|
|
|
174
|
+
Built-in HTML fallback types:
|
|
175
|
+
|
|
130
176
|
| Type | Description | Value Type |
|
|
131
177
|
|------|-------------|------------|
|
|
132
|
-
| `text` | Text input
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
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` |
|
|
137
184
|
| `select` | Dropdown select | `string` |
|
|
138
185
|
| `radio` | Radio button group | `string` |
|
|
139
|
-
| `date` | Date
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
186
|
+
| `date` | Date input | `Date` |
|
|
187
|
+
| `time` | Time input | `string` |
|
|
188
|
+
| `file` | File input | `File` |
|
|
189
|
+
| `hidden` | Hidden input | `string` |
|
|
142
190
|
|
|
143
|
-
|
|
191
|
+
### Adding Custom Field Types (TypeScript)
|
|
144
192
|
|
|
145
|
-
|
|
193
|
+
Extend the `FieldTypeRegistry` interface to add type checking for custom fields:
|
|
146
194
|
|
|
147
|
-
|
|
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:
|
|
148
215
|
|
|
149
216
|
```typescript
|
|
150
|
-
|
|
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
|
+
```
|
|
151
222
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
162
241
|
```
|
|
163
242
|
|
|
164
|
-
|
|
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`:
|
|
165
248
|
|
|
166
249
|
```typescript
|
|
167
|
-
import {
|
|
250
|
+
import { createField, createArrayField, strictFields } from '@connect-soft/form-generator';
|
|
168
251
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
]);
|
|
173
277
|
```
|
|
174
278
|
|
|
175
|
-
###
|
|
279
|
+
### Runtime Field Type Validation
|
|
280
|
+
|
|
281
|
+
Enable runtime validation to catch unregistered field types during development:
|
|
176
282
|
|
|
177
283
|
```typescript
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
onChange={setSelectedValues}
|
|
184
|
-
leftTitle="Available"
|
|
185
|
-
rightTitle="Selected"
|
|
186
|
-
searchable={true}
|
|
284
|
+
// Warn in console for unregistered types (recommended for development)
|
|
285
|
+
<FormGenerator
|
|
286
|
+
fields={fields}
|
|
287
|
+
onSubmit={handleSubmit}
|
|
288
|
+
validateTypes
|
|
187
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 });
|
|
188
303
|
```
|
|
189
304
|
|
|
190
305
|
---
|
|
191
306
|
|
|
192
307
|
## Custom Validation
|
|
193
308
|
|
|
194
|
-
Use Zod for
|
|
309
|
+
Use Zod for field-level or form-level validation:
|
|
195
310
|
|
|
196
311
|
```typescript
|
|
197
312
|
import { z } from 'zod';
|
|
198
313
|
|
|
314
|
+
// Field-level validation
|
|
199
315
|
const fields = [
|
|
200
316
|
{
|
|
201
317
|
type: 'text',
|
|
@@ -207,104 +323,727 @@ const fields = [
|
|
|
207
323
|
type: 'text',
|
|
208
324
|
name: 'email',
|
|
209
325
|
label: 'Email',
|
|
210
|
-
|
|
211
|
-
validation: z.string().email().endsWith('@company.com'),
|
|
326
|
+
validation: z.string().email(),
|
|
212
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' },
|
|
213
354
|
{
|
|
214
|
-
type: '
|
|
215
|
-
name: '
|
|
216
|
-
label: '
|
|
217
|
-
|
|
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,
|
|
218
364
|
},
|
|
219
365
|
] as const;
|
|
220
366
|
```
|
|
221
367
|
|
|
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
|
+
|
|
222
437
|
---
|
|
223
438
|
|
|
224
|
-
##
|
|
439
|
+
## Custom Layouts
|
|
225
440
|
|
|
226
|
-
|
|
441
|
+
For full control over form layout, pass a render function as `children`:
|
|
227
442
|
|
|
228
443
|
```typescript
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
);
|
|
233
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>
|
|
234
591
|
```
|
|
235
592
|
|
|
236
|
-
###
|
|
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
|
|
237
624
|
|
|
238
625
|
```typescript
|
|
239
|
-
|
|
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}
|
|
240
742
|
fields={fields}
|
|
241
743
|
onSubmit={handleSubmit}
|
|
242
|
-
className="max-w-md mx-auto p-6 bg-white rounded-lg shadow"
|
|
243
744
|
/>
|
|
244
745
|
```
|
|
245
746
|
|
|
246
|
-
###
|
|
747
|
+
### StrictFormGenerator vs FormGenerator
|
|
247
748
|
|
|
248
|
-
|
|
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 |
|
|
249
756
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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 }
|
|
256
818
|
```
|
|
257
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
|
+
// ]
|
|
884
|
+
```
|
|
885
|
+
|
|
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
|
+
|
|
258
894
|
---
|
|
259
895
|
|
|
260
896
|
## TypeScript Type Inference
|
|
261
897
|
|
|
262
|
-
|
|
898
|
+
Get full type inference from field definitions:
|
|
263
899
|
|
|
264
900
|
```typescript
|
|
265
901
|
const fields = [
|
|
266
|
-
{ type: 'text', name: 'email',
|
|
267
|
-
{ type: 'number', name: 'age' },
|
|
902
|
+
{ type: 'text', name: 'email', required: true },
|
|
903
|
+
{ type: 'number', name: 'age', required: true },
|
|
268
904
|
{ type: 'checkbox', name: 'terms' },
|
|
269
|
-
{ type: 'date', name: 'birthdate' },
|
|
270
905
|
] as const;
|
|
271
906
|
|
|
272
907
|
<FormGenerator
|
|
273
908
|
fields={fields}
|
|
274
909
|
onSubmit={(values) => {
|
|
275
|
-
//
|
|
276
|
-
values.
|
|
277
|
-
values.
|
|
278
|
-
|
|
279
|
-
|
|
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 }
|
|
280
931
|
}}
|
|
281
932
|
/>
|
|
282
933
|
```
|
|
283
934
|
|
|
284
935
|
---
|
|
285
936
|
|
|
286
|
-
##
|
|
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
|
|
287
979
|
|
|
288
|
-
|
|
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 |
|
|
289
991
|
|
|
290
|
-
###
|
|
992
|
+
### Watching Form Values
|
|
993
|
+
|
|
994
|
+
Detect when field values change from the parent component:
|
|
291
995
|
|
|
292
996
|
```typescript
|
|
293
|
-
import {
|
|
294
|
-
import {
|
|
997
|
+
import { useRef, useEffect } from 'react';
|
|
998
|
+
import { FormGenerator, FormGeneratorRef } from '@connect-soft/form-generator';
|
|
295
999
|
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
});
|
|
303
1009
|
|
|
304
|
-
|
|
305
|
-
|
|
1010
|
+
return () => subscription?.unsubscribe();
|
|
1011
|
+
}, []);
|
|
306
1012
|
|
|
307
|
-
|
|
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>
|
|
308
1047
|
```
|
|
309
1048
|
|
|
310
1049
|
---
|
|
@@ -315,14 +1054,20 @@ const v2Fields = adaptV1FieldsToV2(v1Fields);
|
|
|
315
1054
|
|
|
316
1055
|
| Prop | Type | Default | Description |
|
|
317
1056
|
|------|------|---------|-------------|
|
|
318
|
-
| `fields` | `
|
|
1057
|
+
| `fields` | `FormItem[]` | **required** | Array of field definitions |
|
|
319
1058
|
| `onSubmit` | `(values) => void \| Promise<void>` | **required** | Form submission handler |
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
1059
|
+
| `schema` | `ZodType` | - | Optional Zod schema for validation |
|
|
1060
|
+
| `defaultValues` | `object` | `{}` | Initial form values |
|
|
1061
|
+
| `className` | `string` | - | CSS class for form element |
|
|
322
1062
|
| `submitText` | `string` | `'Submit'` | Submit button text |
|
|
323
|
-
| `submitButtonVariant` | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'default'` | Submit button style |
|
|
324
1063
|
| `disabled` | `boolean` | `false` | Disable entire form |
|
|
325
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 |
|
|
326
1071
|
|
|
327
1072
|
### Field Base Properties
|
|
328
1073
|
|
|
@@ -337,13 +1082,50 @@ const v2Fields = adaptV1FieldsToV2(v1Fields);
|
|
|
337
1082
|
| `hidden` | `boolean` | Hide field |
|
|
338
1083
|
| `defaultValue` | `any` | Default field value |
|
|
339
1084
|
| `validation` | `ZodType` | Zod validation schema |
|
|
340
|
-
| `className` | `string` |
|
|
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.) |
|
|
341
1124
|
|
|
342
1125
|
---
|
|
343
1126
|
|
|
344
1127
|
## Links
|
|
345
1128
|
|
|
346
|
-
- [Migration Guide](./MIGRATION.md)
|
|
347
1129
|
- [GitLab Repository](https://gitlab.com/connect-soft/components/form-generator)
|
|
348
1130
|
- [Issues](https://gitlab.com/connect-soft/components/form-generator/issues)
|
|
349
1131
|
|
|
@@ -352,17 +1134,3 @@ const v2Fields = adaptV1FieldsToV2(v1Fields);
|
|
|
352
1134
|
## License
|
|
353
1135
|
|
|
354
1136
|
ISC © Connect Soft
|
|
355
|
-
|
|
356
|
-
---
|
|
357
|
-
|
|
358
|
-
## Acknowledgments
|
|
359
|
-
|
|
360
|
-
Built with:
|
|
361
|
-
- [Radix UI](https://www.radix-ui.com/) - Accessible UI primitives
|
|
362
|
-
- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
|
|
363
|
-
- [react-hook-form](https://react-hook-form.com/) - Performant form library
|
|
364
|
-
- [Zod](https://zod.dev/) - TypeScript-first schema validation
|
|
365
|
-
- [react-day-picker](https://react-day-picker.js.org/) - Flexible date picker
|
|
366
|
-
- [lucide-react](https://lucide.dev/) - Beautiful icons
|
|
367
|
-
|
|
368
|
-
Inspired by [shadcn/ui](https://ui.shadcn.com/) component architecture.
|