Skip to content

franleplant/reform

Repository files navigation

Reform Build Status Coverage Status

A form validation library for Javascript, Typescript, React, React Native and the web!

API Docs

Why should I use Reform? How Reform compares to existing solutions?

While there are already solutions for the problem of Form Validation in React, Reform is an attempt to make form validation as simple and versatile as possible.

Characteristics of Reform

Works on React and React Native

Because of it's nature, because it does not rely on the DOM, Reform is just a plain javascript library which runs on anything that runs javascript, and that of course includes React Native. Bonus points: all HTML5 validators such as required, minLength, et al, also work on React Native.

Works with any React view framework and with any custom components

Some libraries like react-validation use Components as their API. The problem with this approach is that it does not play well with libraries such as react-bootstrap which already provide their own input components, or your own custom input components.

Reform works with any component that receives a value and a change function as its props, it does not matter what are the names for this props, whether they are value, text, onChange, onChangeText, onChangeValue or whatever the component author decided to call them, Reform will work with that component.

Works right out of the box without further stuff.

The great redux-form which we've used extensively, does not provide any validators by default.

Reform provides all HTML5 validators right out of the box, for you to start validating your forms right away.

Does not depend on Redux

We love Redux, but except for particular use cases, we don't see the point in storing form state in the application global state, and also, we don't tend to connect Form Component to the Redux Store and that's why Reform only requires that you use the regular React Controlled Components pattern for form Handling.

Does not have dependencies

Reform does not depend on anything, not even in React.

Absurd versatility

Reform provides common abstraction to use with React. Don't like them? Simply build your own using the core (described below)

Introduction

Reform started as a form validation library for React, but it has come a long way since to become a general purpose validation library that can be used anywhere you would use Javascript or Typescript.

Reform is separated into two main parts: core and helpers

The core is a very small set of pure functions that given a value and validation rules will yield the result of evaluating those rules on the value. You can think of it as a mini engine to evaluate rules. The core also packs all the HTML5 default validators, i.e. required, minLength, type=email, etc, with a slight different way of expressing them. This means that you have a lot of ground already covered for you, unlike other form validation libraries out there. This also means that you could use the core to build your own form validation library!

The helpers are where all the bindings for different libraries live. For now we only provide React bindings which is basically a thin wrapper of the core so you can easily start validating your React forms plus some additional helpers to display field errors. reactHelpers will work with any React view library such as react-bootstrap as well as with any custom component you would like to build. One of the drivers of the current Reform design is exactly this: maximum flexibility. It provides some minimal opinions to maintain the usability high! If you want more freedom you can always use the core and build your own helper anyway you like it.

The important thing here is that this library should provide the functionality that you need, the customization that you want and an easy escape hatch if you need it, which in this case is trivial.

The first item, functionality that you need, will be provided by the helpers, i.e. reactHelpers. It will be what you are looking for 90% of the times and it will get you started with form validation in no time.

The second item, customization that you want, will be provided by the core, which can be easily used along side with the helpers or as a standalone module.

The third item, escape hatch if you need it, is provided by the inherent structure of the library as you will see as you go along with the docs.

The central dogma that Reform trays to live by is: be useful with little entry or exit costs

Getting Started

npm install --save @franleplant/reform

(or use yarn if you are into that)

yarn add @franleplant/reform

Quick Start

This is a complete working example of a React Form plus Reform validation

import React, { Component } from 'react';
import Reform from '@franleplant/reform';

/*
  Reform HTML5 mode example (validate onSubmit)
*/
export default class GettingStarted extends Component {
  /*
   Initialize your field and error state
  */
  state = {
    fields: {
      email: '',
      password: '',
    },

    errors: {},

    // This is not reform specific
    message: '',
  }

  /*
   Declare validation rules for your fields
  */
  validationRules = {
    email: { email: true, required: true},
    password: { required: true, minLength: 6},
  };

  /*
   [Optional] Easy way of displaying error messages
  */
  validationMessages = {
    required: (ruleKey, ruleValue, fieldName) => `${fieldName} is required`,
    email: (ruleKey, ruleValue, fieldName) => `${fieldName} must be a valid email`,
    default: (ruleKey, ruleValue, fieldName) => `${fieldName} is invalid`,
  }

  /*
    Hook Reform into your component
  */
  constructor(...args) {
    super(...args);
    Reform.reactMixins.functionalMixin(this);
    // The above call with add new methods to your component, including:
    // - validateFormFromState
    // - formIsValid
    // - fieldIfError
    // - mapFieldErrors
  }


  /*
    Regular onChange handlers from React world
  */
  onChangeFactory = (fieldName) => {
    return event => {
      const value = event.target.value;
      this.setState(state => {
        const fields = {
          ...state.fields,
          [fieldName]: value,
        };

        return {...state, message: '', error: {}, fields};
      });
    }
  }

  /*
   onSubmit handler
  */
  onSubmit = (e) => {
    e.preventDefault();
    /*
     This form will only validate onSubmit
    */
    const isValid = this.validateFormFromState();
    if (!isValid) {
      this.setState({ message: 'Invalid Form'})
      return
    }

    this.setState({ message: 'Valid Form'})
  }

  render() {
    return (
      <form>
        <div>
          {/*
            Regular React controlled component
          */}
          <input type="email" value={this.state.fields.email} onChange={this.onChangeFactory('email')} />
          {/*
            Display errors (first way)
          */}
          <ul>
            {
              this.mapFieldErrors('email')
              .map((message, index) => {
                return (
                  <li key={index}>{message}</li>
                );
              })
            }
          </ul>
        </div>


        <div>
          {/*
            Regular React controlled component
          */}
          <input type="password" value={this.state.fields.password} onChange={this.onChangeFactory('password')} />
          {/*
            Display errors (second way) (there are more)
          */}
          <ul>
            { this.fieldIfError('password', 'required') &&
              <li>password is required</li>
            }

            { this.fieldIfError('password', 'minLength') &&
              <li>password must be at least 6 characters long</li>
            }
          </ul>
        </div>

        <p>{this.state.message}</p>
        {/*
          Regular submit!
        */}
        <button onClick={this.onSubmit}>Submit</button>
      </form>
    );
  }
}

Validators

Reform provides all the HTML5 validators, they are slightly differently expressed

These are the rules of thumb for translating HTML5 validator attributes to Reform validation rules:

  • All validation attributes, such as required, min-length=6 and max-length=6 are expresed as {required: true}, {minLength: 5}, {maxLenght: 6}
  • All type="something" are expressed as {something: true}, i.e.type="email is {email: true}
  • All validation rules that depend on the type of the field are expresed like ${rule}${type}. i.e. minNumber, maxDate, minWeek etc.

Dates, week, time and color types are supported exactly as the HTML5 standard says, so if you find any differences please report a bug.

A complete list and their API is available in the API docs.

Example

// Use all the HTML5 validation Rules in a single field
this.validationRules = {
  myField: {
    // Regular validation attributes
    // attributes with no value in HTML5 use true as value in Reform
    required: true,
    // validation attributes with an actual value in HTML5
    // use that same value in Reform
    minLength: 5,
    maxLength: 15,
    // pattern accepts directly a RegExp object instead of a string
    pattern: /banana|apple/,


    // type validation attributes
    // type attributes in HTML5 use true as value in Reform
    email: true,
    number: true,
    range: true,
    color: true,
    date: true,
    time: true,
    url: true,
    month: true,
    week: true,


    // type dependent validation attributes
    minNumber: 5,
    maxNumber: 15,
    minDate: '2016-12-26',
    maxDate: '2016-12-26',
    minMonth: '2016-9',
    maxMonth: '2016-9',
    minTime: '17:45',
    maxTime: '17:45',
    minWeek: '2015-W32',
    maxWeek: '2015-W32',
  }
}

Custom validators

There are two ways of using a custom validator in Reform:

  • ad hoc
  • Global

Example: ad hoc

// Instead of an attribute pass a function that conforms to the
// Validator type (see api docs)
this.validationRules = {
  myField: {
    myAdHocValidator: (value, validatorArgument) => {/*return true if the value is erroneus*/}
    // Example of confirm a password
    passwordShouldMatch: (value) => value !== this.state.fields.password
  }
}

Example: global

// Define a global validator
Reform.validators.set('myCustomGlobalValidator', (value, validatorArgument) => {/*return a boolean*/});

// Now you can use it in any place of your app

this.validationRules = {
  myField: {
    myCustomGlobalValidator: validatorArgument,
  }
}

React Helpers Guide

Hooking Reform into our component

There are several ways of hooking Reform reactHelpers into your components

// by hand and selectively with property initializers
class MyComp extends React.Component {
  fieldIsValid = Reform.reactHelpers.fieldIsValid;
  formIsValid = Reform.reactHelpers.formIsValid;
}

// by hand and selectively inside the constructor
class MyComp extends React.Component {
  constructor(props) {
    super(props);
    this.fieldIsValid = Reform.reactHelpers.fieldIsValid.bind(this);
    this.formIsValid = Reform.reactHelpers.formIsValid.bind(this);
  }
}


// With functional mixin (not selectively)
class MyComp extends React.Component {
  constructor(props) {
    super(props);
    Reform.reactMixins.functionalMixin(this);

    //All Reform.reactHelper methods are available inside `this`
  }
}

// With object mixin (use it in typescript to get type information)
class MyComp extends React.Component {
  re = Reform.reactMixins.objectMixin(this);
  //All Reform.reactHelper methods are available inside `this.re`
  //you can use any other attribute name
}

objectMixin is the most appropiate with Typescript since it will give you autocomplete and proper typechecking.

Triggering Validation

There are several ways you can trigger validation, let's checkout how:

Fields

//onChange
handleChange(e) {
  const value = e.target.value;
  // this function will validate `value` with `this.validationRules[fieldName]`
  // and update `this.state.errors[fieldName]`
  // NOTE: this wont update `this.state.fields[fieldName]` with the newest value
  this.validateField(fieldName, value);
}

//onBlur
handleBlur() {
  // this function will validate `this.state.fields[fieldName]` with `this.validationRules[fieldName]`
  // and update `this.state.errors[fieldName]`
  // Note that we assume that we already have the latest value inside `this.state.fields[fieldName]`
  // NOTE: this wont update `this.state.fields[fieldName]` with the newest value
  this.validateFieldFromState(fieldName);
}

//you can use any other event you might like

NOTE: You can get fieldName from a closure, from a partial application (bind), form a dom attribute, etc

Form

// validate on submit
handleSubmit() {
  // Validate the form with all the field values in `this.state.fields`
  // with `this.validationRules` and update `this.state.errors`
  const isValid = this.validateFormFromState();
  if (!isValid) {
    // Handle invalid case
  } else {
    // Handle valid case
  }
}

Managing fieldName

Most of the time I use this pattern

handleChangeFactory(fieldName) {
  return function handleChange(e) {
    const value = e.target.value;
    // Update your state
    this.setState(state => {
      state.fields[fieldName] = value;
      return state;
    })
    // Validate and update your validation state
    this.validateField(fieldName, value);
  }
}

Sometimes this pattern can affect performance since it will create a new function on every render causing any children like this:

<input onChange={this.handleChangeFactory(fieldName)} {...otherProps}/>

To be re-rendered because it's props will change!. So if you want to avoid this, the most easy thing to do is to cache the handleChange functions generated by handleChangeFactory.

A very simple implementation

handleChangeFactory(fieldName) {
  if (!this.handleChangeCache[fieldName]) {
    this.handleChangeCache[fieldName] = function handleChange(e) {
      const value = e.target.value;
      // Update your state
      this.setState(state => {
        state.fields[fieldName] = value;
        return state;
      })
      // Validate and update your validation state
      this.validateField(fieldName, value);
    }
  }

  return this.handleChangeCache[fieldName];
}

I've only run with performance problem with this when using custom native components in React Native, but not on the web.

Field Validity?

These are all the ways you can check if a field is valid or not:

// After triggering a validation (these update `this.state.errors[fieldName]`)
const isValid = this.validateField(fieldName, value);
const isValid = this.validateFieldFromState(fieldName);

// By asking (this will use `this.state.errors[fieldName]` to check validity)
const isValid = this.fieldIsValid(fieldName)

// With the core (but don't use it unless you really know what you are doing)
const isValid = Reform.core.fieldIsValid(this.state.errors[fieldName]);
// With the core but re-evaluating the validity of the field
const isValid = Reform.core.fieldIsValid(value, this.validationRules[fieldName]);

Form Validity?

// After triggering form validation (these update `this.state.errors`)
const isValid = this.validateForm(this.state.fields);
const isValid = this.validateFormFromState();

// By asking (this will validate `this.state.fields` with `this.validationRules`)
const isValid = this.formIsValid();

Displaying errors

A very important part of form validation, and Reforms gives you greate versatility in this

// Manual manipulation
// check that there are errors for that field, then check that the rule we care about
// is true (which means there is an error) and then display error message
{ this.state.errors.myField && this.state.errors.myField.required &&
  <p>myField is required!</p>
}

// With helper 1
{ this.fieldIfError('myField', 'required') &&
  <p>myField is required!</p>
}

// With helper 2 (unstable)
// first declare your validation messages
this.validationMessages = {
  required: _ => 'Field is required!',
  // default message
  default: _ => 'Field is invalid',
}

//inside render
{ this.mapFieldErrors('myField').map((errorMessage, index) => (
  <p key={index}>{message}</p>
))}

Check the api for this.validationMessages for more details, but it offers a lot of options. You can for example do this

this.validationMessages = {
  minLength: (ruleValue, ruleKey, fieldName) => `${fieldName} needs at least ${ruleValue} characters`,
  // default message
  default: _ => 'Field is invalid',
}

Common Solutions to Common Problems

TODO

Contributing

Licence

MIT