How to create your instant form validation

2018-12-17 · 4 min read

It is easy to use any libraries in JavaScript to add validations, however, have you ever wonder how the form validation works?

Let's see how we can roll out our own form validations with error messages!

What we are going to create today: alt="Our form with validation"


First step: create your patterns!

In the last few days, I tried to create my own form validation system for learning purpose. First we can do is: to create the basic structure of form.

<form class="c-form" id="form">
    <label for="email"><span class="c-form-label">Email <abbr title="Required">*</abbr></span>
        <input class="c-form-input" id="form-email" type="email" name="email" required="required" pattern=".{3,}" />
        <span class="u-alert" id="form-email-error">Please enter a valid email format</span>
    </label>
    <label for="name"><span class="c-form-label">Name</span>
        <input class="c-form-input" id="form-name" type="text" name="name" pattern="^[A-Za-z ,.'-]+$" />
        <span class="u-alert" id="form-name-error">Please enter a valid name with alphabets only</span>
    </label>
    <label for="message"><span class="c-form-label">Message <abbr title="Required">*</abbr></span>
        <textarea class="c-form-input" id="form-message" minlength="10" maxlength="200" rows="4" name="message"
            required="required"></textarea><span class="c-form-input__length"><span id="form-message-length">0</span>/200</span>
        <span class="u-alert" id="form-message-error">Please enter at least 10 characters and less than 200 characters.</span>
    </label>
    <label class="c-form-group" for="signup">
        <input class="c-form-checkbox" id="form-signup" type="checkbox" name="signup" />
        <span class="c-form-label">Sign me up for latest updates</span>
    </label>
    <input class="c-form-submit" id="form-submit" type="submit" value="Submit" name="submit" />
    <span class="u-alert" id="form-submit-error">Please correct all information and try again.</span>
</form>

The most important thing in this snippet is the pattern property in all input elements. The mysterious string in the pattern is Regular Expressions (RegEx), that means if the user input anything that mismatch the pattern, it becomes invalid.

For example, there is a store that only sold apple, and they have a form that allows user to input what fruit they want.

Unfortunately, they only have apple, so they can restrict people to enter apple only by using pattern="apple". Thus, all users can only enter apple, otherwise, the form becomes invalid.

Of course, no shop would do this, just for example :)


Step 2: hide the error message

Now, you form is ready, but the error message is always under the input element. How can we fix it?

We can use CSS to control the visibility of this element.

.c-form-label {
  display: block;
  margin: 1em 0 0.2em 0;
  font-size: 0.8em;
  text-transform: uppercase;
  color: rgba(25, 25, 25, 0.8);
}

.c-form-label abbr {
  text-decoration: none;
}

.u-alert {
  display: block;
  height: 0;
  opacity: 0;
  height: 0;
  overflow: hidden;
  transition: ease 400ms;
  font-size: 0.8em;
}

.u-alert.invalid {
  color: red;
  opacity: 1;
  height: auto;
  max-height: none;
  margin-top: 0.3em;
}

The base class of error message is u-alert, its default is opacity: 0 and height: 0. Of course, need to add overflow: hidden, otherwise it would still occupy your spaces.

If the form is invalid, we will add invalid class to this error message (later via JavaScript). You can see, if it adds invalid class, its opacity becomes 1 and height: auto.

You may wonder why we won't use display: none at u-alert class, the reason is that we want to have transitions between valid and invalid state. That's why I added transition: ease 400ms at the end.


Step 3: trigger the error message

After hiding our error messages, now it's time to trigger the error message!

First, we need to get all inputs in the page, using:

`Array.prototype.slice.call(document.getElementsByTagName('input'))`

Then, the browser will automatically get all inputs.

const inputList = Array.prototype.slice.call(document.getElementsByTagName('input')).filter(item => item.type !== 'submit' && item.type !== 'checkbox');
const input = inputList.concat(Array.prototype.slice.call(document.getElementsByTagName('textarea')));

//valid each item and set error message
  function isValid(input) {
    const target = input.id ? input.id : input.target.id;
    const valid = document.getElementById(target).validity.valid;
    if (valid) {
      document.getElementById(`${target}-error`).classList.remove('invalid');
      document.getElementById(`${target}-error`).removeAttribute('role');
      document.getElementById(target).setAttribute('aria-invalid', 'false');
    } else {
      document.getElementById(`${target}-error`).classList.add('invalid');
      document.getElementById(`${target}-error`).setAttribute('role', 'alert');
      document.getElementById(target).setAttribute('aria-invalid', 'true');
      console.log(document.getElementById(target));
    }

    if (document.getElementById('form-message').value.length !== length) {
      length = document.getElementById('form-message').value.length;
      document.getElementById('form-message-length').innerText = length;
    }

    return valid;
}

Tips: Why I would use slice here is for supporting IE11, as IE11 treats document.getElementsByTagName('input') as NodeList, not an array. We cannot directly use forEach without transforming to array. (Well, I know IE11 don't support arrow function. but this is just for demonstration purpose.)

The magic is coming!

input.forEach(item => {
  if (item.type !== 'submit' && item.type !== 'checkbox') {
    //add event listener for input
    item.addEventListener('input', function(e){
      isValid(e);
    })
}

Using event listener "input" (which supports well in all browsers), you can detect the validity when user is typing.

The difference between "change" and "input" is, change will trigger only after user stop typing and did not select that input element right now (out of focus). Input is continuously responses when user is typing or selecting something.


Step 4: instant validation is coming! Yay!

The last thing we need here is the "isValid" function. It uses the validation in browser (via the pattern property), you can get the valid status using document.getElementById(<your-element>).validity.valid and it returns boolean true or false.

Then add this CSS to make it works better:

.c-form-input {
  display: block;
  border-color: rgba(25, 25, 25, 0.1);
  border-width: 0 0 2px 0;
  padding: 0.2em 2em 0.2em 0;
  transition: border-color ease 300ms;
  background-repeat: no-repeat;
  background-size: 20px 20px;
  background-position: 99% 50%;
  width: 100%;
}

.c-form-input:focus {
  border-color: #03A9F4;
}

.c-form-input[aria-invalid="false"] {
  background-image: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path fill='%234CAF50' d='M9.984 17.016l9-9-1.406-1.453-7.594 7.594-3.563-3.563-1.406 1.406zM12 2.016c5.531 0 9.984 4.453 9.984 9.984s-4.453 9.984-9.984 9.984-9.984-4.453-9.984-9.984 4.453-9.984 9.984-9.984z'></path></svg>");
  margin-bottom: 0;
}

.c-form-input[aria-invalid="true"] {
  background-image: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path fill='%23F44336' d='M17.016 15.609l-3.609-3.609 3.609-3.609-1.406-1.406-3.609 3.609-3.609-3.609-1.406 1.406 3.609 3.609-3.609 3.609 1.406 1.406 3.609-3.609 3.609 3.609zM12 2.016c5.531 0 9.984 4.453 9.984 9.984s-4.453 9.984-9.984 9.984-9.984-4.453-9.984-9.984 4.453-9.984 9.984-9.984z'></path></svg>");
  border-color: red;
  margin-bottom: 0;
}

Tips: You can also use .c-form-input:invalid and .c-form-input:valid to create valid / invalid effects, but it will always show even the form is empty. That means when user first visit your form, they will see lots of red crosses!

Using aria-invalid to style input elements is good for both accessibility and user experience, since user will not see lots of red crosses when they visit your form at the first time.

For the background image, I have used inline SVG because:

  1. I don't want to link the SVG from my GitHub page
  2. It should be faster for loading performance, if browser did not need to get SVG from GitHub

(Icons are from Material Icon and get inline SVG from Icomoon)

Tips: for submission, you need to create a new function & event listener and add preventDefault() inside, otherwise it will show default error popup! (which is not very good-looking)


At the end

You can view my finished form here with styles and extra gimmicks like debounce function and custom event for auto complete validation.

See the Pen Instant Form Validation by Yuki (@snowleo208) on CodePen.

Hope you learn more about form validation! What is your thoughts on form validation? Do you have better ways to do so? Feel free to let me know! :)



Written by Yuki Cheung

Hey there, I am Yuki! I'm a front-end software engineer who's passionate about both coding and gaming.

When I'm not crafting code, you'll often find me immersed in the worlds of Fire Emblem and The Legend of Zelda—those series are my absolute favorites! Lately, I've been delving into the fascinating realm of AI-related topics too.