DEV Community

Arpy Vanyan
Arpy Vanyan

Posted on • Originally published at Medium on

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 ☺️

Top comments (5)

Collapse
 
cuginoale profile image
Alessio Carnevale

Very nice article!
I am with you when it comes to form validation! I do think tho that the logic in the validate function can be simplified.
I am exploring a very similar approach here: dev.to/cuginoale/form-validationsu...

Your feedback would be highly appreciated! :)

Collapse
 
shelaghlewins profile image
Shelagh-Lewins • Edited

Thank you for the excellent article. It's a really nice reusable validated form component.

I have a couple of comments on the code in your CodePen based on my experience building this component.

I think the regular expression doesn't need to specify the length? The minLength attribute covers this and gives a more useful error.

When passing a regex in to a React component, you may need to modify it slightly (see stackoverflow.com/questions/444979...). I replaced

"(?=.*\d)(?=.*[a-z]).{6,}"

with

"^(?=.*\d)(?=.*[a-z]).+$"

In the Form component, I replaced

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

with

render() {
    const props = { ...this.props };

because it generated an error. I think the original code was trying to cast an object to an array?

Collapse
 
skptricks profile image
skptricks

Thank you so much for this great article, I have found similar article :
skptricks.com/2018/06/simple-form-...

Collapse
 
_arpy profile image
Arpy Vanyan

Hi! Thanks for sharing. That's a nice article, but there is a huge difference her :) The article is about a single case of validating a user registration form with a custom validation check. While I suggest a common form component that can be used for all types of forms regardless of the inputs and the validation is done with the already existing and awesome HTML5 input validation ;)

Collapse
 
jgentes profile image
James Gentes

Nice article.. readers should know you need this babel plugin to make it work: babeljs.io/docs/en/next/babel-plug...