DEV Community

Antonio Radovcic
Antonio Radovcic

Posted on • Edited on

Approaching Inline-Form-Validation

Checking the input from a user must always happen on the server, so why check it in the browser beforehand? Because UX. The days of reloading pages with error messages (and therefore erased password-fields) are over, and have been for years. Your users shouldn't be able to submit a form with missing data.

I want to demonstrate that inline-validation is no rocket-surgery, and that it's doable with a few lines of JS and current web-standards, without any libraries and frameworks.

Sticking to browser-standards

It's a good approach to stick to web-standards whenever possible, and not to reimplement browser-features. You'll probably do a worse job than the browser-vendors. Modern browsers all have some kind of instant validation, which will prevent a form-submit when e.g. an input with the "required"-attribute has no value.

Before you attempt to implement this yourself, consider sticking to this standard. For simpler forms in modern browsers, this will suffice.

Read all about browser-native form-validation on MDN.

Roll your own inline-validation

Sometimes the built-in ways are not sufficient. You may have to support older browsers, which don't do validation. Often a client or designer is not happy with the style of the browser-feedback not matching your site's style. Most of the time, validation changes depending on certain fields, like the birthday, which is also not supported by browsers by default.

Let's see how to tackle such a task.

The HTML

We'll start with a minimal form, where the user has to enter their name and tick a checkbox.
(The attribute "novalidate" disables the browser's validation, which makes it easier to develop and test your code.)

<form id="first-name-form" action="#" method="POST" novalidate>
    <label>
        Your first name:
        <input type="text" name="firstname">
    </label>
    <label>
        <input type="checkbox" name="acceptTerms">
        I accept the terms.
    </label>
    <button type="submit">Submit Form</button>
</form>

For starters, we should think about a way to attach the validation-rules to the corresponding fields. One possible way would be to define some generic rules, and add them to our inputs as data-attributes.

<input type="text" name="firstname" data-validate="required noNumbers maxLength">
<input type="checkbox" name="acceptTerms" data-validate="mustBeChecked">

This way we can easily add and remove validation-rules, once they are properly set up.

Another thing that is missing are the validation-messages. The user needs to know, what they forgot, or entered wrong.

For every validation-rule, there should be a corresponding message. We could create one container for the feedback-messages, and add them via JS when the validation-errors happen, or we could add all the possible messages to the markup and only show the applicable ones. I'm a big fan of keeping all messages in the markup, since it's easier to handle languages this way. We can get the correct translations when the server renders the page, and don't need to know about it in JS.

Let's add one message for each validation-rule. We'll add them right next to the input-tag in the label. The aria- and role-attributes help to keep our form accessible.

<!-- Messages for the name-input -->
<small role="alert" aria-hidden="true" data-validation-message="noNumbers">
    Please don't enter any numbers.
</small>
<small role="alert" aria-hidden="true" data-validation-message="maxLength">
    Please enter 10 characters or fewer.
</small>
<small role="alert" aria-hidden="true" data-validation-message="required">
    Please enter a name.
</small>

In our CSS we will hide the messages by default, and only show them, when our script adds the class "message-visible".

[data-validation-message] {
    display: none;
}
[data-validation-message].message-visible {
    display: block;
}

This way our form will still look OK with JS deactivated or broken.

Let's have some fun! Now our JS-implementation builds on no frameworks or libraries at all. We're using ES6-syntax and -features, which means you'll need to use something like Babel, if you need to support older browsers.

The approach

  • Every validator (like "noNumbers") will be a function, which receives the input-element as parameter and returns true (is valid) or false (is invalid).
  • We'll create a function, which checks a field for validity by checking its value against all corresponding validators, by calling the functions from the first point.
  • It will be called whenever the user focuses out of it (the blur-event).
  • If a validator fails, that function will take care of showing the correct error-message to the user.
  • When the user submits the form, we will check every form-field once, and prevent the submit, if there are any invalid fields left.

The validators

The validators are the most straightforward part. We'll create a functions for every rule we need. Let's take "noNumbers" as example.

function noNumbers(element) {
    return !element.value.match(/[0-9]/g);
}

We'll have several more of those, so why not collect them in an object? We'll add the other two we need from our example-markup, and add some ES6-Syntax while we're at it.

const validators = {
    required: element => element.value.length > 0,
    noNumbers: element => !element.value.match(/[0-9]/g),
    maxLength: element => element.value.length <= 10,
    mustBeChecked: element => element.checked
};

The validation

Now we need a function that calls all those checks we provided in the data attribute of the input-element. It will parse the content of its "data-validate"-attribute, and convert it to an array. Since the names of the rules are equal to the names of their corresponding function, we can invoke them by calling "validators[rule](value)".

function validateElement(element) {
    const rules = element.dataset.validate.split(" ");
    rules.forEach(rule => {
        if(validators[rule](element)) {
            return;
        } else {
            markElementInvalid(element, rule);
        }
    });
}

If the validator returns "false", we know that the validation has failed, and need to show the correct feedback-message and add some classes. For this purpose we'll create a new function called "markElementInvalid".

function markElementInvalid(element, validatorName) {
    element.classList.add("invalid");
    element.setAttribute("aria-invalid", true);
    const feedbackMessage = element
                            .parentNode
                            .querySelector(
                                `[data-validation-message=${validatorName}]`
                            );
    feedbackMessage.classList.add("message-visible");
    feedbackMessage.setAttribute('aria-hidden', false);
}

"markElementInvalid" will set some classes and attributes in the input-field and the feedback-message. The "aria"-attributes will help to enhance the accessibility a bit. We'll use the "invalid"-class to style the invalid-state of the input, so we'll need to set that in our CSS, too.

input.invalid {
  border-color: brown;
}

The main functionality is there, it just needs to be triggered at the right time.

There are several points in time where triggering the validation is possible.

  • On page-load
  • After the user focuses an input-field
  • After the user changes an input-field
  • After the user unfocuses an input-field
  • After the user submits the form

We don't want to annoy the user, so we need to be careful. Showing a failing validation too early might come across as pushy. It's a good practise to check a field after unfocus, or blur, which means the input loses focus by pressing "tab" or clicking outside of it. We'll check every field one more time, after the user submits the form, to prevent sending false data.

Let's enable the validation for our field on blur. This code will initialize our previous work.

const form = document.getElementById("first-name-form");
const formElements = Array.from(form.elements);

formElements.forEach(formElement => {
  formElement.addEventListener("blur", () => {
    validateElement(formElement);
  });
});

One caveat: The "blur"-event works well for text-inputs. For other types, "focusout" or "change" may work better, depending on the desired behavior.

The user now gets feedback after entering bad data. But there's no way to remove the error-state after they corrected the mistakes. We'll introduce the "resetValidation"-function, which is basically the exact opposite of the "markElementInvalid". It's a bit simpler, since there is no validator to consider. The input-field will be reset to its initial state.

function resetValidation(element) {
    element.classList.remove("invalid");
    element.setAttribute("aria-invalid", false);
    element
        .parentNode
        .querySelectorAll("[data-validation-message]")
        .forEach(e => {
            e.classList.remove("message-visible");
            e.setAttribute("aria-hidden", true);
        });
}

To apply this reset-function, we'll call it everytime we do a validation, before the check. This way we make sure it's always set to the initial state before we do anything. We'll insert the following line as first thing in the "validateElement"-function.

function validateElement(element) {
  resetValidation(element);
  //...
}

Lastly, we need to prevent the form-submit, if there are any invalid fields left. The user might click directly on "Submit" without focusing out of a field, which would leave no chance to correct the input. We'll add an event-listener to the form, which checks all fields and prevents the submit, if some are not valid. We'll also add the "invalid"-class to the whole form, so the user is certain that something wrong happened.

form.addEventListener("submit", event => {
    let formIsValid = true;
    form.classList.remove("invalid");

    formElements.forEach(formElement => {
        if (!formElement.dataset) return;
        if (!formElement.dataset.validate) return;
        validateElement(formElement);
    });

    formIsValid = form.querySelectorAll(".invalid").length === 0;

    if (formIsValid === false) {
        form.classList.add("invalid");
        event.preventDefault();
    }
});


`

A Working example

That's it! We now have a basic working inline-validation for our form. Here's the working example, feel free to fork it and play around with it:

Extending and improving

We have established a minimal working validation. Here are some possible ways this could be extended:

  • Adding animation to error-messages for better visibility. The feedback-messages could flash for two seconds, so it's clearer that something went wrong.
  • For long forms, a scroll-to-first-error could be added in case the user still has errors left on form-submit.
  • Forms get complex very quickly. Sometimes, the validation of one field depends on the value of another. For example: A field might be required, if the user is younger than a certain age. In that case, the validators would have to be extended to receive arguments from the data-attribute.
  • To activate/disable entire groups of inputs, we could add a "data-disable-validation"-attribute to their container, and prevent checking an input if it has such a parent-element.

That's all for now. If you like to give feedback or suggestions, hit me up on Twitter.

Thanks for reading!

Top comments (0)