Translator: cherryjin

The original link

In the last article, I showed you how to use the native browser form validation mechanism by combining input types (for example, ) with validation attributes (like required and pattern).

Admittedly, this approach is very simple and lightweight. But it still has some disadvantages.

  1. You can style the error field with the :invalid pseudo-selector, but you can’t style the error message itself.

  2. Behavior is also inconsistent across browsers.

From Christian Holst and Luke Wroblewski (the two are separate), displaying an error message immediately when the user leaves a field and keeping the error message displayed until the user fixes it provides a faster and better user experience.

Unfortunately, none of the browsers behave this way. However, there are still ways to give the browser this behavior without going through a huge JavaScript form validation library.

Article Series:

  1. Constraint validation in HTML

  2. Write a constraint validation API in JavaScript (you’re reading this!)

  3. A Validity State API Polyfill

  4. Verify the MailChimp subscription form

Constraint validation API

In addition to HTML attributes, native browser validation also provides us with a JavaScript API that we can use to customize the behavior of form validation.

Although this API exposes only a few methods, they are very powerful, and the Validity State allows us to use the browser’s own field validation algorithm in scripts without having to write it ourselves.

In this article, I will show you how to use Validity State to customize forms to validate the behavior, style, and content of error messages.

Validity State

The validity attribute provides a range of information about the form field in the form of Boolean values (true/false).

var myField = document.querySelector('input[type=""text""]');
var validityState = myField.validity;

Copy the code

The returned object contains the following properties:

  • Valid – The value of the property is true when the field is validated.

  • ValueMissing – The property value is true when a required field is empty.

  • TypeMismatch – The attribute value is true if the type of the field is email or URL but the entered value is not of the correct type.

  • TooShort – The property value is true when a field has the minLength property set and the length of the entered value is less than the set value.

  • TooLong – True when a field has the maxLength property set and the length value entered is greater than the set value.

  • PatternMismatch – If a field contains a Pattern attribute and the entered value does not match the pattern, the attribute value will be true.

  • BadInput – The attribute value is true when the input type value is number and the input value is not a number.

  • StepMismatch – If the field has a step property and the entered value does not match the interval value, the value will be true.

  • RangeOverflow – When the field has a Max property and the numeric value entered is greater than the set maximum value, the value of this property is true.

  • RangeUnderflow – When the field has the min property and the numeric value entered is less than the set minimum, the value of this property is true.

With the validity attribute and the combination of our input type and HTML validation attribute, we can create a robust form validation script with a little JavaScript code that gives us a great user experience.

Let’s make it happen!

### Disable native form validation

Since we needed to write our own validation script, we added the novalidate attribute to the form to disable the browser’s native validation. We can still use the constraint validation API – we just want to prevent native error messages from being displayed.

The best way to add this property is to use JavaScript to ensure that the browser’s native form validation works if our script fails to load.

/ / when JS load add novalidate var attribute forms = document. QuerySelectorAll (' form '); for (var i = 0; i < forms.length; i++) { forms[i].setAttribute('novalidate', true); }Copy the code

There may be forms that you don’t want to validate (for example, search forms that show up on every page). So instead of running our validation script for every form, we validate only those forms that have the.validate class.

/ / when JS load add novalidate var attribute forms = document. QuerySelectorAll (' validate '); for (var i = 0; i < forms.length; i++) { forms[i].setAttribute('novalidate', true); }Copy the code

See Chris Ferdinandi’s demo Form Validation: Add novalidate Programmatic (@cferdinandi) on CodePen.

Validates a field when the user leaves it

Whenever a user leaves a field, we want to check that the value entered is valid. To do this, we need to register an event listener.

Instead of adding a listener for each field, we can use a technique called event bubbling (or event propagation) to listen for all blur events.

/ / to monitor all the lost focus event document. The addEventListener (' blur ', function (event) {/ / when loses focus some processing... }, true);Copy the code

You may notice that the last parameter of addEventListener is set to true. This parameter is called useCapture and is usually set to false. Blur events, unlike click events, do not bubble. Setting this parameter to true allows us to capture all blur events, not just those that occur on the elements we are listening on directly.

Next, we need to verify that the out-of-focus element is a field in the form with.validate. We can call event.target to retrieve this element and call event.target.form to retrieve its parent element. We can then use classList to check if the form has a Validation class.

If all of the above conditions are met, we can check if the field complies with the validation rules.

/ / to monitor all of the lost focus event document. The addEventListener (' blur ', function (event) {/ / only when the field is to verify that the form is to run the if (! event.target.form.classList.contains('validate')) return; Var error = event.target.validity; console.log(error); }, true);Copy the code

If the value of error is true, the field complies with the validation rules. Otherwise, an error is thrown.

See Chris Ferdinandi’s demo Form Validation: Validate On Blur (@cferdinandi) On CodePen.

### Catch error

Once we know that an error has occurred, it is very useful to know the details of the error. We can obtain this information using other Validity State attributes.

Since we’re going to test each property, it might be a little long to put the code together. We can write this part of the code in a separate function and pass the form fields to it.

Var hasError = function (field) {// Get the error}; Document.addeventlistner ('blur', function (event) {// Run if (! event.target.form.classList.contains('validate')) return; Var error = hasError(event.target); }, true);Copy the code

There are a few fields we need to ignore: the disabled fields, the file and reset input types, and the Submit input type and button. If a field is not in the above fields, we can verify it.

Var hasError = function (field) {// Do not validate submits, buttons, The file and reset inputs, as well as the disabled fields if (field. The disabled | | field. Type = = = 'file' | | field. Type = = = 'reset' | | field. Type = = = 'submit' || field.type === 'button') return; Var validity = field.validity; };Copy the code

If there is no error, we return null. Otherwise, we test each Validity State property until we find the error.

If we find the property, we return a string containing an error message. If none of the attributes has a true value but the validity attribute is false, we return a generic “”catchall”” error message (I can’t imagine a scenario where this would happen, but it’s always good to be prepared for a rainy day). .

Var hasError = function (field) {// do not validate submits, buttons, The file and reset inputs, as well as the disabled fields if (field. The disabled | | field. Type = = = 'file' | | field. Type = = = 'reset' | | field. Type = = = 'submit' || field.type === 'button') return; Var validity = field.validity; // Null if (validity.valid) return; // If (validity.valuemissing) return 'Please fill out this field.'; // If (validity.typemismatch) return 'Please use the correct input type.'; // If (validity.tooshort) return 'Please lengthen this text.'; // If (validity.toolong) return 'Please shorten this text.'; If (validity.badinput) return 'Please enter a number.'; // If the number does not match the step interval if (validity.stepmismatch) return 'Please select a valid value. // If (validity.rangeoverflow) return 'Please select a smaller value.'; If (validation. rangeUnderflow) return 'Please select a larger value.'; // If (validity.patternmismatch) return 'Please match the requested format.'; Return 'The value you entered for this field is invalid.'; };Copy the code

This is a good start, but we can also do some extra parsing to make our error hints more useful. For typeMismatch, we can check whether it is an email or URL and set error messages accordingly.

// If the input type is incorrect if (validity.typemismatch) {// Email if (field.type === 'Email ') return 'Please enter an Email address.  // URL if (field.type === 'url') return 'Please enter a URL.'; }Copy the code

If the value of the field is too long or too short, we can detect the length of the field itself and the actual length of the field. We can include this information in the error message.

// If (validity.tooshort) return 'Please lengthen this text to '+ field.getAttribute('minLength') +' characters or more. You are currently using ' + field.value.length + ' characters.'; // If (validity.toolong) return 'Please short this text to no more than '+ field.getAttribute('maxLength') +  ' characters. You are currently using ' + field.value.length + ' characters.';Copy the code

If a numeric field value is outside the specified range, we can include the set minimum and maximum values in our error message.

If (validation. rangeOverflow) return 'Please select a value that is no more than '+ field.getAttribute('max') + '.'; If (validity.rangeunderflow) return 'Please select a value that is no less than '+ field.getAttribute('min') + '.';Copy the code

If pattern does not match the field value, and the field has a title, we use the value of the title attribute as our error message, just like native browser behavior.

// If the pattern does not match if (validity.patternmismatch) { If (field.hasattribute ('title')) return field.getAttribute('title'); Return 'Please match the requested format.'; }Copy the code

This is the complete code for our hasError() function.

Var hasError = function (field) {// do not validate submits, buttons, The file and reset inputs, as well as the disabled fields if (field. The disabled | | field. Type = = = 'file' | | field. Type = = = 'reset' | | field. Type = = = 'submit' || field.type === 'button') return; Var validity = field.validity; // Null if (validity.valid) return; // If (validity.valuemissing) return 'Please fill out this field.'; // If the type is incorrect if (validity.typemismatch) {// Email if (field.type === 'Email ') return 'Please enter an Email address. // URL if (field.type === 'url') return 'Please enter a URL.'; } // If (validity.tooshort) return 'Please lengthen this text to '+ field.getAttribute('minLength') +' characters or more. You are currently using ' + field.value.length + ' characters.'; // If (validity.toolong) return 'Please shorten this text to no more than '+ field.getAttribute('maxLength')  + ' characters. You are currently using ' + field.value.length + ' characters.'; If (validity.badinput) return 'Please enter a number.'; // If the number does not match the step interval if (validity.stepmismatch) return 'Please select a valid value. // If (validity.rangeoverflow) return 'Please select a value that is no more than '+ field.getAttribute('max') + '.'; If (validation. rangeUnderflow) return 'Please select a value that is no less than '+ field.getAttribute('min') + '.'; // If the pattern does not match if (validity.patternmismatch) { If (field.hasattribute ('title')) return field.getAttribute('title'); Return 'Please match the requested format.'; } return 'The value you entered for this field is invalid.'; };Copy the code

Try it for yourself on Codepen.

See Chris Ferdinandi’s demo Form Validation: Get the Error (@cferdinandi) on CodePen.

Display error message

Once we catch the error, we can display it below the field. We can do this by creating a showError() function, passing in the form field and error message, and then calling it in our event listener.

Var showError = function (field, error) { }; / / monitor all the blur event document. The addEventListener (' blur ', function (event) {/ / only when the field is to verify that the form is to run the if (! event.target.form.classList.contains('validate')) return; Var error = hasError(event.target); If (error) {showError(event.target, error); } }, true);Copy the code

In our showError function, we implement the following:

  1. We can add a class to the field with the error so that we can style it.

  2. If an error message already exists, we update it with new information.

  3. Otherwise, we create a message and insert it after the field in the DOM structure.

We use the field ID to create a unique ID for this information, which we can retrieve later (return the name of the field if there is no ID).

Var showError = function (field, error) {// Add ('error'); / / to obtain field id or name var id = field. The id | | field. The name; if (! id) return; Var message = field.form.querySelector('. Error -message#error-for-' + id); if (! message) { message = document.createElement('div'); message.className = 'error-message'; message.id = 'error-for-' + id; field.parentNode.insertBefore( message, field.nextSibling ); } // Update the error message message.innerhtml = error; // Display error message message.style.display = 'block'; message.style.visibility = 'visible'; };Copy the code

To ensure that screen browsers or other assistive technologies know that our error messages are associated with fields, we need to add an Aria-Describedby.

Var showError = function (field, error) {// Add the error class to field. Classlist.add ('error'); / / to get field id or name var id = field. The id | | field. The name; if (! id) return; Var message = field.form.querySelector('. Error -message#error-for-' + id); if (! message) { message = document.createElement('div'); message.className = 'error-message'; message.id = 'error-for-' + id; field.parentNode.insertBefore( message, field.nextSibling ); } // Add ARIA role to field field.setAttribute('aria-describedby', 'error-for-' + id); // Update the error message message.innerhtml = error; // Display error message message.style.display = 'block'; message.style.visibility = 'visible'; };Copy the code

Style error messages

We can use the.error and.error-message classes to style our form fields and error messages.

As a simple example, you might want to change the border of a field to red when an error occurs and have the error message displayed in red italics.

.error {
  border-color: red;
}

.error-message {
  color: red;
  font-style: italic;
}

Copy the code

See Chris Ferdinandi’s demo Form Validation: Display the Error (@cferdinandi) on CodePen.

Hiding error messages

Once we show a bug, your users will fix it (hopefully). When the field validates, we want to remove this error message. So, let’s create another function, removeError(), and pass in the field. We still call this function in the event listener.

Var removeError = function (field) {// Remove the error message... }; / / monitor all the blur event document. The addEventListener (' blur ', function (event) {/ / only when the field is to verify that the form is to run the if (! event.target.form.classList.contains('validate')) return; Var error = event.target.validity; If (error) {showError(event.target, error); return; } // If not, remove all existing error messages removeError(event.target); }, true);Copy the code

In the removeError() function, we want to do the following:

  1. Removes the error class for the field.

  2. Remove the field aria-Describedby.

  3. Hide all error messages in the DOM.

Sometimes there are multiple forms on a page, and they may have the same name or ID (even if this is not the norm, it does happen). , we use the querySelector method to search for error messages only on the current form field, not the entire document.

Var removeError = function (field) {field.classlist. remove('error'); // Remove the ARIA role field. RemoveAttribute ('aria-describedby'); / / obtain the field id or name var id = field. The id | | field. The name; if (! id) return; Var message = field.form.querySelector('. Error -message#error-for-' + id + '); if (! message) return; // If so, hide it message.innerhtml = "; message.style.display = 'none'; message.style.visibility = 'hidden'; };Copy the code

See Chris Ferdinandi’s demo Form Validation: Remove the Error After It’s Fixed (@cFerdinandi) on CodePen.

If the field is a radio button or check box, we need to change the way we add the error message to the DOM structure.

Label The label often comes after a field or surrounds a field, like those input types. Also, if the radio button is part of a collection, we want this error message to appear after the collection and not after the radio button.

See Chris Ferdinandi’s demo Form Validation: Issues with Radio Buttons & Checkboxes (@cFerdinandi) on CodePen.

First, we need to modify the showError() method. If a field’s type is radio and it has a name, we get all radio buttons with the same name (i.e. All the other radio buttons in the collection) and assigns the last element in the collection to the field variable.

Var showError = function (field, error) {field.classlist.add ('error'); // If the field is a radio button and is part of a collection, add error classes to all fields and get the last element of the collection if (field.type === 'radio' && field.name) {var group = document.getElementsByName(field.name); if (group.length > 0) { for (var i = 0; i < group.length; If (group[I].form!) {if (group[I].form! == field.form) continue; group[i].classList.add('error'); } field = group[group.length - 1]; }}... };Copy the code

When we add our information to the DOM, we should first check whether the field is of type radio or checkbox. If so, we get the label label and insert our error message after the label label, not after the field.

Var showError = function (field, error) {... Var message = field.form.querySelector('. Error -message#error-for-' + id); if (! message) { message = document.createElement('div'); message.className = 'error-message'; message.id = 'error-for-' + id; // If the field is a radio button or check box, insert the error field after the label field var label; if (field.type === 'radio' || field.type ==='checkbox') { label = field.form.querySelector('label[for=""' + id + '""]') || field.parentNode; if (label) { label.parentNode.insertBefore( message, label.nextSibling ); }} // Otherwise, insert after the field itself if (! label) { field.parentNode.insertBefore( message, field.nextSibling ); }}... };Copy the code

When we remove the error message, we also need to check if the field is a radio button in the collection, and if so, use the last radio button in the collection to get the ID of our error message.

Var removeError = function (field) {field.classlist. remove('error'); // If the field is a radio button and is part of a collection, Remove all error information and access to set the last element of the if (field. Type = = = 'radio' && field. The name) {var group = document. The getElementsByName (field. The name); if (group.length > 0) { for (var i = 0; i < group.length; i++) { // Only check fields in current form if (group[i].form ! == field.form) continue; group[i].classList.remove('error'); } field = group[group.length - 1]; }}... };Copy the code

See Chris Ferdinandi’s demo Form Validation: Fixing Radio Buttons & Checkboxes (@cferdinandi) on CodePen.

Check all fields when submitting the form

When a user submits a form, we validate every field in the form and display an error message under any invalid field. We also need to give error fields a default focus so that users can fix them first.

To do this, we need to add an event listener to the Submit event.

/ / check that all fields before submit the document. The addEventListener (' submit ', function (event) {/ / verify all field... }, false);Copy the code

If the form has a.validate class, we get all the fields of the form and iterate through them in turn to check for error messages. We store form fields with detected errors in a variable and bring them into focus after the inspection. If no errors are found, the form can be submitted normally.

/ / check that all fields when submit the document. The addEventListener (' submit ', function (event) {/ / can only run on the marker for verification form if (! event.target.classList.contains('validate')) return; Var fields = event.target.elements; // Validate each field // Store the first field with an error in a variable so that we will get focus by default later var error, hasErrors; for (var i = 0; i < fields.length; i++) { error = hasError(fields[i]); if (error) { showError(fields[i], error); if (! hasErrors) { hasErrors = fields[i]; If (hasErrors) {event.preventdefault (); if (event.preventdefault (); hasErrors.focus(); } // Otherwise, submit the form normally // You can also fill in the Ajax form submission process here}, false);Copy the code

See Chris Ferdinandi’s demo Form Validation: Validate on Submit (@cferdinandi) on CodePen.

Put all the code together

Our code is only 6KB (2.7KB compressed). You can download the plugin version on GitHub.

It works on all modern browsers and supports browsers up to IE10. However, the browser itself still has some flaws…

  1. Everything is not rosy, and not all browsers support every Validity State attribute.

  2. Internet Explorer is like this, of course, and while IE10+ supports tooLong, Edge does not. Let’s be clear about that.

Here’s the good news: with a lightweight polyfill (5KB, 2.7KB after compression) we can extend browser support to IE9 by adding properties that IE doesn’t support without touching our core code.

There is one field that all browsers except IE9 support: radio buttons. IE9 does not support CSS3 selectors (like [name=””‘ + field.name + ‘””]). So when we use this method to determine the selected button in a collection, IE9 will report an error.

In the next article, I’ll show you how to create this polyfill.

Article Series:

  1. Constraint validation in HTML

  2. Write one (you’re reading this!) in JavaScript.

  3. A Validity State API Polyfill

  4. Verify the MailChimp subscription form