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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@loform/react

v4.6.9

Published

loform is light, easy to use and extendable form validation library written in TypeScript.

Downloads

127

Readme

loform

loform is light, easy to use and extendable form validation library written in TypeScript. Currently available for React.

See Examples in Storybook here

Why not Redux-Form?

Below is a quote from the authors of Formik

By now, you might be thinking, "Why didn't you just use Redux-Form?" Good question.

  1. According to our prophet Dan Abramov, form state is inherently ephemeral and local, so tracking it in Redux (or any kind of Flux library) is unnecessary
  2. Redux-Form calls your entire top-level Redux reducer multiple times ON EVERY SINGLE KEYSTROKE. This is fine for small apps, but as your Redux app grows, input latency will continue to increase if you use Redux-Form.
  3. Redux-Form is 22.5 kB minified gzipped (Formik is 7.8 kB)

"Why should you choose loform over Formik then?", you may ask.

  1. Sometimes size matter, and loform is lighter than Formik.
  2. Less mess. In loform, validation is sole responsibility of an input. If you delete an input, you don't need to worry about updating your form.
  3. More complex forms, easier to maintain. You can create and manage state of only one form in Formik, while loform allows you to control multiple forms by sharing same instance of FormService
  4. With loform you can submit your Form outside of Form component. Actually, you can do it anywhere in the application using FormEventEmitter. You cannot do that with Formik.

Table of Contents

It can be used with TypeScript (definition files included) and pure JavaScript.

React

Module size

23kb minified (5kb gzipped)

loform for React was inspired by Render Props concept. Here's why to use Render Props

Requirements


  • React and React DOM version ^16.5.0

Go straight to Docs

Installation


npm

npm install @loform/react --save

yarn

yarn add @loform/react

Usage


All examples are in JavaScript

Basic form

import React from 'react';
import { Form, TextInput, PasswordInput, emailValidator } from '@loform/react';

const renderErrors = (errors, inputName) =>
  errors[inputName] &&
  errors[inputName].length && // Since version 4.0 you will always receive array for a given field. If the field is valid, array of errors should be empty.
  errors[inputName].map((error, index) => (
    <span key={index} className="error">
      {error}
    </span>
  ));

const LoginForm = () => (
  <Form className="form" onSubmit={values => console.log(values)}>
    {({ submit, errors }) => (
      <>
        {/* Since version 3.0 you control styles and rendering order of errors */}
        {renderErrors(errors, 'email')}
        <TextInput
          className="emailInput"
          name="email"
          value="[email protected]"
          placeholder="Enter email address"
          validators={[emailValidator('Value is not a valid email address')]}
          required
          requiredMessage="Email is required."
        />
        {renderErrors(errors, 'password')}
        <PasswordInput
          name="password"
          required
          requiredMessage="Password is required."
        />
        <button onClick={() => submit()}>Submit form</button>
      </>
    )}
  </Form>
);

Custom input

In order for input to work, you need to wrap it with FormInputDecorator HOC

Props passed by FormInputDecorator HOC
  • id: string
  • name: string
  • value: any
  • onChange: (value?: any) => any
  • onBlur: (event: React.FocusEvent) => any
  • disabled?: boolean
  • placeholder?: string
  • ...rest all other props given to the HOC will be passed down to your component (eg. options in SelectInput)
import React from 'react';
import classnames from 'classnames';
import { FormInputDecorator } from '@loform/react';

const ON = 'on';
const OFF = 'off';

export const SwitchInput = ({ onChange, onBlur, hasErrors, value }) => (
  <div
    className={classnames('switchInput', { switchInput__hasErrors: hasErrors })}
  >
    SWITCH ME ON
    <input
      type="radio"
      value={ON}
      checked={value === ON}
      onChange={() => onChange(ON)}
      onBlur={onBlur} // You need to pass onBlur function, in order for onInputBlur validation to work
    />
    <input
      type="radio"
      value={OFF}
      checked={value === OFF}
      onChange={() => onChange(OFF)}
      onBlur={onBlur} // As above
    />
  </div>
);

export default FormInputDecorator(SwitchInput);

Usage:

const LoginForm = () => (
  <Form className="form" onSubmit={values => console.log(values)}>
    {({ submit, errors }) => (
      <>
        {errors.switch &&
          errors.switch.map((error, index) => (
            <span className="error">{error}</span>
          ))}
        <SwitchInput
          name="switch"
          hasErrors={!!errors.switch}
          validators={[
            {
              errorMessage: 'Switch should be on to submit',
              validate: value => value === 'on',
            },
          ]}
          value="off"
        />
        <button onClick={() => submit()}>Submit form</button>
      </>
    )}
  </Form>
);

The checkbox input problem

Consider you have a standard html form with <input name="agreement" value="accepted" checked="checked" /> element. On it's submission you'll probably expect a data structure equal to the following json:

{ "agreement": "accepted" }

And if the input wasn't checked, you wouldn't get any data.

This means, that marked checkbox has a value of accepted and checked attribute set to true, while unmarked checkbox has a value of undefined and checked attribute set to false. This logic is unnecessarily complicated and neglects the existence of a boolean type. Infact, you can recreate this logic in loform with the following ComplicatedCheckbox component:

import React from 'react';
import { FormInputDecorator } from '@loform/react';

class ComplicatedCheckbox extends React.Component {
  constructor(props) {
    super(props);

    this.handleChange = this.handleChange.bind(this);
    this.state = {
      checked: this.props.checked || false,
      initialValue: this.props.value || '',
    };
  }

  handleChange(event) {
    const isChecked = event.target.checked;

    if (isChecked) {
      this.props.onChange(this.state.initialValue);
    } else {
      this.props.onChange(undefined);
    }

    this.setState({
      checked: isChecked,
    });
  }

  render() {
    const { id, name, disabled } = this.props;

    return (
      <input
        id={id}
        name={name}
        disabled={disabled}
        value={this.state.initialValue}
        checked={this.state.checked}
        onChange={this.handleChange}
        onBlur={this.props.onBlur}
        type="checkbox"
      />
    );
  }
}

export default FormInputDecorator(ComplicatedCheckbox);

Core concept of loform is that an input can have a single value identified by it's name. The following component is available by importing CheckboxInput:

import * as React from 'react';
import { FormInputDecorator } from '@loform/react';

const CheckboxInput = ({
  id,
  name,
  value = false, // We expect a boolean type as a value
  disabled,
  onChange = () => {},
  onBlur,
  ...rest
}) => {
  return (
    <input
      {...rest}
      id={id}
      name={name}
      checked={value}
      disabled={disabled}
      type="checkbox"
      onChange={e => onChange(e.target.checked)}
      onBlur={onBlur}
    />
  );
};

export default FormInputDecorator(CheckboxInput);

It's simply using native input's checked attribute to pass as a value. You can use it as shown below:

<CheckboxInput name="hasAgreed" value={true} />

If it is checked, the form values will be equal

{
  hasAgreed: true,
}

and

{
  hasAgreed: false,
}

if otherwise.

Advanced form

import { Form, TextInput, FormEventEmitter, FormService } from '@loform/react';

const formEventEmitter = new FormEventEmitter();
const formService = new FormService();

const renderErrors = (errors, inputName) =>
  errors[inputName] &&
  errors[inputName].map((error, index) => (
    <span key={index} className="error">
      {error}
    </span>
  ));

const AddressForm = () => (
  <Form
    formEventEmitter={formEventEmitter}
    formService={formService}
    onSubmit={values => console.log(values)}
  >
    {({ errors }) => (
      <>
        {renderErrors(errors, 'name')}
        <TextInput name="name" placeholder="Name" required />
        {renderErrors(errors, 'street')}
        <TextInput name="street" placeholder="Street" required />
        {renderErrors(errors, 'city')}
        <TextInput name="city" placeholder="City" required />
      </>
    )}
  </Form>
);

const OtherComponent = () => (
  <div>
    <AddressForm />
    <button onClick={() => formEventEmitter.submit()}>Submit outside</button>
  </div>
);

Later in code

const formValues = formService.getValuesFromInputs();

Components


Form

Props

| Name | Type | Required | Description | | :----------------- | :----------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | onSubmit | Function | true | Callback called with FormValues on successful form submit | | className | String | false | Class name added to form element | | onError | Function | false | Callback called with FormErrors on unsuccessful form submit | | clearOnSubmit | Boolean | false | Tells the form to clear inputs upon successful submission | | formService | FormService | false | Service that handles input registration and validation | | formEventEmitter | FormEventEmitter | false | Service that handles submit and update events | | validationStrategy | FormValidationStrategy | false | Default value: onInputBlur. There are more strategies you can use: onlyOnSubmit, onInputChange. You can easily write one yourself. See Validation Strategies section for more info. |

Form requires it's children to be a render function. What it means is that instead of strings, components or array of them you pass a function that returns them:

<Form onSubmit={values => console.log(values)}>
  {(form) => (
    {/* You can access invidual input errors by form.errors object as follows: */}
    {form.errors.username && <span>form.errors.username</span>}
    {/* You must remember that form.errors.username is either undefined or an array */}
    <TextInput name="username" placeholder="Enter username" required />

    {/* You can submit form by calling form.submit() */}
    <button onClick={() => form.submit()}>Submit</button>
    {/* Clear form by calling form.clear() */}
    <button onClick={() => form.clear()}>Clear</button>
  )}
</Form>

Our render function argument consists of following properties:

| Name | Description | | :----------- | :------------------------------------------------------------------------------------------------------------------------------- | | clear | A function that clears form inputs | | submit | A function that submits our form | | errors | FormErrors object | | isValidating | A boolean indicating that form is being validated. Useful with async validators. You can read about it here |

Inputs


FormInput

All inputs extend functionality provided by FormInput component. Checkout here how to create custom input with FormInputDecorator.

Props

| Name | Type | Required | Description | | :--------------- | :--------- | :------- | :---------------------------------------------------------------------------------------------------------------------- | | controlled | Boolean | false | If true, the input value is controlled by the user | | required | Boolean | false | If true, displays error when user is trying to submit form with empty input | | requiredMessage | String | false | Replaces default required error message | | validators | Array | false | Array of InputValidator that input should be validated against upon form submission | | onChange | Function | false | Function called on input value change with it's value | | onBlur | Function | false | Function called on input blur | | debounce | Number | false | Debounce input value (default: 0). Used primarily with async validators | | validateOnChange | Boolean | false | Tells input if should validate on change. Default value is true. Can be set to false to optimize number of requests |

Input

Props

| Name | Type | Required | Description | | :--------------------------------- | :-------- | :------- | :----------------------------------------------------------------------------------------------------------- | | id | String | false | Id of an input. Must be unique. Used internally to identify input in FormService. Generated uuid by default. | | name | String | true | Name of an input. Used to generate FormValues on form submission. | | value | String | false | Can be used to set initial value of an input or to control input's value during it's lifecycle | | disabled | Boolean | false | Can be set to true in order to disable input | | placeholder | String | false | If set, displayed as placeholder of an input | | className | String | false | Class name added to input element | | type | String | false | The type of an input. Default value is text | | ...rest | any[] | false | Any other value you pass as a prop is passed down to the native input (e.g. pattern) | | Props from FormInput | - | - | - |

Example:

<Form onSubmit={onSubmit}>
  {({ submit }) => (
    <>
      <Input
        placeholder="Enter quantity"
        name="quantity"
        type="number"
        min={10}
        max={100}
      />
      <button onClick={() => submit()} />
    </>
  )}
</Form>

TextInput

Props

| Name | Type | Required | Description | | :--------------------------------- | :-------- | :------- | :----------------------------------------------------------------------------------------------------------- | | id | String | false | Id of an input. Must be unique. Used internally to identify input in FormService. Generated uuid by default. | | name | String | true | Name of an input. Used to generate FormValues on form submission. | | value | String | false | Can be used to set initial value of an input or to control input's value during it's lifecycle | | disabled | Boolean | false | Can be set to true in order to disable input | | placeholder | String | false | If set, displayed as placeholder of an input | | className | String | false | Class name added to input element | | Props from FormInput | - | - | - |

PasswordInput

Props

| Name | Type | Required | Description | | :--------------------------------- | :-------- | :------- | :----------------------------------------------------------------------------------------------------------- | | id | String | false | Id of an input. Must be unique. Used internally to identify input in FormService. Generated uuid by default. | | name | String | true | Name of an input. Used to generate FormValues on form submission. | | value | String | false | Can be used to set initial value of an input or to control input's value during it's lifecycle | | disabled | Boolean | false | Can be set to true in order to disable input | | placeholder | String | false | If set, displayed as placeholder of an input | | className | String | false | Class name added to input element | | Props from FormInput | - | - | - |

TextAreaInput

Props

| Name | Type | Required | Description | | :--------------------------------- | :-------- | :------- | :----------------------------------------------------------------------------------------------------------- | | id | String | false | Id of an input. Must be unique. Used internally to identify input in FormService. Generated uuid by default. | | name | String | true | Name of an input. Used to generate FormValues on form submission. | | value | String | false | Can be used to set initial value of an input or to control input's value during it's lifecycle | | disabled | Boolean | false | Can be set to true in order to disable input | | className | String | false | Class name added to input element | | Props from FormInput | - | - | - |

CheckboxInput

Props

| Name | Type | Required | Description | | :--------------------------------- | :-------- | :------- | :----------------------------------------------------------------------------------------------------------- | | id | String | false | Id of an input. Must be unique. Used internally to identify input in FormService. Generated uuid by default. | | name | String | true | Name of an input. Used to generate FormValues on form submission. | | value | Boolean | false | Can be used to set initial value of an input or to control input's value during it's lifecycle | | disabled | Boolean | false | Can be set to true in order to disable input | | className | String | false | Class name added to input element | | Props from FormInput | - | - | - |

SelectInput

Props

| Name | Type | Required | Description | | :--------------------------------- | :-------- | :------- | :----------------------------------------------------------------------------------------------------------- | | id | String | false | Id of an input. Must be unique. Used internally to identify input in FormService. Generated uuid by default. | | name | String | true | Name of an input. Used to generate FormValues on form submission. | | value | String | false | Can be used to set initial value of an input or to control input's value during it's lifecycle | | options | Array | false | Array of Options | | disabled | Boolean | false | Can be set to true in order to disable input | | className | String | false | Class name added to input element | | Props from FormInput | - | - | - |

RadioInput

Props

| Name | Type | Required | Description | | :--------------------------------- | :------- | :------- | :----------------------------------------------------------------------------------------------------------- | | id | String | false | Id of an input. Must be unique. Used internally to identify input in FormService. Generated uuid by default. | | name | String | true | Name of an input. Used to generate FormValues on form submission. | | value | String | false | Can be used to set initial value of an input or to control input's value during it's lifecycle | | options | Array | false | Array of Options | | className | String | false | Class name added to input element | | Props from FormInput | - | - | - |

Types


Option

{
  label: string;
  value: string;
  disabled?: boolean;
}

InputValidator

InputValidator is an object which contains errorMessage as a string and a validation function. Validate function takes validated field value as the first parameter and FormValues object as the second parameter. It must return true if input is successfully validated and false if otherwise.

{
  errorMessage: string;
  validate: (value: string, formValues: FormValues) => Promise<boolean> | boolean;
}
Async validators

Since version 4.0 you can return a promise in validate function. Promise should resolve to a boolean value, indicating successful or unsuccessful validation.

Example:

const usernameAvailabilityValidator = {
  errorMessage: 'Username is not available',
  validate: value =>
    new Promise(resolve => {
      axios.get(someUrl, { params: { username: value } }).then(({ data }) => {
        resolve(data.is_available);
      });
    }),
};

If you want to inform users that your form is being validated, you can use isValidating boolean render function param. Example:

<Form onSubmit={onSubmit}>
  {({ submit, errors, isValidating }) => (
    <>
      {isValidating && 'Form is being validated...'}
      <TextInput
        name="username"
        validators={[usernameAvailabilityValidator]}
        required
        validateOnChange={false} // You can disable validation on change to optimize number of requests to a server
        debounce={1000} // Or you can debounce input change by number of miliseconds. Remember that this won't take effect if you set validateOnChange prop to false
      />
      <button onClick={() => submit()} />
    </>
  )}
</Form>

You can see example of async validation here

FormValues

FormValues is an object representing all of the current form values. Example:

{
  firstName: 'John',
  lastName: 'Doe',
  userName: 'john.doe',
  languages: ['PL', 'EN', 'DE'],
  street: {
    name: 'Corner Street',
    number: '180F2'
  }
}

FormErrors

FormErrors is an object representing invalid inputs with error messages. Error messages are identified by input name prop.

For example, for a form below:

<Form onSubmit={onSubmit}>
  {({ submit, errors }) => (
    <>
      <TextInput
        name="email"
        validators={[emailValidator('Invalid email address')]}
        required
      />
      <TextInput
        name="phone"
        validators={[phoneValidator('Incorrect phone format')]}
      />
      <TextInput name="description" />
      <TextInput name="language[]" value="en" />
      <TextInput
        name="language[]"
        validators={[customLanguageValidator('Incorrect language')]}
      />
      <button onClick={() => submit()} />
    </>
  )}
</Form>

we can receive error structure like this:

{
  email: [
    'Input email is required',
    'Invalid email address'
  ],
  description: [],
  phone: [
    'Incorrect phone format'
  ],
  language: [
    [],
    ['Incorrect language']
  ]
}

Note that valid fields are identified by empty array of errors

InputDescriptor

InputDescriptor is a representation of an input used by FormService and FormEventEmitter

{
  id: string;
  name: string;
  value?: any;
  required: boolean;
  requiredMessage?: string;
  validators?: InputValidator[];
  validateOnChange?: boolean;
}

FormEvent

FormEvent is an enum that can contain following values: "submit", "update", "blur", "clear".

If you are using TypeScript, you will need to use FormEvent.Submit, FormEvent.Update, FormEvent.Clear or FormEvent.Blur enum value.

Please note that Form.Update and Form.Blur event handlers receive InputDescriptor as an argument.

import { FormEvent } from '@loform/react';

and later in code:

const onUpdate = inputDescriptor => console.log(inputDescriptor);

formEventEmitter.addListener(FormEvent.Update, onUpdate);

Services


FormService

FormService is used internally in order to handle inputs, validation and other tasks. For more advanced use can be injected to Form through formService prop.

Methods

Documentation is in development. For FormService methods reference use TypeScript declaration files.

FormEventEmitter

FormEventEmitter is used internally to handle submit and update events. For more advanced use can be injected to Form through formEventEmitter prop.

See example usage of FormEventEmitter

Methods

Documentation is in development and incomplete. For all FormEventEmitter methods reference use TypeScript declaration files.

  • clear()
  • submit()
  • update(input: InputDescriptor)
  • blur(input: InputDescriptor)
  • addListener(event: FormEvent, callback: (...args: any[]) => any)
    • callback for FormEvent.Update and FormEvent.Blur event receives InputDescriptor as a parameter
  • removeListener(event: FormEvent, callback: (...args: any[]) => any)

Check FormEvent type

Validation Strategies


Form can use different validation strategies. Validation Strategies are used to tell the form how to update errors that you receive as a parameter in render function, on form mount, input change and input blur events.

You can see an example of different validation strategies for a registration form on Storybook

There are three strategies available, but you can easily create your own strategy by implementing FormValidationStrategy interface:

type FormErrorsMap = Map<string, string[]>;

interface FormValidationStrategy {
  getErrorsOnFormMount?: (
    errors: FormErrorsMap,
    prevErrors: FormErrorsMap,
  ) => FormErrorsMap;
  getErrorsOnInputBlur?: (
    input: InputDescriptor,
    errors: FormErrorsMap,
    prevErrors: FormErrorsMap,
  ) => FormErrorsMap;
  getErrorsOnInputUpdate?: (
    input: InputDescriptor,
    errors: FormErrorsMap,
    prevErrors: FormErrorsMap,
  ) => FormErrorsMap;
}

FormErrorsMap is a javascript Map object, that contains array of errors as strings identified by input id as string. Input id is accessible using InputDescriptor's id property.

The following is an implementation of onlyOnSubmit strategy, which, on input update, removes errors that were corrected since last submit:

const onlyOnSubmit = {
  getErrorsOnInputUpdate: (input, errors, prevErrors) => {
    const newErrors = new Map();

    for (const [inputId, inputErrors] of Array.from(errors.entries())) {
      newErrors.set(
        inputId,
        inputErrors.filter(error =>
          (prevErrors.get(inputId) || []).includes(error),
        ),
      );
    }

    return newErrors;
  },
};

Note that if you don't define a specific method, errors won't be updated.

Example usage:

import { Form, onlyOnSubmit } from '@loform/react';

const RegistrationForm = () => (
  <Form
    className={styles.form}
    onSubmit={values => console.log(values)}
    onError={errors => console.log(errors)}
    validationStrategy={onlyOnSubmit}
  >
    {({ submit, errors }) => (
      // ...
    )}
  </Form>
);

Development

Project is written in TypeScript and compiled to JavaScript using Webpack.

In order to develop this application you need to install dependencies using yarn:

yarn install

Exemplary components are rendered during development using Storybook:

npm run storybook

Contributing

In order to contribute to loform you need to use conventional commits.

You can freely issue a pull request to the master branch. Mention author for code review before any merges.

If there is a problem issuing a pull request, contact author.