Implementing Dynamic Form Validators in ReactJS

Published Mar 30, 2017
Implementing Dynamic Form Validators in ReactJS

Introduction

As a web developer coming from an AngularJS background, I took for granted the built-in support for form validation present and how easy it is to extend by adding our very own custom validation rules until I tried doing the same in ReactJS. I searched around for suitable ways to do that in ReactJS and came up with an interesting solution, borrowing concepts from AngularJS.

AngularJS Form Validators 101

In AngularJS, a validator works on the information of the input field, which is associated with it, by checking the field value against a certain restriction.
Each input field has a list of validators which are stored as $validators (for synchronous validation) and $asyncValidators (for asynchronous validation).
When the associated input field changes, AngularJS runs the list of validators associated with that input field. The final validity of the input field depends on all of the validators returning true for valid or any of them returning false for invalid - makes sense?
Furthermore, AngularJS also allows us to display a unique error message (for a validator rule that returns false) under the input field associated with that validator, just like so:

screenshot-localhost 8001-2017-03-26-15-24-02.png

Building out similar logic in ReactJS

For this example, we are going to build a sign-in form you saw above in ReactJS. The form has two input fields — one for username and the other for the password — and a button that is disabled unless the form is valid.

Our validator schema or object

For the username to be valid, there are two constraints that must be true

  • The username must be longer than 2 characters
  • The username must contain only alpha-numeric characters.

For the password to be valid, there is only one constraint

  • The password must not be shorter than 6 characters

Now, let's create our validator schema (which is just a plain javascript object) from the description above, and save it as /validators.js.

const validator = {
  username: {
    rules: [
      {
        test: /^[a-z0-9_]+$/,
        message: 'Username must contain only alphabets-numeric lowercase characters',
      },
      {
        test: (value) => {
          return value.length > 2;
        },
        message: 'Username must be longer than two characters',
      },
    ],
    errors: [],
    valid: false,
    state: '',
  },
  password: {
    rules: [
      {
        test: (value) => {
          return value.length >= 6;
        },
        message: 'Password must not be shorter than 6 characters',
      },
    ],
    errors: [],
    valid: false,
    state: ''
  },
};

export default validator;

Dissecting the validator object

From the above snippet, you will notice that the validator object has two keys that correspond to the number of inputs our form has. Each of the keys has the following properties:

  • rules:
    This defines an array of constraints that the given input is to be tested against

  • errors:
    This is an array that will eventually hold error messages returned by running the rules against the current state of the input.

  • valid:
    This represents the state of the input field after applying the rules associated with it.

  • state:
    This holds the current value or state of the input field.

Creating our ReactJS form component.

Create a new file, /signin.jsx.

import React, { Component } from 'react';
import validators from './validators';

export default class Signin extends Component {
  constructor() {
    super();

    this.state = {
      userInfo: {
        username: '',
        password: '',
      }
    };

    // Set of validators for signin form
    this.validators = validators;
    
    // This resets our form when navigating between views
    this.resetValidators();

    // Correctly Bind class methods to reacts class instance
    this.handleInputChange = this.handleInputChange.bind(this);
    this.displayValidationErrors = this.displayValidationErrors.bind(this);
    this.updateValidators = this.updateValidators.bind(this);
    this.resetValidators = this.resetValidators.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.isFormValid = this.isFormValid.bind(this);
  }
  
  /** 
   * This function is called whenever a form input is changed
   * Which in turn updates the state of this component and validators
   */
  handleInputChange(event, inputPropName) {
    const newState = Object.assign({}, this.state);
    newState.userInfo[inputPropName] = event.target.value;
    this.setState(newState);
    this.updateValidators(inputPropName, event.target.value);
  }
  
   /** 
   * This function handles the logic when submiting the form.
   * @TODO make an API request to authenticate the user
   */
  handleSubmit(e) {
    console.log(this.state.userInfo);
    console.log('Yepee! form submitted');
    e.preventDefault();
  }
  
   /** 
   * This function updates the state of the validator for the specified validator
   */
  updateValidators(fieldName, value) {
    this.validators[fieldName].errors = [];
    this.validators[fieldName].state = value;
    this.validators[fieldName].valid = true;
    this.validators[fieldName].rules.forEach((rule) => {
      if (rule.test instanceof RegExp) {
        if (!rule.test.test(value)) {
          this.validators[fieldName].errors.push(rule.message);
          this.validators[fieldName].valid = false;
        }
      } else if (typeof rule.test === 'function') {
        if (!rule.test(value)) {
          this.validators[fieldName].errors.push(rule.message);
          this.validators[fieldName].valid = false;
        }
      }
    });
  }
  
  // This function resets all validators for this form to the default state
  resetValidators() {
    Object.keys(this.validators).forEach((fieldName) => {
      this.validators[fieldName].errors = [];
      this.validators[fieldName].state = '';
      this.validators[fieldName].valid = false;
    });
  }
  
  // This function displays the validation errors for a given input field
  displayValidationErrors(fieldName) {
    const validator = this.validators[fieldName];
    const result = '';
    if (validator && !validator.valid) {
      const errors = validator.errors.map((info, index) => {
        return <span className="error" key={index}>* {info}</span>;
      });

      return (
        <div className="col s12 row">
          {errors}
        </div>
      );
    }
    return result;
  }
  
  // This method checks to see if the validity of all validators are true
  isFormValid() {
    let status = true;
    Object.keys(this.validators).forEach((field) => {
      if (!this.validators[field].valid) {
        status = false;
      }
    });
    return status;
  }
  
  // Renders the template
  render() {
    return (
      <div className="row">
        <div className="col s12 m8 l4 offset-m2 offset-l4 z-depth-4 card-panel login-form">
          <form className="col s12" onSubmit={this.handleSubmit}>
            <div className="row">
              <div className="input-field col s12">
                <h4 className="center login-form-text">Sign into your account</h4>
              </div>
            </div>
            <div className="row margin">
              <div className="input-field col s12">
                <i className="material-icons prefix">person</i>
                <input
                  id="username"
                  type="text"
                  value={this.state.userInfo.username}
                  onChange={event => this.handleInputChange(event, 'username')}
                />
                <label htmlFor="username" className="left-align">username</label>
              </div>
              { this.displayValidationErrors('username') }
            </div>
            <div className="row margin">
              <div className="input-field col s12">
                <i className="material-icons prefix">lock</i>
                <input
                  id="password"
                  type="password"
                  value={this.state.userInfo.password}
                  onChange={event => this.handleInputChange(event, 'password')}
                />
                <label htmlFor="password" className="left-align">Password</label>
              </div>
              { this.displayValidationErrors('password') }
            </div>
            <div className="row">
              <div className="input-field col s12 signup-btn">
                <button className={`btn waves-effect waves-light col s12 ${this.isFormValid() ? '' : 'disabled'}`}>
                  Login
                </button>
              </div>
            </div>
          </form>
        </div>
      </div>
    );
  }
}

When it's all said and done

vslid.png

Conclusion

For the most part, that is all you need to implement a custom dynamic form validation in ReactJS.

Areas of possible improvements may include

  • Creating a reusable component for your input elements where the validators for each input is passed as props.

  • Creating a reusable form component that takes the validator schema as props and generates a functioning form.

References

Here are a few links you might find useful!


We're always available to answer any questions you may have at write@codementor.io!

Discover and read more posts from Orion
get started
Enjoy this post?

Leave a like and comment for Orion

4
Be the first to share your opinion

Get curated posts in your inbox

Read more posts to become a better developer