npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@veams/form

v0.10.0

Published

Form state handlers and React bindings for the VEAMS StatusQuo ecosystem.

Readme

@veams/form

Form state handlers plus optional React bindings for the VEAMS StatusQuo ecosystem.

This package keeps form state generic at the root entrypoint and ships React-only helpers under @veams/form/react.

Docs

Live docs:

https://veams.github.io/status-quo/packages/form/overview

Install

npm install @veams/form @veams/status-quo react

Package Exports

Root exports:

  • FormStateHandler
  • FormActions
  • FormErrors
  • FormFieldName
  • FormFieldValue
  • FormInitStatus
  • FormState
  • FormStateHandlerConfig
  • FormStateHandlerOptions
  • FormTouched
  • FormValues
  • OnInitFn
  • ValidatorFn

React entrypoint:

  • @veams/form/react
  • FormProvider
  • useFormController
  • useFieldMeta
  • useFormMeta
  • useUncontrolledField
  • Controller

Validator adapters:

  • @veams/form/validators
  • @veams/form/validators/zod
  • toZodValidator(schema)

Quickstart

Create a generic form handler:

import { FormStateHandler } from '@veams/form';

type LoginValues = {
  email: string;
  password: string;
};

const loginForm = new FormStateHandler<LoginValues>({
  initialValues: {
    email: '',
    password: '',
  },
  validator: (values) => {
    const errors: Partial<Record<keyof LoginValues, string>> = {};

    if (!values.email) {
      errors.email = 'Email is required';
    }

    if (!values.password) {
      errors.password = 'Password is required';
    }

    return errors;
  },
});

loginForm.setFieldValue('email', '[email protected]');
loginForm.validateForm();

Nested values are supported through dot-path field names:

type ProfileForm = {
  profile: {
    email: string;
  };
};

const profileForm = new FormStateHandler<ProfileForm>({
  initialValues: {
    profile: {
      email: '',
    },
  },
});

profileForm.setFieldValue('profile.email', '[email protected]');
profileForm.setFieldTouched('profile.email', true);

FormProvider Wrapper

By default, FormProvider renders a <form> element. You can change this using the renderAs prop (supports 'form', 'fieldset', 'div', or 'section'). When using a non-form element, onSubmit becomes optional and the native submit event is not automatically attached.

<FormProvider renderAs="div" initialValues={{ name: '' }}>
  {/* ... */}
</FormProvider>

React Quickstart

Use FormProvider to own one handler instance locally and useUncontrolledField() to bind native elements:

import { FormProvider, useUncontrolledField } from '@veams/form/react';

function EmailField() {
  const { meta, registerProps } = useUncontrolledField('email');

  return (
    <label>
      Email
      <input {...registerProps} type="email" />
      {meta.showError ? <span>{meta.error}</span> : null}
    </label>
  );
}

function LoginForm() {
  return (
    <FormProvider
      initialValues={{ email: '', password: '' }}
      onSubmit={async (values) => {
        await submitLogin(values);
      }}
      validator={(values) => ({
        ...(values.email ? {} : { email: 'Email is required' }),
        ...(values.password ? {} : { password: 'Password is required' }),
      })}
    >
      <EmailField />
      <button type="submit">Sign in</button>
    </FormProvider>
  );
}

Validation Timing

In the React layer, fields validate on first blur by default and revalidate on change after they have been touched once. That keeps empty fields quiet until the user leaves them, while still clearing stale errors as they type a fix.

<FormProvider
  initialValues={{ email: '', password: '' }}
  onSubmit={handleSubmit}
  validator={validator}
  validationMode="blur"
  revalidationMode="change"
>
  <EmailField />
</FormProvider>

You can override that behavior per field:

function EmailField() {
  const { meta, registerProps } = useUncontrolledField('email', {
    validationMode: 'change',
  });

  return (
    <label>
      Email
      <input {...registerProps} type="email" />
      {meta.showError ? <span>{meta.error}</span> : null}
    </label>
  );
}

function RoleField() {
  return (
    <Controller
      name="role"
      validationMode="submit"
      render={({ field, fieldState }) => (
        <>
          <RoleSelect
            onBlur={field.onBlur}
            onChange={field.onChange}
            value={field.value as string}
          />
          {fieldState.touched && fieldState.error ? <span>{fieldState.error}</span> : null}
        </>
      )}
    />
  );
}

Available modes are 'change', 'blur', 'submit', and 'inherit'. 'inherit' means "use the current FormProvider defaults".

Custom Bindings

The library exports FormValidationConfigContext, defaultFormValidationConfig, resolveValidationBehavior, and shouldValidateFieldInteraction from @veams/form/react for custom field bindings.

import { useContext } from 'react';
import {
  FormValidationConfigContext,
  resolveValidationBehavior,
  shouldValidateFieldInteraction,
} from '@veams/form/react';

const config = useContext(FormValidationConfigContext);
const behavior = resolveValidationBehavior(config, overrides);

function handleBlur() {
  if (shouldValidateFieldInteraction('blur', touched, behavior)) {
    validateField();
  }
}

Touched-Only Validation

Use validateTouchedFields() when a form must validate only fields the user has touched. The action runs the configured validator, stores only related errors, and returns true when no touched field has an error.

An error stays when its path is:

  • a touched path;
  • a parent of a touched path;
  • a child of a touched path.

isValid then reflects the stored touched-field error map only. Call validateForm() before submit.

The filter applies to one run. setFieldValue() and React blur validation call the full validateForm() pipeline. The next interaction writes the full error map again. See Validation Timing for React behavior.

A cross-field error on an untouched path is not stored. For example, a validator can return "Passwords do not match" on passwordConfirm while the user touched only password.

type LoginValues = {
  email: string;
  password: string;
};

declare const submitValues: (values: LoginValues) => void;
declare const validator: (values: LoginValues) => Partial<Record<keyof LoginValues, string>>;

const form = new FormStateHandler({
  initialValues: { email: '', password: '' },
  validator,
});

form.setFieldTouched('email');
form.validateTouchedFields();

function handleSubmit() {
  if (!form.validateForm()) {
    return;
  }

  submitValues(form.getState().values);
}

Uncontrolled Field Principle

Native fields should stay uncontrolled by default in VEAMS Form, while FormStateHandler remains the source of truth for values, errors, touched state, and submit state.

Why this default is useful:

  • Lower render churn: typing updates the DOM directly without forcing controlled React value props on every keystroke.
  • Native behavior stays intact: browser input semantics, selection handling, and autofill work naturally.
  • Cleaner component code: field components mostly spread registerProps and render meta.
  • Clear ownership boundaries: feature/form behavior stays in the handler, React stays a binding layer.

When a component requires controlled props (value + onChange), use Controller intentionally for that field only.

Feature-Owned Form State

A feature handler can own the form handler and pass it into the React provider. This keeps cross-field validation and non-form UI state in the same feature boundary. When formHandlerInstance is provided, initialValues and validator stay on the handler and are not passed to FormProvider.

import { SignalStateHandler } from '@veams/status-quo';
import { FormStateHandler } from '@veams/form';

type LoginValues = {
  email: string;
  password: string;
};

type LoginState = {
  isPasswordVisible: boolean;
};

type LoginActions = {
  getFormHandler: () => FormStateHandler<LoginValues>;
  submitLogin: (values: LoginValues) => Promise<void>;
  togglePasswordVisibility: () => void;
};

class LoginStateHandler extends SignalStateHandler<LoginState, LoginActions> {
  private readonly formHandler = new FormStateHandler<LoginValues>({
    initialValues: {
      email: '',
      password: '',
    },
    validator: (values) => ({
      ...(values.email ? {} : { email: 'Email is required' }),
      ...(values.password ? {} : { password: 'Password is required' }),
    }),
  });

  constructor() {
    super({
      initialState: {
        isPasswordVisible: false,
      },
    });
  }

  getActions(): LoginActions {
    return {
      getFormHandler: () => this.formHandler,
      submitLogin: async (_values) => undefined,
      togglePasswordVisibility: () => {
        this.setState({
          isPasswordVisible: !this.getState().isPasswordVisible,
        });
      },
    };
  }
}
import { useStateFactory } from '@veams/status-quo/react';
import { FormProvider, useUncontrolledField } from '@veams/form/react';

function PasswordField({ isVisible }: { isVisible: boolean }) {
  const { meta, registerProps } = useUncontrolledField('password', {
    type: isVisible ? 'text' : 'password',
  });

  return (
    <label>
      Password
      <input {...registerProps} />
      {meta.showError ? <span>{meta.error}</span> : null}
    </label>
  );
}

function LoginFeature() {
  const [state, actions] = useStateFactory(() => new LoginStateHandler(), []);

  return (
    <FormProvider
      formHandlerInstance={actions.getFormHandler()}
      onSubmit={actions.submitLogin}
    >
      <PasswordField isVisible={state.isPasswordVisible} />
      <button onClick={actions.togglePasswordVisibility} type="button">
        Toggle password visibility
      </button>
      <button type="submit">Sign in</button>
    </FormProvider>
  );
}

Async Initial Values (onInit)

Sometimes the real initial values come from an API. Passing them synchronously is impossible, and applying them later with setFieldValue or resetForm makes the load look like a user change.

onInit solves this at the lifecycle level. initialValues stays required and acts as the synchronous skeleton; onInit loads the real baseline once, on the first connect:

import { FormStateHandler } from '@veams/form';

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

const profileForm = new FormStateHandler<ProfileValues>({
  // Synchronous skeleton — rendered while loading.
  initialValues: { name: '', email: '' },
  // Loads the real baseline. The signal aborts if the form disconnects first.
  onInit: async ({ signal }) => {
    const profile = await fetchProfile({ signal });
    return { name: profile.name, email: profile.email };
  },
  validator: validateProfile,
});

The form state exposes the lifecycle through initStatus:

  • 'ready' — values are the final baseline. Synchronous forms (no onInit) start here.
  • 'initializing'onInit is pending; values are still the skeleton.
  • 'error'onInit failed; initError holds the message.

Lifecycle rules:

  • onInit runs once, when the first consumer connects. Repeated connects do not re-run it.
  • If the form disconnects while loading, the AbortSignal fires, the stale result is dropped, and a later reconnect retries the load.
  • The resolved values are applied via initialize(): they become the new baseline for resetForm() and isDirty, touched state stays empty, and the validator does not run — validation starts with the first interaction.
  • While initStatus is 'initializing', FormProvider ignores submit events.

With the React bindings, FormProvider accepts onInit directly and useFormMeta() exposes the status:

import { FormProvider, useFormMeta, useUncontrolledField } from '@veams/form/react';

function ProfileFields() {
  const { initStatus, initError } = useFormMeta<ProfileValues>();

  if (initStatus === 'error') {
    return <p role="alert">Could not load your profile: {initError}</p>;
  }

  const isLoading = initStatus === 'initializing';

  return (
    <fieldset disabled={isLoading}>
      <NameField />
      <EmailField />
    </fieldset>
  );
}

function ProfileForm() {
  return (
    <FormProvider
      initialValues={{ name: '', email: '' }}
      onInit={({ signal }) => fetchProfileValues({ signal })}
      onSubmit={saveProfile}
      validator={validateProfile}
    >
      <ProfileFields />
      <button type="submit">Save</button>
    </FormProvider>
  );
}

Fields are not force-disabled during 'initializing' — disabling is a UI decision the consumer makes through initStatus, as shown above.

initialize() vs resetForm()

onInit is sugar over the public initialize(values) primitive. The two reset-like actions have a deliberate semantic split:

  • initialize(values)set a new baseline. Rebases initialValues, clears errors/touched, sets isDirty to false, marks initStatus as 'ready'. Use it whenever loaded data should become "what the form started from" — async prefills, switching the edited entity, loading a draft.
  • resetForm(values?)go back to the baseline. Without arguments it reverts to the current baseline. With values it sets them as current values but does not rebase — a later resetForm() still returns to the baseline.
const form = new FormStateHandler({ initialValues: { email: '' } });

form.initialize({ email: '[email protected]' }); // new baseline
form.setFieldValue('email', '[email protected]');
form.resetForm();                               // back to '[email protected]', not ''

A manual initialize() call also cancels a still-pending onInit — the explicit call wins.

Dirty Tracking (isDirty)

isDirty reports whether the current values deviate from the baseline (initialValues), using deep structural comparison. It answers a different question than touched:

  • touched"has the user interacted with this field?" (per field, set on blur)
  • isDirty"do the values differ from the baseline?" (whole form, value-based)

Typing a value and then typing the original value back makes the form clean again:

const form = new FormStateHandler({
  initialValues: { email: '[email protected]' },
});

form.getState().isDirty;                       // false
form.setFieldValue('email', '[email protected]');
form.getState().isDirty;                       // true
form.setFieldValue('email', '[email protected]');
form.getState().isDirty;                       // false — values match the baseline again

isDirty interacts with the baseline actions as follows:

| Action | Effect on isDirty | | --- | --- | | setFieldValue(...) | recomputed against the baseline | | resetForm() | false (values equal the baseline) | | resetForm(values) | recomputed — stays true if values differ from the (unrebased) baseline | | initialize(values) | false (the values are the new baseline) |

Typical consumer uses: an unsaved-changes guard, or disabling save buttons.

import { useFormMeta } from '@veams/form/react';

function SaveBar() {
  const { isDirty, isSubmitting } = useFormMeta<ProfileValues>();

  return (
    <button disabled={!isDirty || isSubmitting} type="submit">
      Save changes
    </button>
  );
}

Server-Driven Prefill Without Shadow State

initialize() and isDirty together replace the common "remember what we prefilled last time" boilerplate. A feature handler that prefills a form from a server query only needs one rule — never overwrite user edits:

class CompanyEditFormStateHandler extends NativeStateHandler<State, Actions> {
  private readonly formHandler = new FormStateHandler<CompanyValues>({
    initialValues: emptyCompanyValues,
    validator: validateCompany,
  });

  protected override onConnect(): void {
    this.bindSubscribable(this.companyProfileQuery, this.syncWithCompanyProfileQuery);
  }

  private syncWithCompanyProfileQuery = (snapshot: QuerySnapshot): void => {
    if (snapshot.status !== 'success') return;

    // The form knows whether the user edited anything since the last baseline.
    if (this.formHandler.getState().isDirty) return;

    // Apply the server data as the new baseline — not as a user change.
    this.formHandler.initialize(toFormValues(snapshot.data));
  };
}

The flow: every successful query emission rebases the form while it is untouched; as soon as the user edits anything, isDirty becomes true and prefills stop. No copies of the last prefilled values, no manual comparison helpers. As a bonus, a user-triggered resetForm() returns to the prefilled baseline instead of the empty skeleton.

Controlled Components

Use Controller when a third-party field expects value and onChange instead of native uncontrolled props. It supports the same validationMode and revalidationMode overrides as useUncontrolledField().

import { Controller, FormProvider } from '@veams/form/react';

function ControlledRoleSelect() {
  return (
    <Controller
      name="role"
      render={({ field, fieldState }) => (
        <>
          <RoleSelect onBlur={field.onBlur} onChange={field.onChange} value={field.value as string} />
          {fieldState.touched && fieldState.error ? <span>{fieldState.error}</span> : null}
        </>
      )}
    />
  );
}

function RoleForm() {
  return (
    <FormProvider
      initialValues={{ role: 'user' }}
      onSubmit={(values) => saveRole(values.role)}
    >
      <ControlledRoleSelect />
      <button type="submit">Save</button>
    </FormProvider>
  );
}

Form-Level Submit Errors

Keep backend errors that are not tied to one field out of the field error map. Use setSubmitError() for those cases and read aggregate state through useFormMeta().

import { FormProvider, useFormMeta } from '@veams/form/react';

function SubmitErrorBanner() {
  const { submitError } = useFormMeta<{ email: string; password: string }>();

  return submitError ? <p role="alert">{submitError}</p> : null;
}

Schema Validators (Zod)

@veams/form does not depend on Zod, but it exposes a lightweight adapter for Zod-style safeParse schemas. The package currently includes only the Zod adapter because that is the most common schema setup in current usage. PRs for additional adapters are welcome as long as the package remains dependency-free.

import { z } from 'zod';
import { FormStateHandler } from '@veams/form';
import { toZodValidator } from '@veams/form/validators/zod';

const loginSchema = z.object({
  email: z.string().min(1, 'Email is required').email('Enter a valid email address'),
  password: z.string().min(12, 'Use at least 12 characters'),
});

type LoginValues = z.infer<typeof loginSchema>;

const form = new FormStateHandler<LoginValues>({
  initialValues: {
    email: '',
    password: '',
  },
  validator: toZodValidator(loginSchema),
});

If you work directly with FormStateHandler, setFieldValue(name, value, { validate: false }) updates the value without rerunning the validator. The React bindings use that option internally when a field is configured to wait for blur or submit before validating.