@connect-soft/form-generator
v1.1.0-alpha5
Published
Headless, type-safe form generator with react-hook-form and Zod validation
Downloads
357
Maintainers
Readme
@connect-soft/form-generator
Headless, type-safe form generator with react-hook-form and Zod validation
Features
- Headless: Bring your own UI components (Radix, MUI, Chakra, or plain HTML)
- Type-Safe: Full TypeScript inference for form values and field types
- Field Type Checking: Compile-time validation of
field.typewith autocomplete - Extensible Types: Add custom field types via module augmentation
- Imperative API: Control form via ref (
setValues,reset,submit, etc.) - Flexible: Register custom field components with a simple API
- Validation: Built-in Zod validation support
- Layouts: Support for sections and multi-column layouts
- Lightweight: No UI dependencies, minimal footprint
- HTML Fallbacks: Works out of the box with native HTML inputs
Installation
npm install @connect-soft/form-generatorPeer Dependencies
react^19.0.0react-dom^19.0.0zod^4.0.0
Quick Start
The library works immediately with HTML fallback components:
import { FormGenerator } from '@connect-soft/form-generator';
const fields = [
{ type: 'text', name: 'email', label: 'Email', required: true },
{ type: 'number', name: 'age', label: 'Age', min: 18, max: 120 },
{ type: 'select', name: 'country', label: 'Country', options: [
{ label: 'United States', value: 'us' },
{ label: 'Germany', value: 'de' },
]},
{ type: 'checkbox', name: 'subscribe', label: 'Subscribe to newsletter' },
] as const;
function MyForm() {
return (
<FormGenerator
fields={fields}
onSubmit={(values) => {
console.log(values); // Fully typed!
}}
/>
);
}Registering Custom Components
Register your own UI components to replace the HTML fallbacks:
import { registerFields, registerFormComponents } from '@connect-soft/form-generator';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Checkbox } from './ui/checkbox';
import { Button } from './ui/button';
// Register field components
registerFields({
text: ({ field, formField }) => (
<Input
{...formField}
type={field.fieldType || 'text'}
placeholder={field.placeholder}
disabled={field.disabled}
/>
),
number: ({ field, formField }) => (
<Input
{...formField}
type="number"
min={field.min}
max={field.max}
/>
),
checkbox: {
component: ({ field, formField }) => (
<Checkbox
checked={formField.value}
onCheckedChange={formField.onChange}
disabled={field.disabled}
/>
),
options: {
className: 'flex items-center gap-2',
}
},
});
// Register form wrapper components
registerFormComponents({
FormItem: ({ children, className }) => <div className={className}>{children}</div>,
FormLabel: Label,
FormMessage: ({ children }) => <span className="text-red-500 text-sm">{children}</span>,
SubmitButton: Button,
});Field Types
Built-in HTML fallback types:
| Type | Description | Value Type |
|------|-------------|------------|
| text | Text input | string |
| email | Email input | string |
| password | Password input | string |
| number | Number input with min/max | number |
| textarea | Multi-line text | string |
| checkbox | Checkbox | boolean |
| select | Dropdown select | string |
| radio | Radio button group | string |
| date | Date input | Date |
| time | Time input | string |
| file | File input | File |
| hidden | Hidden input | string |
Adding Custom Field Types (TypeScript)
Extend the FieldTypeRegistry interface to add type checking for custom fields:
// types/form-generator.d.ts
import { CreateFieldType } from '@connect-soft/form-generator';
declare module '@connect-soft/form-generator' {
interface FieldTypeRegistry {
// Add your custom field types
'color-picker': CreateFieldType<'color-picker', string, {
swatches?: string[];
showAlpha?: boolean;
}>;
'rich-text': CreateFieldType<'rich-text', string, {
toolbar?: ('bold' | 'italic' | 'link')[];
maxLength?: number;
}>;
}
}Now TypeScript will recognize your custom field types:
const fields = [
{ type: 'color-picker', name: 'theme', swatches: ['#fff', '#000'] }, // ✅ Valid
{ type: 'unknown-type', name: 'test' }, // ❌ Type error
] as const;Don't forget to register the component for your custom field:
import { registerField } from '@connect-soft/form-generator';
import { ColorPicker } from './components/ColorPicker';
registerField('color-picker', ({ field, formField }) => (
<ColorPicker
value={formField.value}
onChange={formField.onChange}
swatches={field.swatches}
showAlpha={field.showAlpha}
/>
));Custom Validation
Use Zod for field-level or form-level validation:
import { z } from 'zod';
// Field-level validation
const fields = [
{
type: 'text',
name: 'username',
label: 'Username',
validation: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
},
{
type: 'text',
name: 'email',
label: 'Email',
validation: z.string().email(),
},
] as const;
// Or use a full schema for type inference
const schema = z.object({
username: z.string().min(3),
email: z.string().email(),
});
<FormGenerator
fields={fields}
schema={schema}
onSubmit={(values) => {
// values is inferred from schema
}}
/>Layouts
Organize fields with sections and columns:
const fields = [
{
type: 'section',
title: 'Personal Information',
children: [
{ type: 'text', name: 'firstName', label: 'First Name' },
{ type: 'text', name: 'lastName', label: 'Last Name' },
],
},
{
type: 'columns',
columns: [
{
width: '1',
children: [
{ type: 'text', name: 'city', label: 'City' },
],
},
{
width: '1',
children: [
{ type: 'text', name: 'zip', label: 'ZIP Code' },
],
},
],
},
];Custom Layouts
For full control over form layout, pass a render function as children:
<FormGenerator
fields={[
{ type: 'text', name: 'email', label: 'Email' },
{ type: 'password', name: 'password', label: 'Password' },
] as const}
title="Login"
onSubmit={handleSubmit}
>
{({ fields, buttons, title }) => (
<div className="login-form">
<h1>{title}</h1>
<div className="field-row">{fields.email}</div>
<div className="field-row">{fields.password}</div>
<div className="actions">{buttons.submit}</div>
</div>
)}
</FormGenerator>Render Props API
The render function receives:
| Property | Type | Description |
|----------|------|-------------|
| fields | TemplateFields | Pre-rendered fields (see below) |
| layouts | TemplateLayouts | Pre-rendered named layouts |
| buttons | { submit, reset? } | Pre-rendered buttons |
| title | string | Form title prop |
| description | string | Form description prop |
| form | UseFormReturn | react-hook-form instance |
| isSubmitting | boolean | Form submission state |
| isValid | boolean | Form validity state |
| isDirty | boolean | Form dirty state |
| renderField | function | Manual field renderer |
| renderLayout | function | Manual layout renderer |
Fields Object
Access fields by name or use helper methods:
{({ fields }) => (
<div>
{/* Access individual fields */}
{fields.email}
{fields.password}
{/* Render all fields */}
{fields.all}
{/* Render only fields not yet accessed */}
{fields.remaining}
{/* Check if field exists */}
{fields.has('email') && fields.email}
{/* Get all field names */}
{fields.names.map(name => <div key={name}>{fields[name]}</div>)}
{/* Render specific fields */}
{fields.render('email', 'password')}
</div>
)}Mixed Layout Example
Highlight specific fields while rendering the rest normally:
<FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
{({ fields, buttons }) => (
<div>
<div className="highlighted">{fields.email}</div>
<div className="other-fields">{fields.remaining}</div>
{buttons.submit}
</div>
)}
</FormGenerator>Form State Access
Use form state for conditional rendering:
<FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
{({ fields, buttons, isSubmitting, isValid, isDirty }) => (
<div>
{fields.all}
<button type="submit" disabled={isSubmitting || !isValid}>
{isSubmitting ? 'Saving...' : 'Submit'}
</button>
{isDirty && <span>You have unsaved changes</span>}
</div>
)}
</FormGenerator>TypeScript Type Inference
Get full type inference from field definitions:
const fields = [
{ type: 'text', name: 'email', required: true },
{ type: 'number', name: 'age', required: true },
{ type: 'checkbox', name: 'terms' },
] as const;
<FormGenerator
fields={fields}
onSubmit={(values) => {
values.email; // string
values.age; // number
values.terms; // boolean | undefined
}}
/>Or provide an explicit Zod schema:
const schema = z.object({
email: z.string().email(),
age: z.number().min(18),
terms: z.boolean(),
});
<FormGenerator
fields={fields}
schema={schema}
onSubmit={(values) => {
// values: { email: string; age: number; terms: boolean }
}}
/>Imperative API (Ref)
Access form methods programmatically using a ref:
import { useRef } from 'react';
import { FormGenerator, FormGeneratorRef } from '@connect-soft/form-generator';
function MyForm() {
const formRef = useRef<FormGeneratorRef>(null);
const handleExternalSubmit = async () => {
await formRef.current?.submit();
};
const handleReset = () => {
formRef.current?.reset();
};
const handleSetValues = () => {
formRef.current?.setValues({
email: '[email protected]',
age: 25,
});
};
return (
<>
<FormGenerator
ref={formRef}
fields={fields}
onSubmit={(values) => console.log(values)}
/>
<button type="button" onClick={handleExternalSubmit}>Submit Externally</button>
<button type="button" onClick={handleReset}>Reset Form</button>
<button type="button" onClick={handleSetValues}>Set Values</button>
</>
);
}Available Ref Methods
| Method | Description |
|--------|-------------|
| setValues(values) | Set form values (partial update) |
| getValues() | Get current form values |
| reset(values?) | Reset to default or provided values |
| submit() | Programmatically submit the form |
| clearErrors() | Clear all validation errors |
| setError(name, error) | Set error for a specific field |
| isValid() | Check if form passes validation |
| isDirty() | Check if form has unsaved changes |
| form | Access underlying react-hook-form instance |
API Reference
FormGenerator Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| fields | FormItem[] | required | Array of field definitions |
| onSubmit | (values) => void \| Promise<void> | required | Form submission handler |
| schema | ZodType | - | Optional Zod schema for validation |
| defaultValues | object | {} | Initial form values |
| className | string | - | CSS class for form element |
| submitText | string | 'Submit' | Submit button text |
| disabled | boolean | false | Disable entire form |
| mode | 'onChange' \| 'onBlur' \| 'onSubmit' \| 'onTouched' \| 'all' | 'onChange' | Validation trigger mode |
| children | TemplateRenderFn | - | Render function for custom layout |
| title | string | - | Form title (available in render props) |
| description | string | - | Form description (available in render props) |
| showReset | boolean | false | Include reset button in buttons.reset |
| resetText | string | 'Reset' | Reset button text |
Field Base Properties
| Property | Type | Description |
|----------|------|-------------|
| type | string | Field type (text, number, select, etc.) |
| name | string | Field name (must be unique) |
| label | string | Field label |
| description | string | Helper text below field |
| required | boolean | Mark field as required |
| disabled | boolean | Disable field |
| hidden | boolean | Hide field |
| defaultValue | any | Default field value |
| validation | ZodType | Zod validation schema |
| className | string | CSS class for field wrapper |
Links
License
ISC © Connect Soft
