form-forge Svelte Themes

Form Forge

Zero-dependency, schema-based form validation with a chainable TypeScript API. Optional adapters for React, Vue 3, and Svelte ship as separate entry points — tree-shake out whatever you don't use.

form-forge

Zero-dependency, schema-based form validation with a chainable TypeScript API. Optional adapters for React, Vue 3, and Svelte ship as separate entry points — tree-shake out whatever you don't use.


Features

  • Zero runtime dependencies — pure TypeScript core
  • Dual ESM + CJS output — works in every modern bundler
  • Full type inferenceInferSchema<typeof schema> gives you your form's type for free
  • Async rulescustom() accepts async functions (e.g. API availability checks)
  • Auto-coercion — number fields accept "24", boolean fields accept "true"/"false"
  • Per-field validationvalidateField() for onChange/onBlur live feedback
  • Framework adapters — React hook, Vue 3 composable, Svelte store (all optional)

Installation

npm install form-forge

Framework adapters have no extra install — they are already bundled. Add the relevant framework as a peer dependency if it isn't already in your project:

# React
npm install react

# Vue
npm install vue

# Svelte
npm install svelte

Quick start

import { forge, f } from 'form-forge';

const validator = forge({
  email:    f.string().email().required(),
  password: f.string().min(8).required(),
  age:      f.number().min(18).integer(),
  terms:    f.boolean().isTrue('Must accept terms').required(),
  role:     f.string().oneOf(['admin', 'user']).required(),
  site:     f.string().url(),
  handle:   f.string().matches(/^@/).trim().required(),
  username: f.string().custom(async val => val !== 'taken' || 'Username taken').required(),
});

const result = await validator.validate({
  email:    '[email protected]',
  password: 'secret123',
  age:      '25',       // strings are auto-coerced for number fields
  terms:    true,
  role:     'admin',
  handle:   '  @cool  ',  // trimmed to '@cool'
  username: 'free',
});

if (result.success) {
  console.log(result.data);
  // { email: string; password: string; terms: boolean; role: string; handle: string; username: string; age?: number; site?: string }
} else {
  console.log(result.errors);
  // [{ field: 'email', message: '...', rule: 'email' }, ...]
}

API reference

forge(schema)

Compiles a schema into a reusable Validator.

const validator = forge({
  name: f.string().required(),
});

validator.validate(data)

Validates the entire form object. All fields run in parallel. Returns:

// success
{ success: true,  data: T,    errors: [] }

// failure
{ success: false, data: null, errors: [{ field, message, rule }] }

validator.validateField(field, value)

Validates a single field — ideal for onChange/onBlur handlers.

const { valid, error } = await validator.validateField('email', inputValue);

Field builders (f)

f.string()

Method Description
.required() Fail if empty / missing
.min(n, msg?) Minimum character length
.max(n, msg?) Maximum character length
.email(msg?) Valid email format
.url(msg?) Valid http(s):// URL
.matches(regex, msg?) Must match pattern
.oneOf(values, msg?) Must be one of the listed strings
.trim() Strip whitespace; trimmed value is returned in data
.custom(fn, key?) Sync or async rule (true = pass, string = error message)

f.number()

String inputs are auto-coerced ("24"24).

Method Description
.required() Fail if missing
.min(n, msg?) Minimum value (inclusive)
.max(n, msg?) Maximum value (inclusive)
.integer(msg?) Must be a whole number
.positive(msg?) Must be > 0
.custom(fn, key?) Sync or async custom rule

f.boolean()

String inputs "true" / "false" are auto-coerced.

Method Description
.required() Fail if missing
.isTrue(msg?) Must be strictly true (checkbox "must agree")
.isFalse(msg?) Must be strictly false
.custom(fn, key?) Sync or async custom rule

InferSchema<S>

Derive your form's TypeScript type directly from the schema — no duplicate type declarations.

import { type InferSchema } from 'form-forge';

const schema = {
  email: f.string().email().required(),
  age:   f.number().min(18),
};

type FormData = InferSchema<typeof schema>;
// { email: string; age?: number }

Required fields (.required()) are non-optional; everything else is T | undefined.


React adapter

import { useForge } from 'form-forge/react';
import { f } from 'form-forge';

const schema = {
  email:    f.string().email().required(),
  password: f.string().min(8).required(),
};

function LoginForm() {
  const {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    reset,
  } = useForge(schema, {
    initialValues:    { email: '', password: '' },
    validateOnBlur:   true,   // default
    validateOnChange: false,  // default
  });

  return (
    <form onSubmit={handleSubmit(async data => {
      await loginAPI(data);
    })}>
      <input
        value={values.email ?? ''}
        onChange={e => handleChange('email', e.target.value)}
        onBlur={e  => handleBlur('email',  e.target.value)}
      />
      {touched.email && errors.email && <span>{errors.email}</span>}

      <input
        type="password"
        value={values.password ?? ''}
        onChange={e => handleChange('password', e.target.value)}
        onBlur={e  => handleBlur('password',  e.target.value)}
      />
      {touched.password && errors.password && <span>{errors.password}</span>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in…' : 'Login'}
      </button>
      <button type="button" onClick={reset}>Reset</button>
    </form>
  );
}

Vue 3 adapter

<script setup lang="ts">
import { useForge } from 'form-forge/vue';
import { f } from 'form-forge';

const schema = {
  email:    f.string().email().required(),
  password: f.string().min(8).required(),
};

const { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit, reset } =
  useForge(schema, { initialValues: { email: '', password: '' } });
</script>

<template>
  <form @submit.prevent="handleSubmit(async data => await loginAPI(data))()">
    <input
      v-model="values.email"
      @change="handleChange('email', values.email)"
      @blur="handleBlur('email', values.email)"
    />
    <span v-if="touched.email && errors.email">{{ errors.email }}</span>

    <input
      type="password"
      v-model="values.password"
      @change="handleChange('password', values.password)"
      @blur="handleBlur('password', values.password)"
    />
    <span v-if="touched.password && errors.password">{{ errors.password }}</span>

    <button :disabled="isSubmitting">{{ isSubmitting ? 'Logging in…' : 'Login' }}</button>
    <button type="button" @click="reset">Reset</button>
  </form>
</template>

Svelte adapter

<script lang="ts">
import { forgeStore } from 'form-forge/svelte';
import { f } from 'form-forge';

const schema = {
  email:    f.string().email().required(),
  password: f.string().min(8).required(),
};

const form = forgeStore(schema, {
  initialValues: { email: '', password: '' },
});
</script>

<form on:submit|preventDefault={form.handleSubmit(async data => await loginAPI(data))}>
  <input
    bind:value={$form.values.email}
    on:change={() => form.handleChange('email', $form.values.email)}
    on:blur={()  => form.handleBlur('email',  $form.values.email)}
  />
  {#if $form.touched.email && $form.errors.email}
    <span>{$form.errors.email}</span>
  {/if}

  <input
    type="password"
    bind:value={$form.values.password}
    on:change={() => form.handleChange('password', $form.values.password)}
    on:blur={()  => form.handleBlur('password',  $form.values.password)}
  />
  {#if $form.touched.password && $form.errors.password}
    <span>{$form.errors.password}</span>
  {/if}

  <button disabled={$form.isSubmitting}>
    {$form.isSubmitting ? 'Logging in…' : 'Login'}
  </button>
  <button type="button" on:click={form.reset}>Reset</button>
</form>

Contributing

Pull requests are welcome. Run npm test to verify all 19 tests pass before submitting. For larger changes, open an issue first.


License

MIT — see LICENSE.

Top categories

Loading Svelte Themes