@formos/kernel
v0.1.0
Published
Headless form engine for Formos - framework-agnostic state management, validation, and effects
Maintainers
Readme
@formos/kernel
Headless, framework-agnostic form engine for the Formos form builder.
Purpose
@formos/kernel provides the runtime execution layer for forms defined by @formos/schema. It's a completely headless engine that manages all form logic without any UI concerns - no React, no DOM, no browser APIs.
Think of it as the "brain" of the form - it handles state, validation orchestration, effect execution, and multi-step navigation, but leaves rendering entirely to UI packages.
Responsibilities
This package is responsible for:
- State Management: Maintaining form values, errors, touched fields, and dirty state
- Validation Orchestration: Coordinating validation execution with pluggable validator adapters
- Effect Execution: Triggering side effects (field syncing, API calls) via effect executors
- Conditional Logic Evaluation: Determining field visibility and required status dynamically
- Multi-Step Flow Control: Managing step navigation with validation guards
- Lifecycle Management: Handling submit, reset, and field value updates
What This Package Does NOT Do
❌ NO UI Rendering
- Does not create DOM elements
- Does not provide React components
- Does not handle user interactions directly
❌ NO Framework Dependencies
- No React, Vue, Svelte, or any UI framework
- No browser-specific APIs (localStorage, fetch, etc.)
- Completely runtime-agnostic
❌ NO Built-in Validation Libraries
- Does not include Zod, Yup, or any validators
- Uses adapter pattern - you provide validators
- Same for effects - no built-in API clients or transformers
❌ NO Schema Definition
- Does not define schema types (that's
@formos/schema) - Consumes
NormalizedSchemaV1as input - Does not modify or mutate schemas
Think of it as: The engine that executes the schema's instructions, not the schema itself.
Stability Guarantee
FormEngine API is STABLE and FINAL ✅
The FormEngine interface is committed to backward compatibility:
- Method signatures won't change
- Return types remain consistent
- Behavior is predictable and documented
This guarantee enables:
- Safe updates: Upgrade the kernel without UI changes
- Framework flexibility: Build UI packages for React, Vue, Svelte
- Long-term reliability: Code written today works tomorrow
What Can Change
✅ Internal implementations can be optimized
✅ New optional methods may be added to FormEngine
✅ Performance improvements without API changes
✅ Bug fixes that don't alter public behavior
Adapter Contract
The adapter types (ValidatorExecutor, EffectExecutor) are stable:
- Validators return
string | null(error or valid) - Effects receive context with
getValueandsetValue - These interfaces won't have breaking changes
Installation
pnpm add @formos/kernel @formos/schemaBasic Usage
Create a Form Engine
import { createFormEngine } from '@formos/kernel';
import { normalizeSchema, type FormSchemaV1 } from '@formos/schema';
// 1. Define schema
const schema: FormSchemaV1 = {
version: 'v1',
fields: [
{
name: 'email',
type: 'email',
required: true,
validations: [
{
trigger: ['onChange', 'onBlur'],
validator: 'email',
rule: {},
message: 'Invalid email'
}
]
},
{ name: 'password', type: 'password', required: true }
]
};
// 2. Normalize schema
const normalized = normalizeSchema(schema);
// 3. Create engine with validators
const engine = createFormEngine(normalized, {
validators: {
required: (value) => value ? null : 'Required',
email: (value) => {
const regex = /\S+@\S+\.\S+/;
return regex.test(String(value)) ? null : 'Invalid email';
}
},
onSubmit: async (values) => {
console.log('Form submitted:', values);
}
});Get and Set Values
// Get field value
const email = engine.getValue('email');
console.log('Email:', email); // undefined initially
// Set field value (triggers validation + effects)
await engine.setValue('email', '[email protected]');
// Set without validation
await engine.setValue('email', '[email protected]', {
trigger: 'manual',
skipValidation: true
});Validation
// Validate single field
const isEmailValid = await engine.validate('email', 'onChange');
// Validate entire form
const isFormValid = await engine.validate();
// Check validation state
const emailError = engine.getError('email');
const allErrors = engine.getErrors();
const isValid = engine.isValid();
// Manual error management
engine.setError('email', 'Custom error message');
engine.clearError('email');Form State
// Check if field was modified
const isEmailDirty = engine.isDirty('email');
const isFormDirty = engine.isDirty(); // Any field dirty?
// Check if field was touched (focused and blurred)
const wasEmailTouched = engine.isTouched('email');
// Mark field as touched
engine.markTouched('email');Submit and Reset
// Submit form (validates all fields first)
const success = await engine.submit();
if (success) {
console.log('Form submitted successfully');
} else {
console.log('Validation failed:', engine.getErrors());
}
// Reset form to initial state
engine.reset();Multi-Step Forms
const multiStepSchema: FormSchemaV1 = {
version: 'v1',
fields: [
{ name: 'firstName', type: 'text' },
{ name: 'lastName', type: 'text' },
{ name: 'email', type: 'email' },
{ name: 'phone', type: 'tel' }
],
steps: [
{ id: 'personal', title: 'Personal Info', fields: ['firstName', 'lastName'] },
{ id: 'contact', title: 'Contact', fields: ['email', 'phone'] }
]
};
const normalized = normalizeSchema(multiStepSchema);
const engine = createFormEngine(normalized, { validators });
// Step navigation (only available for multi-step forms)
const currentStep = engine.getCurrentStep?.(); // 0
const totalSteps = engine.getTotalSteps?.(); // 2
// Go to next step (validates current step first)
const canProceed = await engine.nextStep?.();
if (!canProceed) {
console.log('Fix errors before proceeding');
}
// Go to previous step (no validation)
engine.prevStep?.();
// Jump to specific step (validates current step first)
const moved = await engine.goToStep?.(1);
// Check if can go to step
const canGo = engine.canGoToStep?.(1);Advanced: Custom Validators
Validators use the adapter pattern for framework flexibility:
import type { ValidatorExecutor } from '@formos/kernel';
// Simple validator
const required: ValidatorExecutor = (value, params, context) => {
return value ? null : 'Required';
};
// Cross-field validator
const matchesField: ValidatorExecutor = (value, params, context) => {
const { targetField } = params as { targetField: string };
const targetValue = context.getValue(targetField);
return value === targetValue ? null : 'Fields must match';
};
// Async validator (e.g., API check)
const uniqueUsername: ValidatorExecutor = async (value, params, context) => {
const response = await fetch(`/api/check-username?username=${value}`);
const { available } = await response.json();
return available ? null : 'Username already taken';
};
const engine = createFormEngine(normalized, {
validators: {
required,
matchesField,
uniqueUsername
}
});Advanced: Effect Executors
Effects handle side effects triggered by field changes:
import type { EffectExecutor } from '@formos/kernel';
// Sync effect: Calculate fullName from firstName + lastName
const syncFullName: EffectExecutor = (params, context) => {
const firstName = context.getValue('firstName') as string || '';
const lastName = context.getValue('lastName') as string || '';
context.setValue('fullName', `${firstName} ${lastName}`.trim());
};
// API effect: Fetch cities when country changes
const fetchCities: EffectExecutor = async (params, context) => {
const country = context.sourceValue as string;
const response = await fetch(`/api/cities?country=${country}`);
const cities = await response.json();
context.setValue('cities', cities);
};
const engine = createFormEngine(normalized, {
effectExecutors: {
syncFullName,
fetchCities
}
});Conditional Logic
The kernel evaluates conditional visibility and required logic:
import { evaluateConditional, isFieldVisible, isFieldRequired } from '@formos/kernel';
// Check field visibility
const field = schema.fieldMap?.get('ssn');
if (field && isFieldVisible(field, (name) => engine.getValue(name))) {
// Render SSN field
}
// Check if field is dynamically required
if (field && isFieldRequired(field, (name) => engine.getValue(name))) {
// Show required indicator
}
// Direct conditional evaluation
const condition = {
logic: 'and',
rules: [
{ field: 'age', operator: 'greaterThan', value: 18 },
{ field: 'country', operator: 'equals', value: 'US' }
]
};
const result = evaluateConditional(condition, (name) => engine.getValue(name));API Reference
Core Function
createFormEngine(schema, options): Create form engine instance
FormEngine Interface
State Accessors:
getValue(fieldName): Get field valuegetError(fieldName): Get field errorgetErrors(): Get all errorsisValid(): Check if form is validisDirty(fieldName?): Check dirty stateisTouched(fieldName): Check touched state
State Mutators:
setValue(fieldName, value, options?): Set field valuesetError(fieldName, error): Set field errorclearError(fieldName): Clear field errormarkTouched(fieldName): Mark field as touched
Validation:
validate(fieldName?, trigger?): Validate field(s)
Lifecycle:
submit(): Submit form (validates first)reset(): Reset form to initial state
Multi-Step Navigation (if applicable):
getCurrentStep?(): Get current step indexgetTotalSteps?(): Get total step countnextStep?(): Go to next step (validates first)prevStep?(): Go to previous stepgoToStep?(stepIndex): Jump to specific stepcanGoToStep?(stepIndex): Check if can navigate to step
Adapter Types
ValidatorExecutor:(value, params, context) => string | null | Promise<string | null>EffectExecutor:(params, context) => unknown | Promise<unknown>ValidationContext: Read-only access to form valuesEffectContext: Read-write access to form values
Utility Functions
evaluateConditional(condition, getValue): Evaluate conditional expressionisFieldVisible(field, getValue): Check field visibilityisFieldRequired(field, getValue): Check if field is required
Package Structure
@formos/kernel/
├── engine/
│ ├── factory.ts # createFormEngine, FormEngine interface
│ ├── values.ts # getValue, setValue operations
│ ├── validation-ops.ts # validate, getError, isValid
│ ├── lifecycle.ts # submit, reset
│ ├── init.ts # Initialization helpers
│ └── helpers.ts # Internal utilities
├── state.ts # FormState class
├── validation.ts # Validation orchestration
├── effects.ts # Effect execution
├── conditionals.ts # Conditional evaluation
├── steps.ts # Multi-step controller
├── types.ts # Adapter type definitions
└── index.ts # Public API exportsTypeScript Configuration
This package uses strict mode TypeScript:
noUncheckedIndexedAccess: true- Optional chaining (
?.) used everywhere for safety moduleResolution: "NodeNext"module: "NodeNext"
All exports are ESM only (type: "module").
Design Principles
1. Adapter Pattern
The kernel doesn't include validation libraries or HTTP clients. Instead, it defines interfaces (ValidatorExecutor, EffectExecutor) that you implement with your preferred tools.
2. Immutable Schema
The kernel never modifies the schema. It reads the normalized schema and executes its instructions, but the schema remains a pure data structure.
3. Framework Agnostic
No React hooks, no DOM manipulation, no browser APIs. This enables:
- React bindings via
@formos/react - Vue bindings (future)
- Svelte bindings (future)
- Node.js usage (server-side form validation)
4. Optional Chaining Everywhere
Since schemas are user-provided, the kernel uses optional chaining extensively for safety.
Contributing
This package is part of the Formos monorepo. See the root README for contribution guidelines.
License
MIT
