/ Form

Higher Order Component for Validating Forms in React

Higher Order Component for Validating Forms in React

Revalidation

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:

  • initialState (Object)

  • rules (Object)

  • validateSingle (Boolean)

  • validateOnChange: (Boolean|Function)

  • asyncErrors (Object)

  • updateForm (Object)


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:

  • form: form values
  • onChange: a function expecting form name and value, additionally one can specify if the value and/or the validation should be updated and also accepts a callback function that will be run after an update has occurred. i.e.
onChange('name', 'foo')
// or
onChange('name', 'foo', [UPDATE_FIELD])
// or
onChange('name', 'foo', null, ({valid, form}) => valid ? submitCb(form) : null )
  • updateState: a function expecting all the form values, f.e. Useful when wanting to reset the form. Depending on the setting either a validation will occur or not.
 <button onClick={() => updateState({ name: '', random: '' })}>Reset</button>
  • valid: calculated validation state, f.e. initially disabling the submit button when a form is rendered.
  • submitted: set to true once the form has been submitted.
  • errors: the errors object containing an array for every form field.
  • onSubmit: validates all fields at once, also accepts a callback function that will be called after the a validation state has been calculated. The callback function receives the current state including the valid state.
<button
  onClick={() => onSubmit(({form, valid}) => valid ? submitCb(form) : console.log('something went wrong!'))}
>
  Submit
</button>
  • updateErrors: Enables to update any errors.

  • updateAsyncErrors: Enables to update any asynchronous errors. Useful when working with asynchronous validations.
    Pass the updateAsyncErrors to a callback, once the validation is finished set the result manually.

<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() {
    {/* ... */}
  }
}
  • settings: access the current settings: { validateOnChange: true, validateSingle: true }

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

  • debounce: a helper function for triggering asynchronous validations. The passed in asynchronous validation can be debounced by a specified time. i.e. 1000 ms.
<input
  type="text"
  value={form.name}
  onChange={debounce.name(usernameExists, 1000)}
/>
  • updateValue: update a specific field value. Important to note that no validation will run. Use updateValueAndValidate if you need to update and validate of field. A name attribute must be defined on the element for updateValue to update the value.
  <input
    type='text'
    className={isValid(errors.random) ? '' : 'error'}
    name='random'
    value={form.random}
    onChange={updateValue}
  />
  • validateValue: validates a specific field. Useful when validation should happen after an onBlur i.e.
    A name attribute must be defined on the element for validateValue to validate the value.
  <input
    type='text'
    className={isValid(errors.random) ? '' : 'error'}
    name='random'
    onBlur={validateValue}
    value={form.random}
    onChange={updateValue}
  />
  • updateValueAndValidate: Updates and validates the value for the specified element.
    A name attribute must be defined on the element for updateValueAndValidate to update the value.
  <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={{name: 'foobar', random: ''}}
/>

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={{name: ['Not available.']}}
  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.

GitHub