An advanced, high-performance, schema-first form management and validation engine for modern TypeScript and JavaScript applications.
This framework is framework-agnostic, type-safe, and optimized for complex enterprise-grade forms with zero runtime dependencies.
- Framework Agnostic: Core business logic and validation run independently of UI frameworks. Works seamlessly with React, Vue, Svelte, Angular, or Vanilla JS.
- Schema-First Design: Define forms using a declarative, fluent schema builder (similar to Zod) and automatically infer complete TypeScript types.
- Performance-First Architecture: Features a channel-based event bus that isolates field updates, minimizing dirty checks and maximizing responsiveness (sub-100ms initialization for 100+ fields, ~30ms validation).
- Inversion of Control (IoC): Built on a robust Dependency Injection container (
ServiceManager), allowing developers to swap or extend core services (validation, configurations, events) seamlessly. - Prototype-Based Classes: Leverages explicit prototype manipulation instead of ES6 class syntax sugar. This yields highly optimized execution speed and minimal bundle footprints.
- Built-in Localization (i18n): Ships with translation assets and localized validators for 6 languages: English, French, Spanish, German, Portuguese, and Italian.
- Country-Specific Validation: Includes out-of-the-box validation rules for 12+ countries, including specialized format checks like Swiss AHV/social security and US SSN.
- Submission Strategies: Extensible submission handling (e.g.
DirectSubmissionStrategy,ContextSubmissionStrategy) to adapt to different runtime contexts and state managers.
Install the package via your preferred package manager:
pnpm add formular.devOr using npm or Yarn:
npm install formular.dev
# or
yarn add formular.devThe Simple API allows you to create fully-functional forms in a single step, hiding the underlying Dependency Injection setup while keeping complete type safety.
import{createForm,f}from'formular.dev';// 1. Define your schema with type-safe constraintsconstsignUpSchema=f.object({username: f.string().min(3).max(30).nonempty(),email: f.string().email().nonempty(),age: f.number().min(18).max(120),acceptTerms: f.boolean().refine((val)=>val===true,{message: 'You must accept the terms and conditions'})});// 2. Infer TypeScript types automatically from the schematypeSignUpData=f.infer<typeofsignUpSchema>;// 3. Create the form instance (asynchronous operation)constform=awaitcreateForm({schema: signUpSchema,defaultValues: {username: '',email: '',age: 18,acceptTerms: false},onSubmit: async(data: SignUpData)=>{// data is strongly typed hereawaitapi.post('/register',data);},onSuccess: (response,data)=>{console.log('Registration successful:',response);},onError: (error)=>{console.error('Registration failed:',error.message);}});// 4. Introspect and update values programmaticallyform.updateField('username','alex_dev');console.log(form.getField('username')?.value);// 'alex_dev'// 5. Run form submission (triggers schema validation automatically)constsuccess=awaitform.submit();if(success){console.log('Form validated and submitted successfully');}// 6. Clean up resources and dispose of the IoC containerform.destroy();The schema builder f provides a fluent, zero-dependency interface to build complex validation structures.
f.string(): Matches string values.f.number(): Matches numeric values.f.boolean(): Matches boolean values.f.date(): Matches Date objects.f.literal(value): Matches an exact literal value (string, number, or boolean).f.enum([val1, val2]): Matches any value in the provided read-only list.f.array(schema): Matches an array of elements conforming to the child schema.f.object(shape): Matches an object conforming to the given shape.f.union(schema1, schema2): Matches any of the provided sub-schemas.f.record(keySchema, valueSchema): Matches an object map of key-value pairs.
Every schema type supports standard modifiers:
.optional(): Allowsundefinedvalues..nullable(): Allowsnullvalues..default(value): Sets a fallback value when the field is empty or undefined..transform(fn): Transforms the output value..refine(predicate, options): Applies a custom validation function with custom error messages.
f.string().email()// Format check for email.url()// Format check for URL.min(length)// Minimum character length.max(length)// Maximum character length.length(exact)// Exact character length.pattern(regex)// Custom regular expression match.nonempty()// Requires a non-empty string.trim()// Trims whitespace (transformation).toLowerCase()// Converts to lowercase (transformation).toUpperCase();// Converts to uppercase (transformation)f.number().min(value)// Minimum value.max(value)// Maximum value.int()// Requires an integer.positive()// Greater than zero.negative()// Less than zero.nonnegative()// Greater than or equal to zero.nonpositive()// Less than or equal to zero.multipleOf(step)// Must be a multiple of the step value.finite()// Prevents Infinity / -Infinity.safe();// Within Number.MIN_SAFE_INTEGER and MAX_SAFE_INTEGERValidation for country-specific fields is built into the string schema builder:
// Phone number validation (CH, US, UK, FR, DE, IT, ES, CA, AU, JP, NL, BE, AT)f.string().phone('CH');// Postal/ZIP code validationf.string().postalCode('US');// Swiss AHV (social security number) validationf.string().ahv();Pre-configured schemas and layouts for standard forms are registered in the global registry and can be loaded directly:
import{createFormFromPreset}from'formular.dev';constloginForm=awaitcreateFormFromPreset('login',{onSubmit: async(data)=>{awaitauth.login(data.email,data.password);}});| Preset Name | Purpose | Fields Included |
|---|---|---|
login | Standard authentication | email, password, rememberMe |
signup | User registration | name, email, password, confirmPassword, acceptTerms |
contact | Feedback or contact queries | name, email, subject, message |
profile | User profile details | firstName, lastName, email, phone, bio, avatar |
address | Shipping/Billing locations | street, city, postalCode, country |
payment | Credit card credentials | cardNumber, cardHolder, expiryMonth, expiryYear, cvv |
swiss-user | Switzerland-localized profile | firstName, lastName, email, phone, postalCode, ahv |
newsletter | Newsletter subscription | email, preferences |
search | Content querying and filtering | query, category, sortBy, dateFrom, dateTo |
Custom presets can be registered programmatically:
import{presetRegistry,f}from'formular.dev';presetRegistry.register({name: 'custom-preset',description: 'A custom organization form',schema: f.object({orgName: f.string().nonempty(),taxId: f.string().nonempty()}),fields: {}});For complex, dynamic, or highly customized validation structures, you can interact directly with the Inversion of Control (IoC) container.
import{SetupHelpers,SFormularManager}from'formular.dev';importtype{IFormularManager}from'formular.dev';// 1. Initialize a full-featured containerconstserviceManager=SetupHelpers.forFormApplication();// 2. Resolve the form manager service using its unique symbolconstformularManager=serviceManager.resolve<IFormularManager>(SFormularManager);// 3. Create a form from field descriptorsconstform=awaitformularManager.createFromDescriptors('advanced-form',[{id: 1,name: 'email',label: 'Email Address',type: 'email',value: '',defaultValue: '',isValid: false,isDirty: false,isPristine: true,isFocus: false,shouldValidate: true,errors: [],guides: [],options: [],validationOptions: {required: {value: true,error: {message: 'Email is required',code: 'required'}}}}]);// 4. Validate the form by its registered keyconstisValid=awaitformularManager.validate('advanced-form');Submission strategies control form behaviors like validation callbacks and submission states:
The default strategy, which submits form data directly to a handler:
import{DirectSubmissionStrategy}from'formular.dev';constdirectStrategy=newDirectSubmissionStrategy(async(data)=>{returnawaitapi.submit(data);});A strategy designed for integration with stateful wrappers (like component providers) that need hooks for validation start/complete and dismissal checks:
import{ContextSubmissionStrategy}from'formular.dev';constcontextStrategy=newContextSubmissionStrategy(async(data)=>{returnawaitapi.submit(data);},{isDismissed: ()=>checkUserCanceled(),onValidationStart: ()=>setValidationIndicator(true),onValidationComplete: (isValid)=>{setValidationIndicator(false);if(!isValid)reportFailures();}});Validation failures throw a specialized SchemaValidationError containing context about the failing paths:
import{SchemaValidationError}from'formular.dev';try{awaitform.submit();}catch(error){if(errorinstanceofSchemaValidationError){console.error('Validation failed code:',error.code);console.error('Validation error path:',error.path);// e.g. ['address', 'postalCode']console.error('List of errors:',error.errors);}}Traditional form engines validate by traversing entire object trees or triggering global context re-renders. formular.dev isolates fields through a channel-based pub-sub messaging system.
[Field Component] --(updates)--> [Notification Channel] --(triggers)--> [Specific Validator]
When a field changes or blurs:
- It publishes a notification to its specific channel key (e.g.
field_name). - Only managers and observers subscribed to that channel key execute validation or update states.
- Debouncing and weak reference retention are handled natively, preventing memory leaks and UI thread blockages.
This architecture ensures constant-time O(1) validation dispatching, even in forms with hundreds of inputs.
This project is licensed under the AGPLv3 License - see the LICENSE file for details.