Skip to content

binaryjack/formular.dev

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

335 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

formular.dev

Form Creation Validation Bundle Size Performance Tests Version TypeScript

The only form library you'll ever need.
Framework-agnostic โ€ข Schema-first โ€ข Type-safe โ€ข Enterprise-ready

๐Ÿš€ Quick Start โ€ข ๐Ÿ“– Schema API โ€ข ๐ŸŽฏ Simple API โ€ข ๐Ÿ“š Documentation


What's New in v2.0

  • โœจ 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

Why formular.dev?

๐ŸŽฏ True Framework Agnostic

Works seamlessly with React, Vue, Angular, and vanilla JavaScript using the same API. No framework lock-in, ever.

โšก Production-Ready Performance

  • 60-80ms to create 100-field forms
  • 30ms validation with intelligent caching
  • 45KB core bundle (12KB gzipped)
  • Zero runtime dependencies

๐ŸŒ Enterprise Features Built-In

  • 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

๐Ÿ“Š Competitive Advantage

Feature formular.dev v2.0 React Hook Form Formik TanStack Form
Schema system โœ… Built-in โš ๏ธ External โš ๏ธ External โš ๏ธ External
Type inference โœ… Automatic โŒ Manual โŒ Manual โœ… Valibot
Framework support โœ… All (same API) โŒ React only โŒ React only โš ๏ธ Adapters
Built-in i18n โœ… 6 languages โŒ โŒ โŒ
Country validators โœ… 12+ countries โŒ โŒ โŒ
Zero dependencies โœ… โœ… โŒ Lodash etc โš ๏ธ Optional
Bundle size 45KB (12KB gz) ~8KB ~30KB ~15-20KB

Features

  • ๐Ÿš€ 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

Installation

npm install formular.dev
# or
pnpm add formular.dev
# or
yarn add formular.dev

Quick Start

Simple API (createForm)

The 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()

Traditional API (Service Manager)

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')

Schema System

Basic Types

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')

String Validators

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 uppercase

Number Validators

f.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

Country-Specific Validators

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

Complex Types

// 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())

Optional & Nullable

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

Default Values

const schema = f.object({
    role: f.string().default('user'),
    active: f.boolean().default(true),
    count: f.number().default(0)
})

Transforms

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' })
})

Custom Refinements

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' })

Form API

Creating Forms

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

Validation

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

Form State

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

Submission

// Submit form
const result = await form.submit()
if (result) {
    console.log('Form submitted:', result)
}

Form Presets

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 form

Custom Presets

import { 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'
        }
    }
})

Submission Strategies

Control submission behavior for different contexts:

Direct Strategy (Default)

import { createForm, DirectSubmissionStrategy } from 'formular.dev'

const form = createForm({
    schema: userSchema,
    submissionStrategy: new DirectSubmissionStrategy(async (data) => await api.post('/users', data))
})

Context Strategy (For Form Providers)

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()
            }
        }
    )
})

Type Inference

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

Schema Composition

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

Error Handling

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

Traditional API (Advanced)

Traditional API (Advanced)

For advanced use cases with IoC container and custom services:

Service Manager

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()

Form Manager

Form Manager

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')

Built-in Validators

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')

Multilingual Validation

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

Country-Specific Validators (Traditional API)

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')

Available Validators

Available Validators

formular.dev includes 18+ built-in validators plus country-specific validators:

Basic Validators:

  • email - Email validation
  • phone - Phone number validation
  • firstName, lastName, fullName - Name validation
  • passwordStrong, passwordMedium - Password strength validation
  • url - URL validation
  • creditCard - Credit card validation
  • postalCode - Postal/ZIP code validation
  • ssn - Social security number validation
  • currency - Currency validation
  • age - Age range validation
  • username - Username validation
  • time - Time format validation
  • numeric - Numeric value validation
  • date - 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')


Internationalization (i18n)

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!


Integration with Pulsar UI

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

Documentation

Comprehensive Guides

Examples in Codebase

Comprehensive examples are available in the source code:


Performance

  • 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

Main Exports

Simple API (v2.0)

// Form creation
import { createForm, createFormFromPreset, f } from 'formular.dev'

// Submission strategies
import { DirectSubmissionStrategy, ContextSubmissionStrategy } from 'formular.dev'

// Error handling
import { SchemaValidationError } from 'formular.dev'

Traditional API

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

Architecture

formular.dev v2.0 uses a channel-based message bus architecture for optimal field isolation and memory efficiency:

Key Benefits

  • 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 ObservableSubject with weak/strong reference support
  • Standard Patterns - Familiar pub-sub pattern
  • Backward Compatible - Supports both legacy and channel-based APIs

Channel-Based Messaging

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


Dependencies

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.


License

MIT ยฉ 2025 Piana Tadeo

About

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors