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

@s-chat/form-manager

v2.1.1

Published

[![pipeline status](https://gitlab.com/shaggrath-projects-1/public-projects/form-manager/badges/main/pipeline.svg)](https://gitlab.com/shaggrath-projects-1/public-projects/form-manager/-/commits/main) [![coverage report](https://gitlab.com/shaggrath-proje

Downloads

132

Readme

@s-chat/form-manager

pipeline status coverage report npm version npm downloads license

Typed React form manager with LIVR validation.

The library helps describe form fields as a schema, render inputs from that schema, keep form values in React state, validate values through livr, and reset or update the form from code.

Installation

npm install @s-chat/form-manager

The package uses React and LIVR. React is expected to be available in your application.

Quick Start

import { FormEvent } from 'react';
import {
  FormManagerProvider,
  FormSchema,
  useFormManager,
} from '@s-chat/form-manager';

type LoginFormValues = {
  email: string;
  password: string;
  rememberMe: boolean;
  recoveryEmail: string;
};

const loginFormSchema: FormSchema<LoginFormValues> = {
  email: {
    label: 'Email',
    type: 'email',
    defaultValue: '',
    validationRules: ['required', 'email'],
    htmlAttr: {
      placeholder: '[email protected]',
      autoComplete: 'email',
    },
  },
  password: {
    label: 'Password',
    type: 'password',
    defaultValue: '',
    validationRules: ['required', { min_length: 8 }],
    htmlAttr: {
      autoComplete: 'current-password',
    },
  },
  rememberMe: {
    label: 'Remember me',
    type: 'checkbox',
    defaultValue: false,
    validationRules: [],
  },
  recoveryEmail: {
    label: 'Recovery email',
    type: 'email',
    defaultValue: '',
    validationRules: ['email'],
    showIf: {
      rememberMe: true,
    },
  },
};

function LoginForm() {
  const {
    fields,
    formFieldObject,
    validateForm,
    resetForm,
    bulkUpdateForm,
  } = useFormManager<LoginFormValues>(loginFormSchema);

  const handleSubmit = (event: FormEvent) => {
    event.preventDefault();

    const validData = validateForm();

    if (!validData) {
      return;
    }

    console.log('Valid form data:', validData);
  };

  return (
    <form onSubmit={handleSubmit}>
      {fields.map((field) => (
        <label key={String(field.key)} htmlFor={String(field.key)}>
          {field.label}
          <input {...field.inputProps} />

          {field.isError && (
            <span>{field.errorMessage}</span>
          )}
        </label>
      ))}

      <pre>{JSON.stringify(formFieldObject, null, 2)}</pre>

      <button type="submit">Submit</button>
      <button type="button" onClick={resetForm}>Reset</button>
      <button
        type="button"
        onClick={() => bulkUpdateForm({ email: '[email protected]' })}
      >
        Fill demo email
      </button>
    </form>
  );
}

export function App() {
  return (
    <FormManagerProvider>
      <LoginForm />
    </FormManagerProvider>
  );
}

Field Schema

Each form is described by FormSchema<ValuesType>. The schema keys must match the keys of your form values type.

type FormSchema<ValuesType> = {
  [key in keyof ValuesType]: FieldSchema<ValuesType[key], ValuesType>;
};

A field can contain:

| Property | Type | Description | | --- | --- | --- | | label | string | Human-readable field label. Returned as field.label. | | type | string | Input type, for example text, email, password, checkbox, select. | | validationRules | LIVRRuleDefinition | LIVR rules used by validateForm(). | | defaultValue | ValueType | Initial value and the value restored by resetForm(). | | options | FieldOptionType[] | Options for select-like controls. | | showIf | Partial<ValuesType> | Conditions that control whether the field keeps its original type or becomes hidden. | | htmlAttr | Record<string, any> | Extra props merged into field.inputProps. | | errorMessages | Record<string, string> | Field-level error message metadata. | | onBeforeChange | (value) => void | Called before the field state is updated. | | onAfterChange | (value) => void | Called after the field state update is dispatched. |

Conditional Fields

Use showIf to show a field only when other form values match specific values. The keys in showIf are form field keys, and the values are compared with the current form values.

type ProfileFormValues = {
  accountType: 'personal' | 'company';
  companyName: string;
};

const profileFormSchema: FormSchema<ProfileFormValues> = {
  accountType: {
    label: 'Account type',
    type: 'select',
    defaultValue: 'personal',
    validationRules: ['required'],
    options: [
      { label: 'Personal', value: 'personal' },
      { label: 'Company', value: 'company' },
    ],
  },
  companyName: {
    label: 'Company name',
    type: 'text',
    defaultValue: '',
    validationRules: ['required'],
    showIf: {
      accountType: 'company',
    },
  },
};

When the form is initialized, showIf is calculated from field defaultValues. If the condition is not satisfied, the field receives inputProps.type = 'hidden'.

When a form value changes through onChange() or bulkUpdateForm(), all fields with showIf are recalculated:

| Condition | Result | | --- | --- | | All showIf values match current form values | The field receives its original type. | | At least one showIf value does not match | The field receives type: 'hidden'. |

The field value is not cleared when the field becomes hidden.

Rendering Fields

useFormManager() returns a normalized fields array. Each item contains:

| Property | Description | | --- | --- | | key | Field key from the schema. | | label | Field label from the schema. | | inputProps | Props for your input component: value, type, name, id, options, onChange, plus htmlAttr. | | isError | true when the field has a validation error. | | errorMessage | Validation message mapped through FormManagerProvider.errorMessages, or the original LIVR error code when no mapping exists. | | isRequired | true when validationRules includes required. |

Example with a custom renderer:

{fields.map((field) => {
  if (field.inputProps.type === 'select') {
    return (
      <select
        key={String(field.key)}
        value={field.inputProps.value}
        name={String(field.inputProps.name)}
        id={String(field.inputProps.id)}
        onChange={field.inputProps.onChange}
      >
        {field.inputProps.options?.map((option) => (
          <option key={String(option.value)} value={String(option.value)}>
            {option.label}
          </option>
        ))}
      </select>
    );
  }

  return (
    <input key={String(field.key)} {...field.inputProps} />
  );
})}

Validation

Validation is performed by validateForm().

const validData = validateForm();

if (validData) {
  // validData has the same type as your form values type
}

If the form is valid, validateForm() returns validated data. If the form is invalid, it updates field errors and returns void.

Validation errors are resolved through FormManagerProvider.errorMessages. LIVR returns error codes such as REQUIRED or WRONG_EMAIL; the hook uses those codes as keys and stores the mapped message in field.errorMessage. If a code is not present in errorMessages, the original code is used as the field message.

The hook registers livr-extra-rules automatically, so the additional rules from that package can be used in validationRules.

Form State Helpers

useFormManager() returns:

| Helper | Description | | --- | --- | | formFieldObject | Current form values object. | | validateForm() | Validates the form and returns typed valid data or void. | | resetForm() | Restores all fields to their defaultValue. | | bulkUpdateForm(updateData) | Updates multiple field values at once. |

Example:

bulkUpdateForm({
  email: '[email protected]',
  rememberMe: true,
});

Provider Configuration

FormManagerProvider can be used to customize value extraction and register custom LIVR rules.

import { ChangeEvent } from 'react';
import { FormManagerProvider } from '@s-chat/form-manager';

export function App() {
  return (
    <FormManagerProvider
      errorMessages={{
        REQUIRED: 'This field is required',
        WRONG_EMAIL: 'Enter a valid email address',
        NOT_EVEN_NUMBER: 'Enter an even number',
      }}
      fieldValueSelector={{
        checkbox: (event: ChangeEvent<HTMLInputElement>) => event.target.checked,
        number: (event: ChangeEvent<HTMLInputElement>) => Number(event.target.value),
        default: (event: ChangeEvent<HTMLInputElement>) => event.target.value,
      }}
      customValidationRules={{
        even_number: () => (value: number) => (
          value % 2 === 0 ? undefined : 'NOT_EVEN_NUMBER'
        ),
      }}
      options={{
        isValidateOnChange: false,
      }}
    >
      <LoginForm />
    </FormManagerProvider>
  );
}

errorMessages

errorMessages maps LIVR error codes to user-facing validation messages. Provider messages are merged with the default messages.

<FormManagerProvider
  errorMessages={{
    REQUIRED: 'Required field',
    CANNOT_BE_EMPTY: 'Required field',
    WRONG_EMAIL: 'Invalid email',
  }}
>
  <Form />
</FormManagerProvider>

Default messages include:

| Code | Default message | | --- | --- | | DEFAULT | Wrong format | | REQUIRED | Current field is required! | | CANNOT_BE_EMPTY | Current field is required! | | NOT_DECIMAL | Wrong data type! | | NOT_POSITIVE_DECIMAL | Wrong data type! | | NOT_INTEGER | Wrong data type! | | NOT_POSITIVE_INTEGER | Wrong data type! | | WRONG_EMAIL | Is invalid email! | | FIELDS_NOT_EQUAL | Not equal! | | TOO_LOW | Not valid value! | | TOO_HIGH | Not valid value! | | DATE_UNDER_MAX_RANGE | Date is under max range! | | DATE_BELOW_MIN_RANGE | Date is below min range! |

Custom validation rules can return custom error codes. Add matching keys to errorMessages to display custom text for those errors.

fieldValueSelector

fieldValueSelector defines how onChange arguments are converted into field values.

Default selectors:

| Type | Selector | | --- | --- | | checkbox | event.target.checked | | default | event.target.value |

The hook chooses the selector by the field's initial type. If no selector for that type exists, it uses default.

options.isValidateOnChange

Pass options.isValidateOnChange = true to validate after field changes.

const form = useFormManager(formSchema, {
  isValidateOnChange: true,
});

The provider can also define the default value for this option:

<FormManagerProvider options={{ isValidateOnChange: true }}>
  <Form />
</FormManagerProvider>

Type Reference

FieldOptionType<ValueType = string>

type FieldOptionType<ValueType = string> = {
  label: ReactNode;
  value: ValueType;
};

Used for select, radio, checkbox group, or any custom option-based input.

ErrorListType

type ErrorListType = Record<string, string>;

Map of validation error codes to messages.

FormManagerInputProps

type FormManagerInputProps<ValuesType, KeyType extends keyof ValuesType> = {
  value: ValuesType[KeyType];
  type: string;
  name: KeyType;
  id: KeyType;
  options?: FieldOptionType[];
  onChange: (...attrs: any[]) => void;
} & Record<string, any>;

Props generated for an input component. htmlAttr from the field schema is merged into this object.

FormManagerField

type FormManagerField<ValuesType, KeyType extends keyof ValuesType> = {
  inputProps: FormManagerInputProps<ValuesType, KeyType>;
  key: KeyType;
  label: string;
  isError: boolean;
  errorMessage: string;
  isRequired: boolean;
};

Normalized field data returned in the fields array.

FieldSchema

type FieldSchema<ValueType = string, ValuesType = UniversalValuesType> = {
  label: string;
  type: string;
  validationRules: LIVRRuleDefinition;
  defaultValue?: ValueType;
  options?: FieldOptionType[];
  showIf?: Partial<{ [key in keyof ValuesType]: ValuesType[key] }>;
  htmlAttr?: Record<string, any>;
  errorMessages?: ErrorListType;
  onBeforeChange?: (value: ValueType) => void;
  onAfterChange?: (value: ValueType) => void;
};

showIf is typed against the full form values type, so a field can depend on the value of any other field in the same FormSchema.

UseFormManagerReturnType

type UseFormManagerReturnType<ValuesType> = {
  fields: FormManagerField<ValuesType>[];
  formFieldObject: ValuesType;
  validateForm: () => ValuesType | void;
  resetForm: () => void;
  bulkUpdateForm: (updateData: Partial<ValuesType>) => void;
};

Return type of useFormManager().

Build

npm run build

The build script creates CommonJS output in dist and ESM output in dist/ejs.

Tests

npm run test:run

The test suite uses Vitest with jsdom and React Testing Library. It covers conditional fields through showIf, validation message mapping through FormManagerProvider.errorMessages, bulkUpdateForm(), resetForm(), and isValidateOnChange.

For watch mode:

npm test

To generate a coverage report:

npm run test:coverage

The HTML report is written to coverage/.