Skip to content
forge logoForgeForms
Framework-agnostic immutable form state with focused object fields and explicit validation results.
Version
v2.1.1
Size
2.3 KB gzip
BrowserNode ≥22SSRDeno
createFormtoFormDatabindFieldcustomValidatorsaveForm View all 6 exports

Why Forge?

Native form state becomes difficult to inspect once values, validation, draft restoration, and UI bindings share mutable objects. Forge owns one immutable value tree and gives you typed handles for object branches without string paths, scoped controllers, or framework state.

ts
// Before
const values = { email: '', password: '' };
const errors: Record<string, string> = {};

function submit() {
  errors.email = values.email.includes('@') ? '' : 'Invalid email';
  errors.password = values.password.length >= 8 ? '' : 'Use at least eight characters';
}

// After
const form = createForm({
  initialValues: { email: '', password: '' },
  validate: (value) => ({
    fields: {
      email: value.email.includes('@') ? undefined : 'Invalid email',
      password: value.password.length >= 8 ? undefined : 'Use at least eight characters',
    },
  }),
});
FeatureForgeNative form stateFramework-owned form state
Bundle size2.3 KBVaries
Zero external dependencies
Immutable nested valuesVaries
Typed object field handlesVaries
Framework-independent state

Use Forge when form state needs framework-independent immutable values, typed object fields, and one explicit validation boundary.

Consider framework-owned form state when application only needs a single UI framework's native input bindings.

Installation

sh
pnpm add @vielzeug/forge
sh
npm install @vielzeug/forge
sh
yarn add @vielzeug/forge

Install @vielzeug/spell or @vielzeug/vault only when importing Forge's matching optional adapter.

Quick Start

Create a form, update a focused field, and submit only after validation passes.

ts
import { createForm } from '@vielzeug/forge';

const form = createForm({
  initialValues: { profile: { email: '', name: '' } },
  validate: (value) => ({
    fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },
  }),
});

form.field('profile').field('email').set('ada@example.com');

const result = await form.submit(async (value) => {
  const response = await fetch('/api/profile', {
    body: JSON.stringify(value),
    headers: { 'Content-Type': 'application/json' },
    method: 'POST',
  });

  return response.ok;
});

if (!result.ok && result.type === 'validation') console.log(result.errors);

Features

  • form.value exposes one immutable nested value tree.
  • form.field(key) selects typed object branches without string paths.
  • field.set(updater) replaces array values without index handles.
  • form.validate() returns valid, invalid, or aborted results.
  • form.submit(handler) touches, validates, and invokes the handler when valid.
  • bindField() connects one DOM element without owning validation timing.
  • customValidator() maps Spell schema errors into Forge fields.
  • saveForm() and loadForm() persist explicit Vault draft records.

Documentation

See Also

  • Spell — adapt a Spell schema through customValidator().
  • Vault — save and restore explicit Forge draft records.
  • Courier — send a validated form value through a mutation.