JavaScript
Article

HTML5 Forms: JavaScript and the Constraint Validation API

By Craig Buckler

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.

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

  • Raivo Laanemets

    I thought that it was not possible to call e.setCustomValidity() in the submit handler as validation must be done before the submit handler is called. Link to the issue: http://code.google.com/p/chromium/issues/detail?id=95970

    So looks like form.noValidate = true gets around it.

  • Craig Buckler

    Yep – that’s exactly what’s mentioned above!

  • alexander

    As developer of webshim I often have problems with those kinds of tutorials. While the feature detection and legacy implementation has spec violation (for example: Line 29-34 can be simply removed). The whole code was never tested (i.e.: There is no e.checked or e.checkValidity (because e is undefined)).

    But the heaviest problem is. We developer know two ways to write modern code and deal with legacy browsers. The first strategy is called progressive enhancement and the second one is called polyfills. Both strategys try to avoid code branching, inside of the project code. Especially if it comes to form validation most tutorials start to mix legacy code with modern code and therefore simply can never ever showcase the power of modern APIs.

    I tryed to showcase here something, that really can run a browser:
    1. progressive enhancement: http://jsfiddle.net/trixta/ru7jt/
    2. same logic but with polyfill: http://jsfiddle.net/trixta/q4NEn/
    3. similiar approach but using predefined, simply configurable instant validation for html5 forms: http://jsfiddle.net/trixta/T29Kx/light/

    Here is the “main” code for the PE example. As you might see it implements form validation totally different with less and simpler code:

    Array.prototype.forEach.call(document.querySelectorAll(‘form.validate’), function (form) {
    form.noValidate = true;

    form.addEventListener(‘submit’, function (e) {
    if (!form.checkValidity()) {
    e.preventDefault();
    form.querySelector(‘:invalid’).focus();
    }
    });

    form.addEventListener(‘blur’, function (e) {
    if (e.target.matches(‘:invalid’)) {
    setInvalid(e.target);
    } else {
    removeInvalid(e.target);
    }
    }, true);

    form.addEventListener(‘invalid’, function (e) {
    setInvalid(e.target);
    }, true);
    });

  • David Johnson

    Good points made and yes it is very naughty and lazy for the code not to be tested in this blog….loving the webshim stuff by the way :-)

  • Craig Buckler

    Thanks Alexander.

    The code was built and tested, but a few errors slipped in as I copied it to this article. The checks for ‘e.’ should have been ‘field.’ throughout. That said, it was never intended as a usable demo but an explanation of the API.

    Lines 29-34 are required. For example, Firefox and IE both ignore date fields and parse them as text – which will return false-positive validation. Your examples, while shorter, don’t cater for that situation or legacy browsers.

    • alexander

      Craig Buckler

      About lines 29-34. You are right neither current FF nor IE support date (which is tested), but your LegacyValidation does not implement type date either, it implements pattern/required/maxlength. All those attributes are a) implemented in FF/IE and b) do never ever apply on type=”date”. So still line 29-34 implements things, that are already supported, but should never implemented on field date.

      About my examples:

      You do say, that my examples are shorter, but do not work in legacy browsers.

      This isn’t true.

      All examples can be used in all browsers:

      The first example uses only modern JS APIs, so the validation is only done serverside (progressive enhancement).

      The second example uses polyfills, so you can still write modern code and get it work down to IE7/8/9. If you compare the code between version 1 and this second version, you will see it’s actually the same code, but only in a jQuerified way.

      And the third example uses additionally some extensions to turn a HTML5 form into instant validation form with great user experience by only adding some configuration and without writing some JS functions.

      And yes, webshim support also type=date/type=number and so on:
      http://afarkas.github.io/webshim/demos/index.html#Forms

      It’s seems you haven’t understood, what I mean here. The fact, that you do mix legacy code with modern features, makes your code worse and turns it into spaghetti code. If you want to support old browsers: You have the following options: 1. Progressive enhancement, 2. polyfills (you do not use polyfills, you do code branching, pseudo-filling), 3. only use legacy code.

      If you start code branching like you did, your code becomes unmaintainable, because you gather it like in your example with plenty of if and else statements and each browsers takes a different code path. + You will take only those parts of the modern API, which are simpel to simulate and not those, which make your life easier. For example :invalid/:valid selectors are not so useful in CSS, but extreme useful in conjunction with querySelector/matches. Also most developers won’t see any positive effect to use a modern API + some legacy API, if they could simply just use the legacy code. Less code + same code is run in all browser -> less testing.

      If you want to showcase modern APIs, than simply just use them or use a polyfill. Because a polyfill moves away all your different exception statements and bundles the legacy code at a different level. And to be honest a polyfill like webshims is battle tested and not so problematic.

      To make some points here. The whole LegacyValidation function is a spec violation.

      1. Your disabled (i.e.: willValidate) check: You check for disabled property. You should also check for readOnly. Additionally it’s not really about disabled property, it’s about :disabled matching (a small but important difference. see example here: http://jsfiddle.net/trixta/K8EPm/) (here is an implementation for this: https://github.com/aFarkas/webshim/blob/gh-pages/src/shims/form-shim-extend.js#L370-384)

      2. your required check does not work on radio buttons. A user has to check all radio buttons of one group at once??? This issue is quite heavy. Some nitpicking: what about right implementation of select:required. Have you heard about placeholder option? Here is an implementation: https://github.com/aFarkas/webshim/blob/gh-pages/src/shims/form-shim-extend.js#L118-132

      3. Your minlength/maxlength test: Minlength and Maxlength do only “kick in” if the field is dirty. Where do you check this???

      Minlength/maxlength attributes also only do apply to the following field types: url, email, text, search, tel, password and textarea. Normally, I would have not mentioned this error, but you explicitly said you want to run your code on type date. Here is a full implementation: https://github.com/aFarkas/webshim/blob/gh-pages/src/shims/form-shim-extend.js#L153-165

      4. Your pattern check: The generated RegExp is wrong and tests something different. Should look something like this: new RegExp(‘^(?:’ + pattern + ‘)$’);. Again the pattern attribute does not apply to all input elements.

      Your definition of reportValidity, I don’t really understand, what you mean, but it will become quite usefull.

      Something I also want to say. I’m really acting here as the author of the mentioned polyfill, but believe me: HTML5 form validation has a quite bad reputation, often because they only saw some mixed spagetthi code like in this tutorial. And couldn’t really dive into the great things or the bad ones (setCustomValidity is just bad API) (And yes the way you use setCustomValidity is wrong. Because you destroy other scripts, which also might use setCustomValidity.)

  • Craig Buckler

    Thanks for taking the time to write a thorough reply.

    The example above isn’t intended as a polyfill. It illustrates what you need to do to make a (reasonably) consistent experience across all browsers and implement your own validation styles in a fairly generic way. It results in spagetthi code, but that’s the current situation we’re in. I highlight the issues in the last section and things will improve over time.

    To answer your points:

    1. As I understand it, read-only is different to disabled in that the value will still be validated and posted.

    2. Checkboxes and radio buttons are handled, but LegacyValidation only ensures they’re checked if required is set.

    3. LegacyValidation performs a series of generic tests on attributes which could be applied anywhere, e.g. required, maxlength etc. This should be supplemented by data type validation checks which aren’t included because they bulk up the code and weren’t really the point of the article. (Including locale-agnostic date checks is an article in it’s own right!)

    I agree that Firefox/IE will do the basic checks because they’re supported on the fallback “text” input but, since we know the basic (“date”) validation fails, I simply call the function which handles legacy validation for all browsers. I could have checked each specific attribute was supported but that check is likely to be slower than doing the validation itself.

    4. Really? I’ll need to double-check that. I hadn’t noticed any regex issues, but I guess there could be certain cases.

    4a. The script *should* work with others which set setCustomValidity because it’ll fire the “invalid” event but, admittedly, it could overwrite any nicer error messages. That said, you might need to do that to prevent the default bubble when you want to create your own error messages (presuming default validation is enabled).

    Anyway, I think we can both agree the current situation is messy and likely to remain so for a little while yet.

    • PureAbsolute

      Unless you’ve already changed the way the pattern is used, it seems Alexander was being extra restrictive with his comment — for some reason he wanted all patterns to start at the beginning and end at the end, whereas your solution allows any pattern including his pattern style.

      About the article and this thread — I thought your article was fine, although I do appreciate Alexander’s coding style. His response had the attitude of correcting major wrongs when it could have been a less contentious set of pointers. Both of his points about Polyfills and Progressive Enhancements were great, and can make articles in their own right.

  • http://www.labstech.org Vijay Rajbhar

    Thanks for sharing very useful article for me.

  • Tom Baxter

    Overall, a very readable article. Thank you.

    One point: the description you give of “willValidate” is incorrect, I believe. Even after you set form.noValidate= true, the willValidate property is still true for every input element. This is contrary to what you say in the article. Unfortunately, I could not find what the W3C means by, “barred the element from constraint validation”.

  • Sazzad Tushar Khan

    Line#17 should be
    for (f = 0; f < form.elements.length; f++) {
    instead of
    for (f = 0; f < form.elements; f++) {

  • http://fancyjs.com FancyForm

    Try my form lib on JQuery, it has much for validation.
    http://fancyjs.com

  • http://www.thatsitguys.com/author/askyous Yousef Shanawany

    Thank you very much. This was helpful. I didn’t know there was such an attribute called setCustomValidity!

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in JavaScript, once a week, for free.