Skip to main content

Form Builder

The Form Builder is a powerful, framework-agnostic library for building, validating, and managing complex forms with support for multi-step workflows, field validation, custom middleware, and reactive updates.

Key Features

  • Zero-Dependency Core - Pure TypeScript, works everywhere
  • Type-Safe - Full TypeScript support with inference
  • Validation - Built-in validators + Yup/Zod schema integration
  • Multi-Step Forms - Native support for wizard-like forms
  • Field Dependencies - Conditional fields and dynamic behavior
  • Custom Middleware - Hook into form lifecycle
  • Reactive Updates - RxJS Observable-based state management
  • React Hooks - useForm, useField, useFormStep for React

Installation

# Install the form builder package
npm install @codella-software/utils

# Or from JSR
npx jsr add @codella-software/utils

Quick Start

Basic Form (Core)

import { FormBuilder } from '@codella-software/utils';

// Create a form with fields
const form = new FormBuilder({
fields: {
email: {
value: '',
validate: [(value) => {
if (!value.includes('@')) return 'Invalid email';
return null;
}]
},
password: {
value: '',
validate: [(value) => {
if (value.length < 8) return 'Min 8 characters';
return null;
}]
}
}
});

// Subscribe to form value changes
form.value$.subscribe(values => {
console.log('Form values:', values);
});

// Update field value
form.setFieldValue('email', 'user@example.com');

// Get form state
const state = form.state$.value;
console.log('Valid:', state.isValid);
console.log('Errors:', state.errors);

// Handle submission
form.submit$.subscribe(values => {
console.log('Form submitted with:', values);
});

Basic Form (React)

import { useForm } from '@codella-software/react';

export function LoginForm() {
const { values, errors, register, submit, isSubmitting } = useForm({
initialValues: { email: '', password: '' },
onSubmit: async (values) => {
// Handle form submission
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(values)
});
return response.json();
}
});

return (
<form onSubmit={submit}>
<input
{...register('email')}
type="email"
placeholder="Email"
/>
{errors.email && <span>{errors.email}</span>}

<input
{...register('password')}
type="password"
placeholder="Password"
/>
{errors.password && <span>{errors.password}</span>}

<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}

Form with Yup Validation

Integrate with Yup for schema-based validation:

import { FormBuilder } from '@codella-software/utils';
import * as yup from 'yup';

// Define validation schema
const registrationSchema = yup.object({
email: yup
.string()
.email('Invalid email')
.required('Email is required'),
password: yup
.string()
.min(8, 'Password must be at least 8 characters')
.required('Password is required'),
confirmPassword: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match')
.required('Confirm password is required'),
firstName: yup.string().required('First name is required'),
lastName: yup.string().required('Last name is required'),
acceptTerms: yup
.boolean()
.oneOf([true], 'You must accept the terms')
});

// Create form with schema
const form = new FormBuilder({
fields: {
email: { value: '' },
password: { value: '' },
confirmPassword: { value: '' },
firstName: { value: '' },
lastName: { value: '' },
acceptTerms: { value: false }
},
validationSchema: registrationSchema
});

Multi-Step Forms

Build wizard-like forms with multiple steps:

import { useForm, useFormStep } from '@codella-software/react';

export function RegistrationWizard() {
const { values, setValues, submit } = useForm({
initialValues: {
email: '',
password: '',
firstName: '',
lastName: '',
company: '',
jobTitle: ''
},
onSubmit: async (values) => {
await registerUser(values);
}
});

const { currentStep, nextStep, prevStep, goToStep } = useFormStep({
steps: [
{ name: 'Account', fields: ['email', 'password'] },
{ name: 'Personal', fields: ['firstName', 'lastName'] },
{ name: 'Company', fields: ['company', 'jobTitle'] },
{ name: 'Review', fields: [] }
]
});

const renderStep = () => {
switch (currentStep.name) {
case 'Account':
return (
<>
<input
value={values.email}
onChange={(e) => setValues({ ...values, email: e.target.value })}
placeholder="Email"
/>
<input
value={values.password}
type="password"
onChange={(e) => setValues({ ...values, password: e.target.value })}
placeholder="Password"
/>
</>
);
case 'Personal':
return (
<>
<input
value={values.firstName}
onChange={(e) => setValues({ ...values, firstName: e.target.value })}
placeholder="First Name"
/>
<input
value={values.lastName}
onChange={(e) => setValues({ ...values, lastName: e.target.value })}
placeholder="Last Name"
/>
</>
);
case 'Company':
return (
<>
<input
value={values.company}
onChange={(e) => setValues({ ...values, company: e.target.value })}
placeholder="Company"
/>
<input
value={values.jobTitle}
onChange={(e) => setValues({ ...values, jobTitle: e.target.value })}
placeholder="Job Title"
/>
</>
);
case 'Review':
return (
<div>
<h3>Review Your Information</h3>
<p><strong>Email:</strong> {values.email}</p>
<p><strong>Name:</strong> {values.firstName} {values.lastName}</p>
<p><strong>Company:</strong> {values.company}</p>
<p><strong>Job Title:</strong> {values.jobTitle}</p>
</div>
);
default:
return null;
}
};

return (
<div>
<div className="progress-bar">
{currentStep.index + 1} of {4}
</div>

{renderStep()}

<div className="buttons">
{currentStep.index > 0 && (
<button onClick={prevStep}>Previous</button>
)}
{currentStep.index < 3 && (
<button onClick={nextStep}>Next</button>
)}
{currentStep.index === 3 && (
<button onClick={submit}>Submit</button>
)}
</div>
</div>
);
}

Field Types Reference

Text Input

{
email: {
value: '',
validate: [(value) => {
if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
return 'Invalid email format';
}
return null;
}]
}
}

Checkbox

{
acceptTerms: {
value: false
},
subscribe: {
value: true
}
}

Select/Dropdown

{
country: {
value: '',
options: [
{ label: 'United States', value: 'us' },
{ label: 'Canada', value: 'ca' },
{ label: 'Mexico', value: 'mx' }
]
}
}

Radio Group

{
deliveryMethod: {
value: 'standard',
options: [
{ label: 'Standard (5-7 days)', value: 'standard' },
{ label: 'Express (2-3 days)', value: 'express' },
{ label: 'Overnight', value: 'overnight' }
]
}
}

Textarea

{
message: {
value: '',
validate: [(value) => {
if (value.length > 500) return 'Max 500 characters';
return null;
}]
}
}

Field Dependencies

Create conditional fields that appear based on other field values:

import { useForm, useFieldDependency } from '@codella-software/react';

export function ShippingForm() {
const { values, setValues, register } = useForm({
initialValues: {
country: '',
state: '',
zipCode: '',
requiresInternational: false,
internationalNotification: ''
}
});

// Show state field only for US
const showStateField = useFieldDependency(
values.country === 'us'
);

// Show international field only when required
const showInternationalField = useFieldDependency(
values.requiresInternational === true
);

return (
<form>
<select {...register('country')}>
<option value="">Select Country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="other">Other</option>
</select>

{showStateField && (
<select {...register('state')}>
<option value="">Select State</option>
<option value="ca">California</option>
<option value="ny">New York</option>
<option value="tx">Texas</option>
</select>
)}

<input {...register('zipCode')} placeholder="ZIP Code" />

<label>
<input {...register('requiresInternational')} type="checkbox" />
Requires International Shipping
</label>

{showInternationalField && (
<textarea
{...register('internationalNotification')}
placeholder="Special instructions for international delivery"
/>
)}
</form>
);
}

Form Submission

Handle form submissions with validation and async operations:

import { useForm } from '@codella-software/react';

export function ContactForm() {
const {
values,
errors,
register,
submit,
isSubmitting,
submitError
} = useForm({
initialValues: {
name: '',
email: '',
message: ''
},
onSubmit: async (values) => {
// This runs only after validation passes
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
});

if (!response.ok) {
throw new Error('Failed to send message');
}

return { success: true };
} catch (error) {
throw new Error('Failed to send message. Please try again.');
}
},
validationSchema: yup.object({
name: yup.string().required('Name is required'),
email: yup.string().email().required('Email is required'),
message: yup.string().required('Message is required').min(10)
})
});

return (
<form onSubmit={submit}>
<div>
<input {...register('name')} placeholder="Your Name" />
{errors.name && <span className="error">{errors.name}</span>}
</div>

<div>
<input {...register('email')} type="email" placeholder="Email" />
{errors.email && <span className="error">{errors.email}</span>}
</div>

<div>
<textarea {...register('message')} placeholder="Message" />
{errors.message && <span className="error">{errors.message}</span>}
</div>

{submitError && <span className="error">{submitError}</span>}

<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
}

Advanced Features

Custom Validators

// Function-based validator
const minLengthValidator = (min: number) => (value: string) => {
if (value.length < min) {
return `Minimum ${min} characters required`;
}
return null;
};

// Async validator
const uniqueEmailValidator = async (value: string) => {
const exists = await checkEmailExists(value);
if (exists) {
return 'Email already registered';
}
return null;
};

const form = new FormBuilder({
fields: {
email: {
value: '',
validate: [uniqueEmailValidator]
},
password: {
value: '',
validate: [minLengthValidator(8)]
}
}
});

Form Middleware

Hook into form lifecycle with middleware:

import { FormBuilder } from '@codella-software/utils';

const loggingMiddleware = (form: FormBuilder) => {
form.value$.subscribe(values => {
console.log('Form changed:', values);
});

form.state$.subscribe(state => {
console.log('Form state:', state);
});
};

const persistMiddleware = (form: FormBuilder) => {
form.value$.subscribe(values => {
localStorage.setItem('formData', JSON.stringify(values));
});
};

const form = new FormBuilder({
fields: { /* ... */ },
middleware: [loggingMiddleware, persistMiddleware]
});

Field Masking

Apply masks to fields for formatted input:

import { useForm, useFieldMask } from '@codella-software/react';

export function PhoneForm() {
const { register, values } = useForm({
initialValues: { phone: '' }
});

// Apply phone number mask: (XXX) XXX-XXXX
const phoneMask = useFieldMask('(###) ###-####');

return (
<input
{...register('phone')}
{...phoneMask}
placeholder="(555) 123-4567"
/>
);
}

Dynamic Fields

Add and remove fields dynamically:

import { useForm } from '@codella-software/react';

export function DynamicFieldsForm() {
const { values, addField, removeField, register } = useForm({
initialValues: {
email: '',
phones: ['']
}
});

const addPhoneField = () => {
addField('phones', '');
};

const removePhoneField = (index: number) => {
removeField('phones', index);
};

return (
<form>
<input {...register('email')} placeholder="Email" />

<h3>Phone Numbers</h3>
{values.phones.map((_, index) => (
<div key={index}>
<input
{...register(`phones.${index}`)}
placeholder="Phone"
/>
<button onClick={() => removePhoneField(index)}>Remove</button>
</div>
))}

<button onClick={addPhoneField}>Add Phone</button>
</form>
);
}

Comparison with Other Libraries

FeatureFormBuilderReact Hook FormFormik
Core SizeTiny (4KB)Small (9KB)Large (35KB)
Framework-Agnostic✅ Yes❌ React only❌ React only
Zero Dependencies✅ Yes✅ Yes❌ Yup/Joi
RxJS Observable✅ Yes❌ No❌ No
TypeScript✅ Full✅ Full✅ Full
Multi-Step✅ Built-in❌ Manual❌ Manual
Field Dependencies✅ Native❌ Manual❌ Manual

Next Steps