Forms

Framework-independent form engine (@sometic/forms) paired with @sometic/validation. Controllers own values, field meta, validation scheduling, submit, server errors, and optional drafts / steps / feedback, React, Vue, and sometic-form are thin adapters over the same API.

System standout: server map + private drafts

mapServerErrorBody (@sometic/forms/server) normalizes Problem Details-ish payloads. Drafts support omit / pick / sanitize so passwords never hit storage. Pair submit with bindMutationForm for epoch-safe mutation + invalidate.

Installation

pnpm
pnpm add @sometic/forms @sometic/validation
npm
npm install @sometic/forms @sometic/validation
yarn
yarn add @sometic/forms @sometic/validation
bun
bun add @sometic/forms @sometic/validation

Framework UI:

bash
pnpm add @sometic/react   # or @sometic/vue / @sometic/elements

Import map

PathPurpose
@sometic/formscreateForm, types, re-exports
@sometic/forms/draftsDraft persistence
@sometic/forms/stepsMulti-step wizard helpers
@sometic/forms/form-dataFormData ↔ values
@sometic/forms/a11yAnnounce + focus first invalid
@sometic/forms/feedbackFeedback factories + attributes
@sometic/validationValidators, compose, issues, schema contract
@sometic/react/formHooks + Form
@sometic/vue/formComposables + Form
@sometic/elements/formsometic-form

Quick start

js
import { createForm } from "@sometic/forms";
import { required, email, pipe } from "@sometic/validation";

const form = createForm({
    defaultValues: { email: "", name: "" },
    validationMode: "onSubmit",
    debounceMs: 0,
});

form.register("email", {
    validators: [pipe(required(), email())],
    validateOn: "onBlur",
});

form.register("name", { validators: [required()] });

const submit = form.handleSubmit({
    onValid: async (values, { signal }) => {
        await fetch("/api/profile", {
            method: "POST",
            body: JSON.stringify(values),
            signal,
        });
    },
    onInvalid: (issues) => {
        console.warn(issues);
    },
    successMessage: "Profile saved.",
});
ts
import { createForm } from "@sometic/forms";
import { required, email, pipe } from "@sometic/validation";
import type { FormController } from "@sometic/forms";

type ProfileValues = { email: string; name: string };

const form: FormController<ProfileValues> = createForm({
    defaultValues: { email: "", name: "" },
    validationMode: "onSubmit",
    debounceMs: 0,
});

form.register("email", {
    validators: [pipe(required(), email())],
    validateOn: "onBlur",
});

form.register("name", { validators: [required()] });

const submit = form.handleSubmit({
    onValid: async (values, { signal }) => {
        await fetch("/api/profile", {
            method: "POST",
            body: JSON.stringify(values),
            signal,
        });
    },
    onInvalid: (issues) => {
        console.warn(issues);
    },
    successMessage: "Profile saved.",
});
js
import { createForm } from "@sometic/forms";
import { required, email, pipe } from "@sometic/validation";

const form = createForm({
    defaultValues: { email: "", name: "" },
    validationMode: "onSubmit",
    debounceMs: 0,
});

form.register("email", {
    validators: [pipe(required(), email())],
    validateOn: "onBlur",
});

form.register("name", { validators: [required()] });

const formEl = document.querySelector("#profile");
formEl?.addEventListener("submit", (event) => {
    event.preventDefault();
    void form.handleSubmit({
        onValid: async (values, { signal }) => {
            await fetch("/api/profile", {
                method: "POST",
                body: JSON.stringify(values),
                signal,
            });
        },
        onInvalid: (issues) => {
            console.warn(issues);
        },
        successMessage: "Profile saved.",
    })();
});

Subscribe for UI updates:

ts
const unsubscribe = form.subscribe(() => {
    const meta = form.getFormMeta();
    const values = form.getValues();
});

Always call form.dispose() when the form leaves the page (Vue adapters do this for you; React useForm does not).

Core concepts

ConceptMeaning
FormControllerExplicit, disposable instance, no module singleton
Pathsemail, user.email, items[0].qty via @sometic/validation path helpers
FieldMetadirty, touched, visited, valid, invalid, pending, enabled, error, issues
FormMetaAggregate dirty/touched/valid/pending + submitting, submitCount
ValidationModeonChange | onBlur | onSubmit | onTouched
Issues{ code, message, path?, params? } from @sometic/validation

Defaults: validationMode: "onSubmit", debounceMs: 0, feedback flags all enabled.

Controller surface (summary)

MethodRole
getValues / getValue / setValueRead/write (cloned reads)
register / unregisterField lifecycle + RegisterResult
getFieldMeta / getFormMetaUI state
validateField / validateFormImperative validation
handleSubmitAbortable submit pipeline
setServerErrors / clearServerErrorsAPI error mapping
setErrors / clearErrorsClient external errors
reset / partialResetRestore defaults
createFieldArrayArray helpers
subscribe / disposeReactivity + cleanup

Full field details: Fields. Validation: Validation. Adapters: Form component.

When to use

Shared validation and submit orchestration that must look identical in React, Vue, and vanilla, including async validators, server errors, and drafts.

When not to use

  • Trivial one-off native forms with no shared logic
  • Global app state unrelated to a form document, use @sometic/store
  • Replacing a schema library without an adapter, implement SchemaAdapter if you need Zod/Yup