HTML5 form validation in React

_arpy

Arpy Vanyan

Posted on February 18, 2018

HTML5 form validation in React

“Colorful lines of code on a computer screen” by Sai Kiran Anagani on Unsplash

Best data is a validated data

Users… Data collection… Forms… I’m sure you know that user input is good when it is valid ✅. That is why websites must encourage the users to fill in the best fitting data whenever possible.

There are various libraries like jQuery Validation or Validate.js, that help with form input validations. All of them implement the idea of performing predefined checks on each of the form fields before it is submitted. Then, they display error messages, if there are fields that do not qualify.

But there is also the powerful HTML5 validation API. And it’s awesome. Modern browsers almost fully support the API. Of course, each of them implemented its own way of performing the validation and displaying error messages. And sometimes it looks really nasty 🙈

So, why not implement our own layout for the validation errors? Sounds doable. We will use the following aspects from the HTML5 constraint validation API: the checkValidity method and the  :valid/:invalid states of a form, and the validity property of form elements. If curious, they are explained in detail in this superb MDN Web Docs (Mozilla Developer Network) page. What I am going to do is to implement custom validation messages using the API for a React app. Let’s go! 🏁 🚗

The React Component

Well, React means Components! We surely need to create one for this task. And, surprisingly, it will be a custom stateful Form component with its corresponding styles file.

First of all, let's define how we want to display our validation errors. I prefer to have separate messages next to each form field. For our ease, we will assume, that every input field is assigned with  .form-control class, and each of them has a sibling <span> with an  .invalid-feedback class. Each span will hold the error message of its relevant form element. In this implementation, each form element will have its own error message container next to it. Of course, you are free to define your own error containers and even have only one container for displaying all of the messages in one place.

As you might already know, the validity of a form element is identified by a CSS pseudo class. If the element (input, textarea, checkbox,…) passes the defined rules, it is assigned with  :valid pseudo class. Otherwise it gets  :invalid pseudo class. We will use this behavior do decide whether an error message should be displayed next to an element or not. We’ll define a style in our Form.css that will hide the messages of valid elements.

.form-control:valid~.invalid-feedback {display: none;}

The idea of the component is the following. In React, typically, we don’t want to reload the page on form submission. Instead, we want to send the data with ajax request. It doesn’t matter for our validation component how and what data is submitted. We just handle validation. That is why it will receive a property named submit, which is a function, that should be called whenever the form is allowed to be submitted. The component will override the native form submit event in the following way. First, it will check the overall form validity with the checkValidity method. If no errors found, it will perform the submission by calling the submit method from props. If there was at least one invalid element, it will show the corresponding messages and cancel the form submission. And, of course, the component will render a regular <form> tag, with all the child elements nested inside.

Sounds pretty straightforward, right? Let’s see how it looks like as a code 😉

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import './Form.css';

class Form extends Component {
    state = {
        isValidated: false
    }

    validate = () => {
        const formLength = this.formEl.length;

        if (this.formEl.checkValidity() === false) {
            for(let i=0; i<formLength; i++) {
                const elem = this.formEl[i];
                const errorLabel = elem.parentNode.querySelector('.invalid-feedback');

                if (errorLabel && elem.nodeName.toLowerCase() !== 'button') {
                    if (!elem.validity.valid) {
                        errorLabel.textContent = elem.validationMessage;
                    } else {
                        errorLabel.textContent = '';
                    }
                }
            }

            return false;
        } else {
            for(let i=0; i<formLength; i++) {
                const elem = this.formEl[i];
                const errorLabel = elem.parentNode.querySelector('.invalid-feedback');
                if (errorLabel && elem.nodeName.toLowerCase() !== 'button') {
                    errorLabel.textContent = '';
                }
            };

            return true;
        }
    }

    submitHandler = (event) => {
        event.preventDefault();

        if (this.validate()) {
            this.props.submit();
        }

        this.setState({isValidated: true});
    }

    render() {
        const props = [...this.props];

        let classNames = [];
        if (props.className) {
            classNames = [...props.className];
            delete props.className;
        }

        if (this.state.isValidated) {
            classNames.push('.was-validated');
        }

        return (
            <form ref={form => this.formEl = form} onSubmit={this.submitHandler} {...props} className={classNames} noValidate>
                {this.props.children}
            </form>
        );
    }
}

Form.propTypes = {
    children: PropTypes.node,
    className: PropTypes.string,
    submit: PropTypes.func.isRequired
};

export default Form;

Let’s dig into it starting from the bottom ⬆️. So, we render a regular <form> that includes all the children passed to our component. It also gets a  .was-validated class in case we have no errors. We can use this class for styling for example. We also hold a reference to our form element in our component. Thus we would be able to work with it with JavaScript methods. Also, we assign a submit handler function to the form with the onSubmit event.

When the form is submitted (typically with a submit button), we call the component method called validate(). It’s not hard to guess that this is the method where our component’s main functionality is hidden. So, if it returns true, the form is valid, and we are free to call the component’s submit method. Now, how does the validation work 🤔?

The Validate method

In HTML5, a form validity is checked with the checkValidation() method. It returns true if all the form elements qualify defined rules and false, if at least one validation rule fails. We’ll use this behavior in our component.

In case the form is valid, we will loop through its elements and remove the text of their corresponding error containers.

If the form is invalid, we need to show messages for each of the erred elements. If a form element is invalid, its validity.valid property would be false. In such case, we will fill the  .invalid-feedback <span> with the corresponding error message. The error message provided by the API is accessible through the validationMessage property of an element. We will use that message as it is pretty much detailed and already localized with the browser locale. If you want to use your custom error messages, you should assign the errorLabel.textContent with your value instead of elem.validationMessage. As simple as that.

Note, that we skip an element if it is a button. This is because a form refers to all of its elements that a user can interact with, including buttons.

Now, all our invalid fields have error messages next to them and our form is not submitted until all errors are fixed 👍 The only thing left to do is styling! But I guess I’ll leave the funny part to you, as “I want to believe” (👽) in your creativity 😜

Thanks for reading this far, hope you had fun and learned something new here.

Here is a full working CodePen that I created as a playground 🎮 Enjoy, coders!

And one more thing…

Secure yourself from both sides

Remember, validating user input in client side is not enough. A smarty like you can always find a way to avoid the validation. That is why always do checks also in your backend. Believe me, you’ll thank yourself for that ☺️

💖 💪 🙅 🚩
_arpy
Arpy Vanyan

Posted on February 18, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

HTML5 form validation in React
react HTML5 form validation in React

February 18, 2018