npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@connect-soft/form-generator

v1.1.1

Published

Headless, type-safe form generator with react-hook-form and Zod validation

Readme

@connect-soft/form-generator

Headless, type-safe form generator with react-hook-form and Zod validation

Version License


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.type with autocomplete
  • Extensible Types: Add custom field types via module augmentation
  • Imperative API: Control form via ref (setValues, reset, setDefaultValues, submit, etc.)
  • Flexible: Register custom field components with a simple API
  • Validation: Built-in Zod validation support
  • Array Fields: Repeatable field groups with useFieldArray integration
  • Custom Layouts: Full control over form layout via render props
  • Lightweight: No UI dependencies, minimal footprint
  • HTML Fallbacks: Works out of the box with native HTML inputs

Installation

npm install @connect-soft/form-generator

Peer Dependencies

  • react ^19.0.0
  • react-dom ^19.0.0
  • zod ^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. Field components are fully responsible for rendering their own label, description, and error messages:

import { registerFields, registerFormComponent } 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 - they handle their own rendering
registerFields({
  text: ({ field, formField, fieldState }) => (
    <div className="form-field">
      {field.label && <Label>{field.label}{field.required && ' *'}</Label>}
      <Input
        {...formField}
        type={field.fieldType || 'text'}
        placeholder={field.placeholder}
        disabled={field.disabled}
      />
      {field.description && <p className="text-sm text-muted">{field.description}</p>}
      {fieldState.error && <span className="text-red-500 text-sm">{fieldState.error.message}</span>}
    </div>
  ),
  number: ({ field, formField, fieldState }) => (
    <div className="form-field">
      {field.label && <Label>{field.label}</Label>}
      <Input
        {...formField}
        type="number"
        min={field.min}
        max={field.max}
      />
      {fieldState.error && <span className="text-red-500 text-sm">{fieldState.error.message}</span>}
    </div>
  ),
  checkbox: ({ field, formField }) => (
    <div className="flex items-center gap-2">
      <Checkbox
        checked={formField.value}
        onCheckedChange={formField.onChange}
        disabled={field.disabled}
      />
      {field.label && <Label>{field.label}</Label>}
    </div>
  ),
});

// Register the submit button component
registerFormComponent('SubmitButton', Button);

// Optional: Register field wrappers for custom styling
registerFormComponent('FieldWrapper', ({ children, name, type, className }) => (
  <div className={`field-wrapper field-${type}`} data-field={name}>
    {children}
  </div>
));

registerFormComponent('FieldsWrapper', ({ children }) => (
  <div className="form-fields">
    {children}
  </div>
));

Field Wrappers

Customize how fields are wrapped without modifying individual field components:

FieldWrapper

Wraps each individual field. Receives the field's name, type, and className:

registerFormComponent('FieldWrapper', ({ children, name, type, className }) => (
  <div className={`form-group ${className || ''}`} data-field-type={type}>
    {children}
  </div>
));

FieldsWrapper

Wraps all fields together (excludes the submit button). Useful for grid layouts:

registerFormComponent('FieldsWrapper', ({ children, className }) => (
  <div className={`grid grid-cols-2 gap-4 ${className || ''}`}>
    {children}
  </div>
));

Both default to React.Fragment, adding zero DOM overhead when not customized.


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;

Then register the component for your custom field:

import { registerField } from '@connect-soft/form-generator';
import { ColorPicker } from './components/ColorPicker';

// Type-safe: 'color-picker' must exist in FieldTypeRegistry
registerField('color-picker', ({ field, formField }) => (
  <ColorPicker
    value={formField.value}
    onChange={formField.onChange}
    swatches={field.swatches}
    showAlpha={field.showAlpha}
  />
));

// ❌ TypeScript error: 'unknown-type' is not in FieldTypeRegistry
// registerField('unknown-type', MyComponent);

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.

Field Type Validation Helpers

Use helper functions for strict type checking without as const:

import { createField, createArrayField, strictFields } from '@connect-soft/form-generator';

// Create a single field with full type checking
const emailField = createField({
  type: 'email',
  name: 'email',
  label: 'Email',
  placeholder: 'Enter your email'  // TypeScript knows this is valid for email
});

// Create an array field
const contacts = createArrayField({
  name: 'contacts',
  fields: [
    { type: 'text', name: 'name', label: 'Name' },
    { type: 'email', name: 'email', label: 'Email' }
  ],
  minItems: 1,
  maxItems: 5
});

// Create an array of fields with type checking
const fields = strictFields([
  { type: 'text', name: 'username', label: 'Username' },
  { type: 'email', name: 'email', label: 'Email' },
  // { type: 'unknown', name: 'bad' }  // TypeScript error!
]);

Runtime Field Type Validation

Enable runtime validation to catch unregistered field types during development:

// Warn in console for unregistered types (recommended for development)
<FormGenerator
  fields={fields}
  onSubmit={handleSubmit}
  validateTypes
/>

// Throw an error for unregistered types
<FormGenerator
  fields={fields}
  onSubmit={handleSubmit}
  validateTypes={{ throwOnError: true }}
/>

// Manual validation
import { validateFieldTypes, getRegisteredFieldTypes } from '@connect-soft/form-generator';

const registeredTypes = getRegisteredFieldTypes();
validateFieldTypes(fields, registeredTypes, { throwOnError: true });

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
  }}
/>

Array Fields

Create repeatable field groups with useFieldArray integration:

const fields = [
  { type: 'text', name: 'name', label: 'Name' },
  {
    type: 'array',
    name: 'contacts',
    label: 'Contacts',
    fields: [
      { type: 'text', name: 'email', label: 'Email' },
      { type: 'text', name: 'phone', label: 'Phone' },
    ],
    minItems: 1,
    maxItems: 5,
  },
] as const;

Default Array Rendering

Array fields render automatically with add/remove functionality:

<FormGenerator
  fields={fields}
  onSubmit={(values) => {
    console.log(values.contacts); // Array<{ email: string, phone: string }>
  }}
/>

Custom Array Rendering with useArrayField

For full control, use the useArrayField hook in a custom layout:

import { FormGenerator, useArrayField } from '@connect-soft/form-generator';

<FormGenerator fields={fields} onSubmit={handleSubmit}>
  {({ fields, arrays, buttons }) => {
    const contacts = useArrayField(arrays.contacts.field);

    return (
      <div>
        {fields.name}

        <h3>Contacts</h3>
        {contacts.items.map(({ id, index, remove, fields: itemFields }) => (
          <div key={id} className="contact-row">
            {itemFields.email}
            {itemFields.phone}
            {contacts.canRemove && (
              <button type="button" onClick={remove}>Remove</button>
            )}
          </div>
        ))}

        {contacts.canAppend && (
          <button type="button" onClick={contacts.append}>
            Add Contact
          </button>
        )}

        {buttons.submit}
      </div>
    );
  }}
</FormGenerator>

useArrayField Return Values

| Property | Type | Description | |----------|------|-------------| | items | Array<{ id, index }> | Array items with unique ids | | append | () => void | Add new empty item | | appendWith | (values) => void | Add item with values | | prepend | () => void | Add item at beginning | | remove | (index) => void | Remove item at index | | move | (from, to) => void | Move item | | swap | (a, b) => void | Swap two items | | insert | (index, values?) => void | Insert at index | | canAppend | boolean | Can add more items (respects maxItems) | | canRemove | boolean | Can remove items (respects minItems) | | renderField | (index, name) => ReactElement | Render single field | | renderItem | (index) => Record<string, ReactElement> | Render all fields for item |


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) | | arrays | Record<string, TemplateArrayField> | Array field definitions (use with useArrayField) | | 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 | | FieldWrapper | ComponentType | Registered FieldWrapper component | | FieldsWrapper | ComponentType | Registered FieldsWrapper component |

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>

Hook-Based Field Access with useTemplateField

When you need to access fields from child components (not inline in the render function), use the useTemplateField hook. It returns the pre-rendered field element and marks it as accessed, so it won't appear in remaining fields.

import { FormGenerator, useTemplateField, RemainingFields } from '@connect-soft/form-generator';

// Child component that claims a field
function EmailSection() {
  const email = useTemplateField('email');
  return <div className="highlighted">{email}</div>;
}

function PasswordSection() {
  const password = useTemplateField('password');
  return <div className="special">{password}</div>;
}

<FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
  {({ buttons }) => (
    <div>
      <EmailSection />
      <PasswordSection />
      <div className="other-fields">
        <RemainingFields />
      </div>
      {buttons.submit}
    </div>
  )}
</FormGenerator>

Why not just use fields.remaining with hooks? The render function runs before child components render, so fields.remaining is evaluated before any hooks in child components mark fields as accessed. <RemainingFields /> is a component that evaluates at its own render time — after sibling components above it have already claimed their fields.

You can freely mix proxy access in the render function with hook access in child components:

<FormGenerator fields={fieldDefinitions} onSubmit={handleSubmit}>
  {({ fields, buttons }) => (
    <div>
      {/* Proxy access in the render function */}
      <div className="hero">{fields.email}</div>
      {/* Hook access in a child component */}
      <PasswordSection />
      {/* RemainingFields excludes both email and password */}
      <RemainingFields />
      {buttons.submit}
    </div>
  )}
</FormGenerator>

useTemplateField

| Parameter | Type | Description | |-----------|------|-------------| | name | string | Field name to retrieve | | Returns | ReactElement \| undefined | The pre-rendered field element, or undefined if not found |

RemainingFields

A component that renders all fields not yet accessed via useTemplateField or the fields proxy. Must be used within a FormGenerator custom layout.

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>

Raw Field Props with useFieldProps

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.

import { FormGenerator, useFieldProps } from '@connect-soft/form-generator';

const fields = [
  { type: 'text', name: 'email', label: 'Email', required: true },
  { type: 'password', name: 'password', label: 'Password' },
] as const;

// Custom component using the hook
function CustomEmailField() {
  const { value, onChange, onBlur, ref, field, fieldState } = useFieldProps<string>('email');

  return (
    <div className="my-custom-field">
      <label>{field.label}{field.required && ' *'}</label>
      <input
        ref={ref}
        type="email"
        value={value ?? ''}
        onChange={(e) => onChange(e.target.value)}
        onBlur={onBlur}
        placeholder={field.placeholder}
      />
      {fieldState.error && (
        <span className="error">{fieldState.error.message}</span>
      )}
    </div>
  );
}

// Use in FormGenerator with custom layout
<FormGenerator fields={fields} onSubmit={handleSubmit}>
  {({ fields, buttons }) => (
    <div>
      <CustomEmailField />
      {fields.password}  {/* Mix with pre-rendered fields */}
      {buttons.submit}
    </div>
  )}
</FormGenerator>

useFieldProps Return Value

| Property | Type | Description | |----------|------|-------------| | name | string | Field name (with any prefix applied) | | value | TValue | Current field value | | onChange | (value: TValue) => void | Change handler | | onBlur | () => void | Blur handler | | ref | Ref<any> | Ref to attach to input element | | field | BaseField | Full field definition (type, label, required, etc.) | | fieldState | FieldState | Validation state (see below) |

FieldState Properties

| Property | Type | Description | |----------|------|-------------| | invalid | boolean | Whether the field has validation errors | | error | { type: string; message?: string } | Error details if invalid | | isDirty | boolean | Whether the value has changed from default | | isTouched | boolean | Whether the field has been focused and blurred |

When to Use useFieldProps vs Registered Components

| Use Case | Approach | |----------|----------| | Reusable field component across forms | Register with registerField | | One-off custom field in a specific form | Use useFieldProps | | Need full control over a single field | Use useFieldProps | | Consistent field styling across app | Register with registerField |

Example: Complete Custom Form

import { FormGenerator, useFieldProps } from '@connect-soft/form-generator';

const fields = [
  { type: 'text', name: 'firstName', label: 'First Name', required: true },
  { type: 'text', name: 'lastName', label: 'Last Name', required: true },
  { type: 'email', name: 'email', label: 'Email', required: true },
] as const;

function NameFields() {
  const firstName = useFieldProps<string>('firstName');
  const lastName = useFieldProps<string>('lastName');

  return (
    <div className="name-row">
      <div className="field">
        <label>{firstName.field.label}</label>
        <input
          ref={firstName.ref}
          value={firstName.value ?? ''}
          onChange={(e) => firstName.onChange(e.target.value)}
          onBlur={firstName.onBlur}
        />
        {firstName.fieldState.error && (
          <span className="error">{firstName.fieldState.error.message}</span>
        )}
      </div>
      <div className="field">
        <label>{lastName.field.label}</label>
        <input
          ref={lastName.ref}
          value={lastName.value ?? ''}
          onChange={(e) => lastName.onChange(e.target.value)}
          onBlur={lastName.onBlur}
        />
        {lastName.fieldState.error && (
          <span className="error">{lastName.fieldState.error.message}</span>
        )}
      </div>
    </div>
  );
}

function MyForm() {
  return (
    <FormGenerator fields={fields} onSubmit={handleSubmit}>
      {({ fields, buttons, isSubmitting }) => (
        <div className="custom-form">
          <NameFields />
          {fields.email}
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Submitting...' : 'Submit'}
          </button>
        </div>
      )}
    </FormGenerator>
  );
}

Type-Safe Forms with StrictFormGenerator

For maximum type safety, use StrictFormGenerator which requires a Zod schema. This ensures field names match your schema and provides fully typed form values.

import { StrictFormGenerator } from '@connect-soft/form-generator';
import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  age: z.number().min(18, 'Must be 18 or older'),
});

<StrictFormGenerator
  schema={userSchema}
  fields={[
    { type: 'email', name: 'email', label: 'Email' },      // name must be keyof schema
    { type: 'password', name: 'password', label: 'Password' },
    { type: 'number', name: 'age', label: 'Age' },
    // { type: 'text', name: 'invalid' }  // TypeScript error: 'invalid' not in schema
  ]}
  onSubmit={(values) => {
    // values: { email: string; password: string; age: number }
    console.log(values.email);  // Fully typed!
  }}
/>

Typed Field Helpers

Use helper functions for even stricter type checking:

import { StrictFormGenerator, createFieldFactory } from '@connect-soft/form-generator';
import { z } from 'zod';

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string(),
  rememberMe: z.boolean(),
});

// Create a typed field factory from schema
const defineField = createFieldFactory(loginSchema);

// Each field is validated against the schema
const fields = [
  defineField({ type: 'email', name: 'email', label: 'Email' }),
  defineField({ type: 'password', name: 'password', label: 'Password' }),
  defineField({ type: 'checkbox', name: 'rememberMe', label: 'Remember Me' }),
];

<StrictFormGenerator
  schema={loginSchema}
  fields={fields}
  onSubmit={handleSubmit}
/>

StrictFormGenerator vs FormGenerator

| Feature | FormGenerator | StrictFormGenerator | |---------|---------------|---------------------| | Schema | No (infers from fields) | Required (Zod) | | Field name checking | Inferred from fields | Enforced at compile-time | | Type inference | From field definitions | From Zod schema | | Constraint detection | No | Yes (automatic) | | Use case | Quick prototyping | Production apps |

Automatic Schema Constraint Detection

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.

Supported Constraints

Number fields (z.number()): | Zod Method | Field Property | Example | |------------|----------------|---------| | .min(n) | min | z.number().min(0){ min: 0 } | | .max(n) | max | z.number().max(100){ max: 100 } | | .int() | step: 1 | z.number().int(){ step: 1 } | | .multipleOf(n) | step | z.number().multipleOf(0.01){ step: 0.01 } | | .positive() | min | z.number().positive(){ min: 0 } (exclusive) | | .nonnegative() | min: 0 | z.number().nonnegative(){ min: 0 } |

String fields (z.string()): | Zod Method | Field Property | Example | |------------|----------------|---------| | .min(n) | minLength | z.string().min(3){ minLength: 3 } | | .max(n) | maxLength | z.string().max(100){ maxLength: 100 } | | .length(n) | minLength + maxLength | z.string().length(6){ minLength: 6, maxLength: 6 } | | .regex(pattern) | pattern | z.string().regex(/^[A-Z]+$/){ pattern: '^[A-Z]+$' } |

Date fields (z.date()): | Zod Method | Field Property | Example | |------------|----------------|---------| | .min(date) | min (ISO string) | z.date().min(new Date('2020-01-01')){ min: '2020-01-01' } | | .max(date) | max (ISO string) | z.date().max(new Date('2030-12-31')){ max: '2030-12-31' } |

Example

import { StrictFormGenerator } from '@connect-soft/form-generator';
import { z } from 'zod';

const userSchema = z.object({
  username: z.string().min(3).max(20).regex(/^[a-z0-9_]+$/),
  age: z.number().int().min(18).max(120),
  price: z.number().multipleOf(0.01).min(0),
  birthDate: z.date().min(new Date('1900-01-01')).max(new Date()),
});

// No need to specify min/max/minLength/maxLength in fields!
// They are automatically extracted from the schema
<StrictFormGenerator
  schema={userSchema}
  fields={[
    { type: 'text', name: 'username', label: 'Username' },
    { type: 'number', name: 'age', label: 'Age' },
    { type: 'number', name: 'price', label: 'Price' },
    { type: 'date', name: 'birthDate', label: 'Birth Date' },
  ]}
  onSubmit={handleSubmit}
/>

// Field components receive:
// username: { minLength: 3, maxLength: 20, pattern: '^[a-z0-9_]+$', required: true }
// age: { min: 18, max: 120, step: 1, required: true }
// price: { min: 0, step: 0.01, required: true }
// birthDate: { min: '1900-01-01', max: '2026-02-02', required: true }

Using Constraints in Field Components

Your registered field components can use these constraints directly:

registerField('number', ({ field, formField, fieldState }) => (
  <div>
    <label>{field.label}</label>
    <input
      type="number"
      {...formField}
      min={field.min}
      max={field.max}
      step={field.step}
    />
    {fieldState.error && <span>{fieldState.error.message}</span>}
  </div>
));

registerField('text', ({ field, formField, fieldState }) => (
  <div>
    <label>{field.label}</label>
    <input
      type="text"
      {...formField}
      minLength={field.minLength}
      maxLength={field.maxLength}
      pattern={field.pattern}
    />
    {fieldState.error && <span>{fieldState.error.message}</span>}
  </div>
));

Manual Constraint Merging

You can also manually merge constraints using the utility functions:

import { mergeSchemaConstraints, analyzeSchema } from '@connect-soft/form-generator';

const schema = z.object({
  age: z.number().min(0).max(120),
  name: z.string().min(1).max(100),
});

// Analyze schema to get field info
const fieldInfo = analyzeSchema(schema);
// => [
//   { name: 'age', type: 'number', required: true, min: 0, max: 120 },
//   { name: 'name', type: 'string', required: true, minLength: 1, maxLength: 100 }
// ]

// Or merge constraints into existing fields
const fields = [
  { type: 'number', name: 'age', label: 'Age' },
  { type: 'text', name: 'name', label: 'Name' },
];

const fieldsWithConstraints = mergeSchemaConstraints(schema, fields);
// => [
//   { type: 'number', name: 'age', label: 'Age', required: true, min: 0, max: 120 },
//   { type: 'text', name: 'name', label: 'Name', required: true, minLength: 1, maxLength: 100 }
// ]

Available Helpers

| Helper | Description | |--------|-------------| | createFieldFactory(schema) | Create a field factory for a schema | | typedField<typeof schema>() | Create a single typed field | | typedFields<typeof schema>([...]) | Create an array of typed fields |


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,
    });
  };

  // Set values without marking the form as dirty
  const handleLoadData = () => {
    formRef.current?.setValues(
      { email: '[email protected]', age: 30 },
      { shouldDirty: false }
    );
  };

  // Set values and make them the new baseline for isDirty
  const handleSetDefaults = () => {
    formRef.current?.setDefaultValues({
      email: '[email protected]',
      age: 25,
    });
    // isDirty is now false — these are the new default values
  };

  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>
      <button type="button" onClick={handleLoadData}>Load Data</button>
      <button type="button" onClick={handleSetDefaults}>Set Defaults</button>
    </>
  );
}

Available Ref Methods

| Method | Description | |--------|-------------| | setValues(values, options?) | Set form values (partial update). Options: { shouldDirty?, shouldValidate? } | | getValues() | Get current form values | | reset(values?) | Reset to default or provided values | | setDefaultValues(values) | Set values and make them the new baseline for isDirty | | 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 |

SetValuesOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | shouldDirty | boolean | true | Whether setting the value marks the field as dirty | | shouldValidate | boolean | true | Whether to trigger validation after setting values |

setDefaultValues vs reset vs setValues

| Method | Sets values | Resets dirty state | New dirty baseline | |--------|-------------|--------------------|--------------------| | setValues(v) | Yes | No (marks dirty) | No | | setValues(v, { shouldDirty: false }) | Yes | No (keeps current) | No | | reset(v) | Yes | Yes | Yes (merges with original defaults) | | setDefaultValues(v) | Yes | Yes | Yes (merges with original defaults) |

Use setDefaultValues when loading data from an API and you want the loaded values to become the "clean" baseline (e.g., editing an existing record). Use setValues with { shouldDirty: false } when you want to set a value without affecting dirty tracking at all.

Watching Form Values

Detect when field values change from the parent component:

import { useRef, useEffect } from 'react';
import { FormGenerator, FormGeneratorRef } from '@connect-soft/form-generator';

function MyForm() {
  const formRef = useRef<FormGeneratorRef>(null);

  useEffect(() => {
    // Watch all fields for changes
    const subscription = formRef.current?.form.watch((values, { name, type }) => {
      console.log('Changed field:', name);
      console.log('New values:', values);
    });

    return () => subscription?.unsubscribe();
  }, []);

  return (
    <FormGenerator
      ref={formRef}
      fields={fields}
      onSubmit={handleSubmit}
    />
  );
}

Or use useWatch inside a custom layout:

import { FormGenerator, useWatch } from '@connect-soft/form-generator';

function ValueWatcher() {
  const email = useWatch({ name: 'email' }); // Watch specific field

  useEffect(() => {
    console.log('Email changed:', email);
  }, [email]);

  return null;
}

<FormGenerator fields={fields} onSubmit={handleSubmit}>
  {({ fields, buttons }) => (
    <>
      {fields.all}
      <ValueWatcher />
      {buttons.submit}
    </>
  )}
</FormGenerator>

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 | | validateTypes | boolean \| ValidateTypesOptions | false | Runtime validation of field types |

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 |

Field Type-Specific Properties

Text fields (text, email, password, tel, url, search): | Property | Type | Description | |----------|------|-------------| | placeholder | string | Placeholder text | | minLength | number | Minimum character length | | maxLength | number | Maximum character length | | pattern | string | HTML5 validation pattern |

Number fields (number, range): | Property | Type | Description | |----------|------|-------------| | placeholder | string | Placeholder text | | min | number | Minimum value | | max | number | Maximum value | | step | number | Step increment |

Date fields (date, datetime, datetime-local): | Property | Type | Description | |----------|------|-------------| | min | string | Minimum date (ISO format: YYYY-MM-DD) | | max | string | Maximum date (ISO format: YYYY-MM-DD) |

Schema Analysis Utilities

| Function | Description | |----------|-------------| | analyzeSchema(schema) | Extract detailed field info from Zod schema | | mergeSchemaConstraints(schema, fields) | Merge schema constraints into field definitions | | mergeSchemaRequirements(schema, fields) | Merge only required status (legacy) | | getNumberConstraints(schema) | Extract min/max/step from number schema | | getStringConstraints(schema) | Extract minLength/maxLength/pattern from string schema | | getDateConstraints(schema) | Extract min/max dates from date schema | | isSchemaRequired(schema) | Check if schema field is required | | unwrapSchema(schema) | Unwrap optional/nullable wrappers | | getSchemaTypeName(schema) | Get base type name (string, number, etc.) |


Links


License

ISC © Connect Soft