Why Forge?
Native form handling quickly grows repetitive when you need typed values, deterministic submit behavior, and granular subscriptions.
ts
// Before: manual state and ad-hoc validation sequencing
const errors: Record<string, string> = {};
if (!email.includes('@')) errors.email = 'Invalid email';
if (password.length < 8) errors.password = 'Too short';
if (Object.keys(errors).length === 0) await submit({ email, password });
// After: one form controller with explicit transitions
const form = createForm({
defaultValues: { email: '', password: '' },
validators: { email: isEmail, password: min8 },
});
await form.submit(submit);| Feature | Forge | React Hook Form | VeeValidate |
|---|---|---|---|
| Bundle size | 5.5 KB | ~9 kB | ~16 kB |
| Framework-agnostic | React only | Vue only | |
| Typed dot-path APIs | Partial | Partial | |
| Result-based submit flow | |||
| Live field observation | |||
| Full array helpers | |||
| Scoped sub-forms | |||
| Form + field subscriptions | |||
| Zero dependencies |
Use Forge when you want one typed form controller that works across frameworks or in vanilla apps with explicit, predictable state transitions.
Consider framework-specific alternatives when you need deeply integrated framework bindings and are not sharing form logic across runtimes.
Installation
sh
pnpm add @vielzeug/forgesh
npm install @vielzeug/forgesh
yarn add @vielzeug/forgeQuick Start
ts
import { createForm } from '@vielzeug/forge';
const form = createForm({
defaultValues: { email: '', password: '' },
validators: {
email: (v) => (!String(v).includes('@') ? 'Invalid email' : undefined),
password: (v) => (String(v).length < 8 ? 'Min 8 chars' : undefined),
},
});
const { valid, errors } = await form.validate();
if (!valid) {
console.log(errors);
}
const submission = await form.submit(async (values) => {
await fetch('/api/login', {
body: JSON.stringify(values),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
});
});
if (!submission.ok && submission.type === 'validation') {
console.log(submission.errors);
}Features
- Typed field paths with compile-time value inference
- Explicit validation API:
validate()andvalidateFields(fields) - Single-field validation with
validateField(name) - Streaming validation with
validateStream()— yields each field result as it resolves, read-only - Per-connection validation triggers via
connect()withValidationModespresets connect()bindings own independent debounce timers; calldisconnect()on unmountsubmit(handler)— returns{ ok: true, value }or{ ok: false, errors }- Schema integration: pass any
safeParse-compatible schema directly tovalidator scope(prefix)— memoized scoped sub-forms that share parent state with relative field pathssubscribeScoped()— filtered subscription that only fires on changes within the scope's prefixsnapshot()/restore()— capture and replay complete form stateremoveField(name)— clean conditional field lifecycle- Full array helpers:
append,prepend,insert,remove,move,swap,replace - Explicit synchronous subscriptions:
subscribe,subscribeField, andsubscribeScoped - Stable frozen snapshots for
form.stateandform.field(name)(external-store friendly) - Explicit touched and error controls:
touch,untouch,touchAll,untouchAll,setError,resetErrors - Mutation batching with
batch(fn)and dynamic field validators viasetValidator - Baseline-safe
reset/replace/patchmodel - Browser-first utility:
toFormData - Ready-made adapters for React, Vue, and Svelte
@vielzeug/forge/validatorsadapter:fieldValidatorandcomposeValidators
Documentation
See Also
- Spell — schema validation; plug a Spell schema into Forge to validate fields and submission payloads with full type inference
- Courier — HTTP client; submit Forge's validated payload directly through a Courier mutation with loading and error state wired automatically
- Ripple — reactive signals; Forge exposes field values and submission state as signals for fine-grained reactive UI updates