@loform/react
TypeScript icon, indicating that this package has built-in type declarations

4.6.9 • Public • Published

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="example@email.com"
          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.

Package Sidebar

Install

npm i @loform/react

Weekly Downloads

38

Version

4.6.9

License

MIT

Unpacked Size

244 kB

Total Files

154

Last publish

Collaborators

  • awinogrodzki