The only form library you'll ever need.
Framework-agnostic โข Schema-first โข Type-safe โข Enterprise-ready
๐ Quick Start โข ๐ Schema API โข ๐ฏ Simple API โข ๐ Documentation
- โจ Schema-first API - Inspired by Zod but optimized for forms
- ๐ Full type inference - TypeScript types automatically derived from schemas
- ๐ซ No magic strings - Type-safe everything (events, validators, field types)
- ๐ฏ Simple API - One-line form creation with
createForm() - ๐ Enhanced i18n - Country-specific validators (phone, postal codes, SSN)
- ๐ฆ Submission strategies - Flexible handling for different contexts
- โก Performance - Sub-100ms for 100-field forms
Works seamlessly with React, Vue, Angular, and vanilla JavaScript using the same API. No framework lock-in, ever.
- 60-80ms to create 100-field forms
- 30ms validation with intelligent caching
- 45KB core bundle (12KB gzipped)
- Zero runtime dependencies
- 6 languages - English, French, Spanish, German, Portuguese, Italian
- 12+ country formats - Phone, postal codes, SSN validation
- IoC Container - Dependency injection for testability
- Full TypeScript - Complete type safety
| Feature | formular.dev v2.0 | React Hook Form | Formik | TanStack Form |
|---|---|---|---|---|
| Schema system | โ Built-in | |||
| Type inference | โ Automatic | โ Manual | โ Manual | โ Valibot |
| Framework support | โ All (same API) | โ React only | โ React only | |
| Built-in i18n | โ 6 languages | โ | โ | โ |
| Country validators | โ 12+ countries | โ | โ | โ |
| Zero dependencies | โ | โ | โ Lodash etc | |
| Bundle size | 45KB (12KB gz) | ~8KB | ~30KB | ~15-20KB |
- ๐ Framework Agnostic - Works with React, Vue, Angular, or vanilla JavaScript
- ๐ Schema-First - Define once, type-check everywhere with automatic inference
- โ Advanced Validation - 18+ built-in validators + custom validators
- ๐ Multilingual - Built-in translations for 6 languages (EN, FR, ES, DE, PT, IT)
- โก High Performance - Optimized validation caching and parallel processing
- ๐ฏ Type Safe - Full TypeScript support with comprehensive type definitions
- ๐ง IoC Container - Flexible dependency injection system
- ๐ Multi-Country - Phone, postal, SSN validation for 12+ countries
- ๐จ Form Presets - Common patterns (login, signup, contact, etc.) ready to use
- ๐ฆ Submission Strategies - Flexible handling for different contexts
npm install formular.dev
# or
pnpm add formular.dev
# or
yarn add formular.devThe easiest way to create forms in v2.0:
import { createForm, f } from 'formular.dev'
// Define schema with full type inference
const userSchema = f.object({
email: f.string().email().nonempty(),
age: f.number().min(18).max(100),
country: f.enum(['US', 'UK', 'FR', 'DE', 'CH'])
})
// TypeScript infers: { email: string, age: number, country: 'US' | 'UK' | ... }
type User = f.infer<typeof userSchema>
// Create form (one line!)
const form = createForm({
schema: userSchema,
onSubmit: async (data) => {
await api.post('/users', data) // data is fully typed!
},
onSuccess: (response) => console.log('Success!', response),
onError: (error) => console.error('Failed:', error)
})
// Submit
await form.submit()For advanced scenarios with IoC container:
import { SetupHelpers, FormularManager } from 'formular.dev'
// Initialize service manager
const serviceManager = SetupHelpers.forFormApplication()
// Create form manager
const formularManager = serviceManager.resolve<FormularManager>(Symbol.for('IFormularManager'))
// Create form from descriptors
const form = formularManager.createFromDescriptors('user-form', [
{
id: 1,
name: 'email',
label: 'Email Address',
type: 'email',
validation: Validators.email('email')
}
])
// Validate
const isValid = await formularManager.validate('user-form')import { f } from 'formular.dev'
// String
const nameSchema = f.string().min(2).max(50).nonempty().trim()
// Number
const ageSchema = f.number().min(18).max(100).int().positive()
// Boolean
const termsSchema = f.boolean().refine((val) => val === true, { message: 'Must accept terms' })
// Date
const birthDateSchema = f.date().max(new Date())
// Enum
const roleSchema = f.enum(['admin', 'user', 'guest'])
// Literal
const statusSchema = f.literal('active')f.string()
.email() // Email format
.url() // URL format
.min(5) // Min length
.max(100) // Max length
.length(10) // Exact length
.pattern(/^\d+$/) // Regex
.nonempty() // Non-empty string
.trim() // Trim whitespace
.toLowerCase() // Convert to lowercase
.toUpperCase() // Convert to uppercasef.number()
.min(0) // Minimum value
.max(100) // Maximum value
.int() // Integer only
.positive() // > 0
.negative() // < 0
.nonnegative() // >= 0
.nonpositive() // <= 0
.multipleOf(5) // Multiple of value
.finite() // Not Infinity
.safe() // Within safe integer range// Phone numbers (CH, US, UK, FR, DE, IT, ES, CA, AU, JP, NL, BE, AT)
f.string().phone('CH')
// Postal codes
f.string().postalCode('US')
// Swiss AHV (social security)
f.string().ahv()
// Example: Swiss user form
const swissUserSchema = f.object({
email: f.string().email().nonempty(),
phone: f.string().phone('CH'),
postalCode: f.string().postalCode('CH'),
ahv: f.string().ahv()
})// Array
const tagsSchema = f.array(f.string()).min(1).max(10).nonempty()
// Object
const addressSchema = f.object({
street: f.string().nonempty(),
city: f.string().nonempty(),
postalCode: f.string().postalCode('US')
})
// Nested objects
const userSchema = f.object({
name: f.string(),
address: f.object({
street: f.string(),
city: f.string()
})
})
// Union
const statusSchema = f.union(f.literal('active'), f.literal('inactive'), f.literal('pending'))
// Record (key-value pairs)
const preferencesSchema = f.record(f.string(), f.boolean())const schema = f.object({
requiredField: f.string(),
optionalField: f.string().optional(), // string | undefined
nullableField: f.string().nullable(), // string | null
bothField: f.string().optional().nullable() // string | null | undefined
})const schema = f.object({
role: f.string().default('user'),
active: f.boolean().default(true),
count: f.number().default(0)
})const schema = f.object({
email: f.string().trim().toLowerCase().email(),
price: f
.string()
.transform((val) => parseFloat(val))
.refine((val) => val > 0, { message: 'Must be positive' })
})const passwordSchema = f
.string()
.min(8)
.refine((val) => /[A-Z]/.test(val), { message: 'Must contain uppercase' })
.refine((val) => /[0-9]/.test(val), { message: 'Must contain number' })import { createForm, f } from 'formular.dev'
const form = createForm({
schema: f.object({
email: f.string().email(),
password: f.string().min(8)
}),
defaultValues: {
email: '',
password: ''
},
onSubmit: async (data) => {
return await api.login(data)
},
onSuccess: (response, data) => {
console.log('Login successful!', response)
navigate('/dashboard')
},
onError: (error) => {
console.error('Login failed:', error)
toast.error(error.message)
}
})// Validate all fields
const isValid = await form.validateForm()
// Validate single field
form.validateField('email')
// Pre-validate (before blur/change)
const canUpdate = form.preValidateField('email')
// Get errors
const errors = form.getErrors()
// { email: [{ message: 'Invalid email', code: 'invalid_email' }] }// Check form state
form.isValid // All fields valid
form.isDirty // Form modified
form.isBusy // Form submitting
form.submitCount // Number of submissions
// Get/set field values
const email = form.getField('email')?.value
form.updateField('email', 'user@example.com')
// Reset/clear form
form.reset() // Reset to initial values
form.clear() // Clear all values// Submit form
const result = await form.submit()
if (result) {
console.log('Form submitted:', result)
}Common form patterns ready to use:
import { createFormFromPreset } from 'formular.dev'
// Login form
const loginForm = createFormFromPreset('login', {
onSubmit: async (data) => await api.login(data)
})
// Signup form
const signupForm = createFormFromPreset('signup', {
onSubmit: async (data) => await api.signup(data)
})
// Available presets:
// - login - Login form (email, password)
// - signup - Signup form (email, password, confirm)
// - contact - Contact form (name, email, message)
// - profile - Profile form (name, bio, etc.)
// - address - Address form (street, city, postal)
// - payment - Payment form (card details)
// - swiss-user - Swiss-specific user form
// - newsletter - Newsletter subscription
// - search - Search formimport { presetRegistry, f } from 'formular.dev'
presetRegistry.register({
name: 'my-form',
description: 'Custom form preset',
schema: f.object({
customField: f.string().nonempty()
}),
fields: {
customField: {
label: 'Custom Field',
placeholder: 'Enter value'
}
}
})Control submission behavior for different contexts:
import { createForm, DirectSubmissionStrategy } from 'formular.dev'
const form = createForm({
schema: userSchema,
submissionStrategy: new DirectSubmissionStrategy(async (data) => await api.post('/users', data))
})import { createForm, ContextSubmissionStrategy } from 'formular.dev'
const form = createForm({
schema: userSchema,
submissionStrategy: new ContextSubmissionStrategy(
async (data) => await api.post('/users', data),
{
isDismissed: () => userCanceledForm(),
onValidationStart: () => setIsValidating(true),
onValidationComplete: (isValid) => {
setIsValidating(false)
if (!isValid) showErrors()
}
}
)
})const schema = f.object({
email: f.string(),
age: f.number(),
active: f.boolean(),
role: f.enum(['admin', 'user']),
profile: f.object({
name: f.string(),
bio: f.string().optional()
}),
tags: f.array(f.string())
})
// Infer TypeScript type
type User = f.infer<typeof schema>
/*
{
email: string
age: number
active: boolean
role: 'admin' | 'user'
profile: {
name: string
bio?: string
}
tags: string[]
}
*/
// Use with createForm - data is fully typed!
const form = createForm({
schema,
onSubmit: (data: User) => {
data.email // โ
string
data.role // โ
'admin' | 'user'
data.profile.bio // โ
string | undefined
}
})// Base schemas
const baseUserSchema = f.object({
email: f.string().email(),
name: f.string()
})
// Extend
const adminSchema = baseUserSchema.extend({
role: f.literal('admin'),
permissions: f.array(f.string())
})
// Pick specific fields
const loginSchema = baseUserSchema.pick(['email'])
// Omit fields
const publicSchema = baseUserSchema.omit(['email'])
// Make all fields optional
const updateSchema = baseUserSchema.partial()
// Make all fields required
const strictSchema = updateSchema.required()
// Merge schemas
const mergedSchema = schema1.merge(schema2)import { SchemaValidationError } from 'formular.dev'
try {
const result = await form.submit()
} catch (error) {
if (error instanceof SchemaValidationError) {
console.log(error.code) // Error code
console.log(error.path) // Field path
console.log(error.errors) // All validation errors
}
}For advanced use cases with IoC container and custom services:
formular.dev uses an IoC (Inversion of Control) container for dependency injection:
import { SetupHelpers } from 'formular.dev'
// Full-featured setup for form applications
const serviceManager = SetupHelpers.forFormApplication()
// Minimal setup for custom implementations
const minimalSM = SetupHelpers.forCustomImplementation()
// Testing environment setup
const testingSM = SetupHelpers.forTesting()const formularManager = serviceManager.resolve(Symbol.for('IFormularManager'))
// Create form from field descriptors
const form = formularManager.createFromDescriptors('my-form', fieldDescriptors)
// Create form from schema
const schemaForm = formularManager.createFromSchema(entitySchema)
// Create empty form and add fields dynamically
const emptyForm = formularManager.createEmpty('dynamic-form')
// Get form data
const formData = formularManager.getData('my-form')
// Validate specific form
const isValid = await formularManager.validate('my-form')import { Validators } from 'formular.dev'
// Email validation
const emailValidation = Validators.email('email')
// Phone validation
const phoneValidation = Validators.phone('phone')
// Age validation
const ageValidation = Validators.age('age', 18, 100)
// Password validation
const passwordValidation = Validators.passwordStrong('password')import {
createCommonLocalizedValidators,
ValidationLocalizeKeys,
createLocalizedValidator
} from 'formular.dev'
// Create validators with French messages
const localizedValidators = createCommonLocalizedValidators('email', {
locale: 'fr'
})
// Use localized validator
const emailField = {
name: 'email',
validation: localizedValidators.pattern(
/^[^\s@]+@[^\s@]+\.[^\s@]+$/,
ValidationLocalizeKeys.emailError,
ValidationLocalizeKeys.emailGuide
)
}import { phoneCountryValidator, postalCodeCountryValidator, ahvValidator } from 'formular.dev'
// Swiss phone number
const swissPhone = phoneCountryValidator('phone', 'CH')
// German postal code
const germanPostal = postalCodeCountryValidator('postal', 'DE')
// Swiss AHV (social security number)
const ahv = ahvValidator('ahv')formular.dev includes 18+ built-in validators plus country-specific validators:
Basic Validators:
email- Email validationphone- Phone number validationfirstName,lastName,fullName- Name validationpasswordStrong,passwordMedium- Password strength validationurl- URL validationcreditCard- Credit card validationpostalCode- Postal/ZIP code validationssn- Social security number validationcurrency- Currency validationage- Age range validationusername- Username validationtime- Time format validationnumeric- Numeric value validationdate- Date validation
Country-Specific Validators:
Support for 12+ countries including:
- ๐จ๐ญ Switzerland:
phone('CH'),postalCode('CH'),ahv() - ๐บ๐ธ United States:
phone('US'),postalCode('US'),ssn('US') - ๐ฌ๐ง United Kingdom:
phone('UK'),postalCode('UK') - ๐ซ๐ท France:
phone('FR'),postalCode('FR') - ๐ฉ๐ช Germany:
phone('DE'),postalCode('DE') - ๐ฎ๐น Italy:
phone('IT'),postalCode('IT') - ๐ช๐ธ Spain:
phone('ES'),postalCode('ES') - ๐จ๐ฆ Canada:
phone('CA'),postalCode('CA') - ๐ฆ๐บ Australia:
phone('AU'),postalCode('AU') - ๐ฏ๐ต Japan:
phone('JP'),postalCode('JP') - ๐ณ๐ฑ Netherlands:
phone('NL'),postalCode('NL') - ๐ง๐ช Belgium:
phone('BE'),postalCode('BE') - ๐ฆ๐น Austria:
phone('AT'),postalCode('AT')
Built-in support for 6 languages with all translations included:
- ๐ฌ๐ง English (en)
- ๐ซ๐ท French (fr)
- ๐ช๐ธ Spanish (es)
- ๐ฉ๐ช German (de)
- ๐ต๐น Portuguese (pt)
- ๐ฎ๐น Italian (it)
All translations are fully overridable and extensible!
import { createForm, f, ContextSubmissionStrategy } from 'formular.dev'
import { FormProvider } from '@pulsar-framework/pulsar-formular-ui'
const userSchema = f.object({
email: f.string().email().nonempty(),
name: f.string().min(2).nonempty()
})
const MyForm = () => {
const [isDismissed, setIsDismissed] = createSignal(false)
const [isValidating, setIsValidating] = createSignal(false)
const form = useMemo(() => createForm({
schema: userSchema,
submissionStrategy: new ContextSubmissionStrategy(
async (data) => await api.post('/users', data),
{
isDismissed: () => isDismissed(),
onValidationStart: () => setIsValidating(true),
onValidationComplete: (isValid) => setIsValidating(false)
}
)
}), [])
return (
<FormProvider
form={form}
data={userData}
onSaveCallback={handleSave}
onQuitCallback={handleQuit}
>
<InputField name="email" />
<InputField name="name" />
</FormProvider>
)
}- Channel-Based Messaging Architecture - Technical implementation of the message bus system
- Comparison with Other Libraries - Side-by-side comparison with React Hook Form, Formik, and TanStack Form
- Field Types UI Guide - Complete guide to available field types and UI components
- Form Context Integration - Integration with form providers and context
- Implementation Summary - Summary of v2.0 implementation details
Comprehensive examples are available in the source code:
- Configuration Manager Examples - 9 detailed examples
- Service Manager Examples - IoC container usage
- Country Validator Demo - 280+ lines of country-specific validation examples
- Test Suite - Extensive test coverage with real-world patterns
- Form Creation: 60-80ms for 100-field forms
- Validation: ~30ms with intelligent caching (40-50% faster)
- Bundle Size: 45KB (12KB gzipped)
- Dependencies: Zero runtime dependencies
- Memory: 40-50% reduction with channel-based architecture
// Form creation
import { createForm, createFormFromPreset, f } from 'formular.dev'
// Submission strategies
import { DirectSubmissionStrategy, ContextSubmissionStrategy } from 'formular.dev'
// Error handling
import { SchemaValidationError } from 'formular.dev'// Service Manager Setup
import { SetupHelpers, ServiceManagerFactory } from 'formular.dev'
// Form Management
import { FormularManager, Formular } from 'formular.dev'
// Validators
import { Validators } from 'formular.dev'
// Localization
import {
createCommonLocalizedValidators,
createLocalizedValidator,
ValidationLocalizeKeys
} from 'formular.dev'
// Schema & Types
import { FieldDescriptor, FieldSchemaBuilder } from 'formular.dev'
import type { IFormularManager, IFormular, IServiceManager } from 'formular.dev'formular.dev v2.0 uses a channel-based message bus architecture for optimal field isolation and memory efficiency:
- Field Isolation - Channel-based routing prevents cross-field contamination
- Memory Efficient - Singleton managers instead of N instances per field (40-50% reduction)
- Observable Pattern - Built on
ObservableSubjectwith weak/strong reference support - Standard Patterns - Familiar pub-sub pattern
- Backward Compatible - Supports both legacy and channel-based APIs
// Field managers subscribe to specific channels
notificationManager.observers.subscribe('field-123', callback, useWeak)
// Triggers only affect subscribers of that channel
notificationManager.observers.trigger('field-123')
// Supports debounced triggers per channel
notificationManager.observers.debounceTrigger('field-123', 300)This architecture enables formular.dev to handle 100+ field forms with sub-100ms rendering while maintaining complete field isolation and type safety.
See Channel-Based Messaging Architecture for more details.
This package has zero runtime dependencies for maximum compatibility and minimal bundle size. Development dependencies include TypeScript and testing tools.
The optional shared-assets package provides logo icons and other shared resources when needed.
MIT ยฉ 2025 Piana Tadeo