In this second part of a three-part article we will continue our study of native form validation in browsers. Part 1 discussed general UI considerations and CSS. Part 3 will discuss the native error messages and offer general recommendations to come to actually usable native form validation.
In this part we’re going to take a look at a few HTML features and the JavaScript API.
((This article was originally published on Samsung Internet’s Medium channel. Since I do not believe Medium will survive in the long run I re-publish it here.)
As usual in my articles, I’m quite vague about exact browser compatibility patterns because I already collated that information in the inevitable compatibility table. You can find the gory details there.
HTML supports many potentially useful input types and attributes. I did the basic research a while ago, and while some details will have changed, the overall picture is still that most browsers support most features decently.
Here I want to draw attention to two features missing from my old overview: how the title
attribute affects error messages, and the novalidate
attribute.
It’s simple, really. The content of the title
attribute of a field is added to the field’s error message only if the field has a pattern
. This is useful for giving clues about the exact nature of the pattern; something that is impossible for the browser to determine.
It would also be useful to use the title
for giving clues about the exact nature of fields that do not have a pattern
, but, as we’ll see throughout this article, we can’t have nice things because that would make things nice for us. And we’re born to suffer. So title
only works on pattern
.
The novalidate
attribute of forms works in most browsers. When present, the attribute tells the browser not to attempt any native validation. In addition to suppressing the native error messages it also suppresses all the rest of validation, so the form is submitted unless an old-fashioned form validation script that you wrote yourself prevents it.
If you want to retain part of native validation, but not the error messages, you have to use the invalid event, which will be explained in part 3.
Let’s turn to the JavaScript side of things. We will find an entirely different set of problems than in CSS that preclude useful form validation for entirely different reasons.
The Constraint Validation API is part of the HTML5 specification and that doesn’t really do a lot of useful things. (Gem: a form field value can be “suffering from being missing.”) Browsers support this API fairly well, with only one method lacking in older browsers. Unfortunately this is exactly the best-designed and most useful method.
Also, the creators of this spec did not pay any attention to what the CSS people were doing with :invalid
. Here’s an example:
As we saw in part 1, fieldset:invalid
works in most browsers and kicks in when at least one form field in the fieldset is invalid. The API allows us to use the checkValidity()
method on fieldsets as well, but it returns true
, even when the fieldset contains an invalid form field. (To make matters more complicated, several Chromia, but not the latest Google Chrome itself, implement checkValidity()
on fieldsets correctly.)
Right hand, meet left hand. The two of you should connect one of these days.
But anyway. Let’s start with an API feature that actually works. Every form field has a validity
property that contains a bunch of information about its invalidity. All browsers support nearly all properties, even though only a few are actually useful.
All properties come in the form formField.validity.propertyName
. They are best summarised in table form:
Property | Applies to | is true when |
---|---|---|
badInput | number |
the value is not a number |
patternMismatch | pattern |
the value does not conform to the pattern |
rangeOverflow | number |
the value is higher than the max attribute |
rangeUnderflow | number |
the value is lower than the min attribute |
stepMismatch | number |
the value does not conform to the step attribute |
tooLong | maxlength |
the user has attempted to add a character to a form field with a too-long default value |
tooShort | minlength |
the user has entered a character in the field, but there are fewer characters than the minlength value |
typeMismatch | email or URL |
the value is not an email address or a URL |
valid | any | the field is valid |
valueMissing | required |
the field is empty |
The properties that deal with number
fields are useful: we can figure out exactly what kind of error the user made, and adjust our error messages accordingly.
Unfortunately the other properties are rather pointless. If there’s an error in an email
, url
, required
, or pattern
field it’s immediately clear what the problem is. The extra properties of validity
are not necessary.
It would be useful if we’d get more specific data, such as “user hasn’t entered an @ in this email field.” Native error messages in fact do so in some browsers, but the validity
properties don’t.
At least these properties do not actively harm native form validation UX. You will start to appreciate such slight blessings before we’re done with the API.
And then there’s the tooLong
saga. This part of my research took way too long because the browsers saw fit to implement maxlength
and minlength
in a way that’s entirely different from all other constraints. I see no reason not to share my pain with you.
Take the following form field, and note it has a default value. If we validate it straight away we get the validity.typeMismatch
error we would expect:
<input type="URL" value="nonsense">
I did all my tests with this sort of wrong default values because it’s way faster than manually typing in values in five desktop browsers and twenty-five mobile browsers. That works absolutely fine, except with maxlength
and minlength
. Lo and behold, the following field is valid:
<input maxlength="5" value="nonsense">
No problem here, no errors to be thrown, and no, the value is certainly not too long, thanks so much for asking. Incidentally, this field also gets :valid
styles.
Try it here for yourself:
It turns out that maxlength
and minlength
only elecit a response from CSS and the API if the user has actually changed the value of the form field. Although this is not a bad idea in itself, it is vastly different from all the other constraints, and that’s what makes it wrong. Obviously, this exception was necessary in order to make our lives as web developers more miserable.
Before we study the three methods the Constraint Validation API offers, it’s a good idea to quickly review what we would actually like to do:
The validity
properties already allow us to do #3. Nonetheless we are offered an extra method: checkValidity()
. Personally I don’t see the need for it, especially since it does not tell us what is wrong with the field; it just returns true
or false
without further comment.
reportValidity()
also checks a field’s validity, and if it is invalid the native error message is shown. This is a genuinely useful method. Unfortunately it’s also the worst-supported of the three: Edge and quite a few mobile browsers do not support it.
Finally how do we set the text of a native error message? That is the domain of setCustomValidity('string')
. If you use it on a form field the error message becomes the content of the string. If you use an empty string as an argument it resets the error message to its default value. And if you use no argument? It gives an error. Obviously. Allowing an undefined argument to default to the empty string behaviour would be good design, and we’re all agreed this API should be as crappy as possible.
Setting the error message text is not the only thing this method does. If you use a string as an argument it also sets the form field’s validity to false
; if you use the empty string the validity becomes true
.
The problem here is that these two functionalities, while very useful of themselves, are combined in the same method. Setting the validity of a form field is a good idea; for instance, if it has a constraint other than the standard ones built into the browser. Being able to produce a custom error message is also a good idea. But these two quite different tasks should be the jobs of two different methods.
The current method forces us to jump through complicated hoops if we want to set the error message of a standard constraint, since we can only do so if the field in fact turns out to be invalid. It would become something like this:
var field = [the field we're checking]; if (!field.validity.valid) { field.setCustomValidity('custom error message'); } else { field.setCustomValidity(''); }
This is only a few lines of code. The problem is that you should run this code for each individual field every time the form is being readied for validation. That, too, is not impossible, but it’s kludgy and annoying. Above all, it’s bad design.
Anyway, here are the three methods, warts and all, in useful table form:
Method | return value | action |
---|---|---|
checkValidity() | boolean | Checks validity of element |
reportValidity() | boolean | Checks validity of element. If invalid, shows native error message. |
setCustomValidity('error') | none | Sets validity of element to false and sets error message to argument. |
setCustomValidity('') | Sets validity of element to true and restores dedault error message. |
|
setCustomValidity() | Error! You didn’t think you could afford not to send an empty string as an argument, did you? |
That concludes part 2. In part 3 we’ll discuss the native error messages, draw some conclusions, and create a list of recommendations for improvement — and boy, will that list be long!
This is the blog of Peter-Paul Koch, web developer, consultant, and trainer.
You can also follow
him on Twitter or Mastodon.
Atom
RSS
If you like this blog, why not donate a little bit of money to help me pay my bills?
Categories: