HTML5 Forms: JavaScript and the Constraint Validation API

Craig Buckler
Share

For the final article in this three-part series about HTML5 web forms we’ll discuss JavaScript integration and the Constraint Validation API. If you’ve not done so already, please read The Markup and CSS articles to ensure you’re familiar with the concepts.

HTML5 allows us to implement client-side form validation without any JavaScript coding. However, we need to enhance native validation when implementing more sophisticated forms because:

  • not all browsers support all HTML5 input types and CSS selectors
  • error message bubbles use generic text (‘please fill out this field’) and are difficult to style
  • :invalid and :required styles are applied on page load before the user interacts with the form.

A sprinkling of JavaScript magic and the Constraint Validation API can improve the user experience. Be aware that this can get a little messy if you want to support a wide range of browsers and input types which we’ll endeavor to do.

Intercepting Form Submissions

Before HTML5, client-side validation involved attaching a submit handler to the form which would validate the fields, display errors and prevent the submit event.

In HTML5, the browser will perform its own validation first — the submit event will not fire until the form is valid. Therefore, if you want to do something sophisticated such as displaying your own errors, comparing or auto-filling fields, you must switch off native validation by setting the form’s noValidate property to true:

var form = document.getElementById("myform");
form.noValidate = true;

// set handler to validate the form
// onsubmit used for easier cross-browser compatibility
form.onsubmit = validateForm;

Of course, this means you must check for field errors in code, but it is still possible to leverage native browser validation as we’ll see shortly.

The Field .willValidate Property

Every input field has a .willValidate property. This returns:

  • true when the browser will natively validate the field
  • false when the browser will not validate the field, or
  • undefined when the browser does not support native HTML5 validation, e.g. IE8.

Since we disabled native validation above, every field will return false. Let’s create our validateForm handler which loops through all fields and checks whether native validation is available:

function validateForm(event) {

	// fetch cross-browser event object and form node
	event = (event ? event : window.event);
	var
		form = (event.target ? event.target : event.srcElement),
		f, field, formvalid = true;

	// loop all fields
	for (f = 0; f < form.elements; f++) {

		// get field
		field = form.elements[f];

		// ignore buttons, fieldsets, etc.
		if (field.nodeName !== "INPUT" && field.nodeName !== "TEXTAREA" && field.nodeName !== "SELECT") continue;

The loop iterates through all fields in the form’s elements collection and checks they’re inputs rather than other types such as buttons and fieldsets. The next line is important…

// is native browser validation available?
		if (typeof field.willValidate !== "undefined") {

			// native validation available

		}
		else {

			// native validation not available

		}

Both false and undefined are falsey values so you cannot check just field.willValidate!

We now know code inside the first block will evaluate when native validation can be used. However…

Does the Browser Support the Input Type?

If you read part one, you’ll recall that unsupported input types fall back to text. For example:

<input type="date" name="dob" />

is not natively supported in Firefox 29 or IE11. Those browsers will (effectively) render:

<input type="text" name="dob" />

BUT both browsers support validation for text types so field.willValidate will NOT return undefined! We must therefore check that our type attribute matches the object’s .type property — if they don’t match, we need to implement legacy fall-back validation, e.g.

// native validation available
			if (field.nodeName === "INPUT" && field.type !== field.getAttribute("type")) {

				// input type not supported! Use legacy JavaScript validation

			}

The Field .checkValidity() Method

If native validation is available, the .checkValidity() method can be executed to validate the field. The method returns true if there are no issues or false otherwise.

There is a similar .reportValidity() method which returns the current state without re-checking although this is less useful and not supported in all browsers.

Both methods will also:

  1. set the field’s .validity object so errors can be inspected in more detail and
  2. fire an invalid event on the field when validation fails. This could be used to show errors, change colors etc. Note there is no corresponding valid event so remember to reset error styles and messages if necessary.

The Field .validity Object

The .validity object has the following properties:

.valid – returns true if the field has no errors or false otherwise.
.valueMissing – returns true if the field is required and but value has been entered.
.typeMismatch – returns true if the value is not the correct syntax, e.g. a badly-formatted email address.
.patternMismatch – returns true if the value does not match the pattern attribute’s regular expression.
.tooLong – returns true if the value is longer than the permitted maxlength.
.tooShort – returns true if the value is shorter than the permitted minlength.
.rangeUnderFlow – returns true if the value is lower than min.
.rangeOverflow – returns true if the value is higher than max.
.stepMismatch – returns true if the value does not match the step.
.badInput – returns true if the entry cannot be converted to a value.
.customError – returns true if the field has a custom error set.

Not all properties are supported in all browsers so be wary about making too many assumptions. In most cases, .valid or the result of .checkValidity() should be enough to show or hide error messages.

Supporting .validity in Older Browsers

You can manually emulate the .validity object in legacy browsers, e.g.

// native validation not available
			field.validity = field.validity || {};

			// set to result of validation function
			field.validity.valid = LegacyValidation(field);

This ensures .validity.valid can be tested in all browsers.

The Field .setCustomValidity() Method

The .setCustomValidity() method can either be passed:

  • an empty string. This sets the field as valid so .checkValidity() and .validity.valid will return true, or
  • a string containing an error message which will be shown in the message bubble (if used). The message also flags the field as failing so .checkValidity() and .validity.valid will return false and the invalid event will fire.

Note that you can also check the current message using the field’s .validationMessage property.

Putting it all Together

We now have the basis of a simple, generic cross-browser form validation system:

var form = document.getElementById("myform");
form.noValidate = true;

// set handler to validate the form
// onsubmit used for easier cross-browser compatibility
form.onsubmit = validateForm;

function validateForm(event) {

	// fetch cross-browser event object and form node
	event = (event ? event : window.event);
	var
		form = (event.target ? event.target : event.srcElement),
		f, field, formvalid = true;

	// loop all fields
	for (f = 0; f < form.elements; f++) {

		// get field
		field = form.elements[f];

		// ignore buttons, fieldsets, etc.
		if (field.nodeName !== "INPUT" && field.nodeName !== "TEXTAREA" && field.nodeName !== "SELECT") continue;

		// is native browser validation available?
		if (typeof field.willValidate !== "undefined") {

			// native validation available
			if (field.nodeName === "INPUT" && field.type !== field.getAttribute("type")) {

				// input type not supported! Use legacy JavaScript validation
				field.setCustomValidity(LegacyValidation(field) ? "" : "error");

			}

			// native browser check
			field.checkValidity();

		}
		else {

			// native validation not available
			field.validity = field.validity || {};

			// set to result of validation function
			field.validity.valid = LegacyValidation(field);

			// if "invalid" events are required, trigger it here

		}

		if (field.validity.valid) {

			// remove error styles and messages

		}
		else {

			// style field, show error, etc.

			// form is invalid
			formvalid = false;
		}

	}

	// cancel form submit if validation fails
	if (!formvalid) {
		if (event.preventDefault) event.preventDefault();
	}
	return formvalid;
}


// basic legacy validation checking
function LegacyValidation(field) {

	var
		valid = true,
		val = field.value,
		type = field.getAttribute("type"),
		chkbox = (type === "checkbox" || type === "radio"),
		required = field.getAttribute("required"),
		minlength = field.getAttribute("minlength"),
		maxlength = field.getAttribute("maxlength"),
		pattern = field.getAttribute("pattern");

	// disabled fields should not be validated
	if (field.disabled) return valid;

    // value required?
	valid = valid && (!required ||
		(chkbox && field.checked) ||
		(!chkbox && val !== "")
	);

	// minlength or maxlength set?
	valid = valid && (chkbox || (
		(!minlength || val.length >= minlength) &&
		(!maxlength || val.length <= maxlength)
	));

	// test pattern
	if (valid && pattern) {
		pattern = new RegExp(pattern);
		valid = pattern.test(val);
	}

	return valid;
}

The LegacyValidation is purposely left simple; it checks required, minlength, maxlength and pattern regular expressions, but you’ll need additional code to check for emails, URLs, dates, numbers, ranges, etc.

Which leads to the question: if you’re writing field validation code for legacy browsers, why bother using the native browser APIs? A very good point! The code above is only necessary if you wanted to support all browsers from IE6 up and offer a similar user experience. That wouldn’t always be necessary…

  • You may not require any JavaScript code for simple forms. Those using legacy browsers could fall back to server-side validation — which should always be implemented.
  • If you require more sophisticated forms but only need to support the latest browsers (IE10+), you can remove all the legacy validation code. You would only require additional JavaScript if your forms used dates which aren’t currently supported in Firefox and IE.
  • Even if you do require code to check for fields such as emails, numbers etc. in IE9 and below, keep it simple and remove it once you cease to support those browsers. It’s a little messy now, but the situation will improve.

But remember to always use the correct HTML5 field type. The browsers will provide native input controls and enforce faster client-side validation even when JavaScript is disabled.