revalidation

Higher Order Component for Validating Forms in React

View the Project on GitHub

Revalidation

Higher Order Component for Forms in React

Revalidation lets you write your forms as stateless function components, taking care of managing the local form state as well as the validation. Revalidation also works with classes and will support other React-like libraries like Preact or Inferno in the future.

Use Case

Form handling sounds trivial sometimes, but let’s just take a second to think about what is involved in the process. We need to define form fields, we need to validate the fields, we also might need to display errors according to the fact if the input validates, furthermore we need to figure out if the validation is instant or only after clicking on a submit button and so on and so forth.

Why Revalidation?

There are a number of solutions already available in React land, each with there own approach on how to tackle aforementioned problems. Revalidation is another approach on taming the problems that come with forms by only doing two things: managing the local form component state and validating against a defined set of rules. There is no real configuration and very little is hidden away from user land. This approach has pros and cons obviously. The benefit we gain, but declaring an initial state and a set of rules is that we can reuse parts of the spec and compose those specs to bigger specs. The downside is that Revalidation doesn’t abstract away the form handling itself. The only configurations available are validateSingle and validateOnChange, while the first enables to define if the predicates functions are against all fields or only that one updated, the latter enables to turn dynamic validation on and off all together. This is it. Everything is up to the form implementer.

Revalidation enhances the wrapped Component by passing a revalidation prop containing a number of properties and functions to manage the state. There are no automatic field updates, validations or onsubmit actions, Revalidation doesn’t know how the form is implemented or how it should handle user interactions.

Let’s see an example to get a better idea on how this could work. For example we would like to define a number of validation rules for two inputs, name and random. More often that not, inside an onChange(name, value) f.e, we might start to hard code some rules and verify them against the provided input:


onChange(name, value) {
  if (name === 'lastName') {
    if (hasCapitalLetter(lastName)) {
      // then do something
    }
  }  
  // etc...
}

This example might be exaggerated but you get the idea. Revalidation takes care of running your predicate functions against defined field inputs, enabling to decouple the actual input from the predicates.

const validationRules = {
  name: [
    [ isGreaterThan(5),
      `Minimum Name length of 6 is required.`
    ],
  ],
  random: [
    [ isGreaterThan(7), 'Minimum Random length of 8 is required.' ],
    [ hasCapitalLetter, 'Random should contain at least one uppercase letter.' ],
  ]
}

And imagine this is our input data.

const inputData = { name: 'abcdef', random: 'z'}

We would like to have a result that displays any possible errors.

Calling validate validate({inputData, validationRules) should return

{name: true,
 random: [
    'Minimum Random length of 8 is required.',
    'Random should contain at least one uppercase letter.'
]}

Revalidate does exactly that, by defining an initial state and the validation rules it takes care of updating and validating any React Form Component. Revalidate also doesn’t know how your form is built or if it is even a form for that matter. This also means, a form library can be built on top Revalidation, making it a sort of meta form library.

Getting started

Install revalidation via npm or yarn.

npm install --save revalidation

Example

We might have a stateless function component that receives a prop form, which include the needed field values.


import React, {Component} from 'react'

const Form = ({ form, onSubmit }) =>
  (
    <div className='form'>
      <div className='formGroup'>
        <label>Name</label>
        <input
          type='text'
          value={form.name}
        />
      </div>
      <div className='formGroup'>
        <label>Random</label>
        <input
          type='text'
          value={form.random}
        />
      </div>
      <button onClick={() => onSubmit(form)}>Submit</button>
    </div>
  )

Next we might have a defined set of rules that we need to validate for given input values.

const validationRules = {
  name: [
    [isNotEmpty, 'Name should not be  empty.']
  ],
  random: [
    [isLengthGreaterThan(7), 'Minimum Random length of 8 is required.'],
    [hasCapitalLetter, 'Random should contain at least one uppercase letter.'],
  ]
}

Further more we know about the inital form state, which could be empty field values.

const initialState = {password: '', random: ''}

Now that we have everything in place, we import Revalidation.

import Revalidation from 'revalidation'

Revalidation only needs the Component and returns a Higher Order Component accepting the following props:


const enhancedForm = revalidation(Form)

// inside render f.e.

<EnhancedForm
  onSubmit={this.onSubmit} // pass any additional props down...
  initialState={initialState}
  rules={validationRules}
  validateSingle={true}
  validateOnChange={true}
  {/*
    alternatively pass in a function, i.e. enable validationOnChange after a submit.
    validateOnChange={(submitted) => submitted}
  */}
/>

This enables us to rewrite our Form component, which accepts a revalidation prop now.


const createErrorMessage = (errorMsgs) =>
  isValid(errorMsgs) ? null : <div className='error'>{head(errorMsgs)}</div>

const getValue = e => e.target.value

const Form = ({ revalidation : {form, onChange, updateState, valid, errors = {}, onSubmit}, onSubmit: onSubmitCb }) =>
  (
  <div className='form'>
    <div className='formGroup'>
      <label>Name</label>
      <input
        type='text'
        className={isValid(errors.name) ? '' : 'error'}
        value={form.name}
        onChange={compose(onChange('name'), getValue)}
      />
      <div className='errorPlaceholder'>{ createErrorMessage(errors.name) }</div>
    </div>
    <div className='formGroup'>
      <label>Random</label>
      <input
        type='text'
        className={isValid(errors.random) ? '' : 'error'}
        value={form.random}
        onChange={compose(onChange('random'), getValue)}
      />
      <div className='errorPlaceholder'>{ createErrorMessage(errors.random) }</div>
    </div>
    <button onClick={() => onSubmit(onSubmitCb)}>Submit</button>
  </div>
  )

export default revalidation(Form)

revalidtion returns an object containing:

onChange('name', 'foo')
// or
onChange('name', 'foo', [UPDATE_FIELD])
// or
onChange('name', 'foo', null, ({valid, form}) => valid ? submitCb(form) : null )
 <button onClick={() => updateState({ name: '', random: '' })}>Reset</button>
<button
  onClick={() => onSubmit(({form, valid}) => valid ? submitCb(form) : console.log('something went wrong!'))}
>
  Submit
</button>
<button
  onClick={() => onSubmit(({form, valid}) => valid ? submitCb(form, updateAsyncErrors) : console.log('something went wrong!'))}>Submit
</button>

// use in another Component...
class HigherUpComponent extends React.Component {
  onSubmit = (formValues, updateAsyncErrors) => {
    setTimeout(() => {
      // something went wrong...
      updateAsyncErrors({ name: ['Username is not available'] })
    }, 1000)
  }

  render() {
    {/* ... */}
  }
}

Additionally revalidation offers a number of helper functions to quickly update any values or validations.

<input
  type="text"
  value={form.name}
  onChange={debounce.name(usernameExists, 1000)}
/>
  <input
    type='text'
    className={isValid(errors.random) ? '' : 'error'}
    name='random'
    value={form.random}
    onChange={updateValue}
  />
  <input
    type='text'
    className={isValid(errors.random) ? '' : 'error'}
    name='random'
    onBlur={validateValue}
    value={form.random}
    onChange={updateValue}
  />
  <input
    type='text'
    className={isValid(errors.random) ? '' : 'error'}
    name='random'
    onBlur={validateValue}
    value={form.random}
    onChange={updateValue}
  />

Where and how to display the errors and when and how to validate is responsibility of the form not Revalidation. Another aspect is that the form props can be updated when needed.

NOTE: updateForm should be used carefully and only when needed. Make sure to reset or remove updateForm after applying the new form values.

<Form
  onSubmit={this.onSubmit}
  updateForm=
/>

Either define an initial state or use form props to define an actual form state. Revalidation will check for props first and then fallback to the initial state when none is found.

Revalidation also enables to pass in asynchronous error messages via the asyncErrors prop. As side effects are run outside of Revalidation itself, any error messages (from a dynamic validation or after submitting to a server and receiving errors) can be passed back into Revalidation.


// i.e. userNameExists is a function returning a promise and sends a request to validate if the username is available.

<EnhancedSubmitForm
  onSubmit={this.onSubmit}
  rules={validationRules}
  initialState={initialState}
  asyncErrors=
  userNameExists={this.usernameExists}
  validateSingle={true}
  validateOnChange={true}
/>

NOTE: A sensible approach with asynchronous validation functions is useful, Revalidation will not run any effects against an input field. Needed consideration include: when to run the side effects (dynamically or on submit) and how often to trigger an async validation (immediately on every change or debounced)

More: Revalidation also works with deep nested data structure (see the deep nested data example)

check the example for more detailed insight into how to build more advanced forms, f.e. validating dependent fields.

Clone the repository go to the examples folder and run the following commands:

yarn install
npm start.

Demo

Check the live demo

Further Information

For a deeper understanding of the underlying ideas and concepts:

Form Validation As A Higher Order Component Pt.1

Form Validation As A Higher Order Component Pt.2

Credits

Written by A.Sharif

Original idea and support by Stefan Oestreicher

Very special thanks to Alex Booker for providing input on the API and creating use cases.

More

Revalidation is under development.

The underlying synchronous validation is handled via Spected

Documentation

API

FAQ

License

MIT