Extending HTML Form Validation
HTML is awesome; it includes so much out-of-the-box. For example,
<form>
will validate its controls upon submission and report if there
are any validation errors. You can use attributes like required
,
maxlength
, pattern
, etc. to set the validation for different fields.
While these can get you pretty far, sometimes you might want to provide
some more complex validation. Often, this is where developers plop
novalidate
on the <form>
element and then use full replacement for
the browser’s built-in validation. Instead of replacing it, you can use
the Web platform’s Constraint Validation API to extend the built-in
functionality.
Using the Constraint Validation API
The Constraint Validation API allows one to add custom complex validation in their forms (among other things not described in this post).
Here’s an example: there are two text inputs that you want to be unique from each other. We’ll wrap them in a custom element, since they are great for bundling up this sort of functionality.
<form>
<unique-values>
<fieldset>
<legend>Unique values</legend>
<label>A <input name="a"></label>
<label>B <input name="b"></label>
</fieldset>
</unique-values>
<button>Submit</button>
</form>
Now, let’s scaffold the custom element and listen for the input
event
on the <fieldset>
:
class UniqueValues extends HTMLElement {
get fieldset() { return this.querySelector(':scope > fieldset'); }
connectedCallback() {
this.fieldset.addEventListener('input', event => {
// TODO: set and report the validity.
});
}
}
window.customElements.define('unique-values', UniqueValues);
The Constraint Validation API provides a few methods for setting,
checking, and reporting validity. We’ll use
.setCustomValidity(message)
and .reportValidity()
. The first allows
us to set a custom error message. The second is what spawns the tooltip
on a form control.
We’ll use the input
event as our validation logic isn’t very costly
performance-wise. This also follows how HTML seems to work. If you want
to see for yourself, use CSS to style the :invalid
state of a form
control with one the basic HTML validation constraints (e.g. required
,
minlength
, pattern
). The validity is checked as the input changes.
In the input
event handler for <fieldset>
, we can compare the values
of both nested controls, then set and report the validity accordingly.
this.fieldset.addEventListener('input', event => {
const {a, b} = event.currentTarget.elements;
if (a.value === b.value) {
// If the values are the same, set the error, and report it on
// the current form control.
a.setCustomValidity('A and B must be unique.');
b.setCustomValidity('A and B must be unique.');
event.target.reportValidity();
} else {
// If the values aren’t the same, clear the error on each
// control.
a.setCustomValidity('');
b.setCustomValidity('');
}
});
Now, if you pay close attention to how HTML’s built-in validation works, you’ll notice that the tooltip doesn’t show until a submission attempt is made. I’m not presenting any hard opinion about whether or not you should do the same. That’s a decision that’s best determined by research and context, but that’s outside of the scope of this blog post.
You’ll also notice that HTML’s validation also takes place when the elements are first added to a page. We can move the event listener callback into a method.
connectedCallback() {
this.validate();
this.fieldset.addEventListener('input', this.validate.bind(this));
}
validate(event) {
const {a, b} = this.fieldset.elements; // Might as well use fieldset
if (a.value === b.value) {
a.setCustomValidity('A and B must be unique.');
b.setCustomValidity('A and B must be unique.');
// We’ll optionally chain the report call as the event won’t be
// there on page load and it shouldn’t report immediately anyway.
event?.target.reportValidity();
} else {
a.setCustomValidity('');
b.setCustomValidity('');
}
}
Now, here is the code altogether. Remember, this is just a demo. This code is not as robust as it could be (e.g. there are checks I’ve left out, missing teardown, and it’s not very reusable).
<form>
<unique-values>
<fieldset>
<legend>Unique values</legend>
<label>A <input name="a"></label>
<label>B <input name="b"></label>
</fieldset>
</unique-values>
<button>Submit</button>
</form>
<script>
class UniqueValues extends HTMLElement {
get fieldset() { return this.querySelector(':scope > fieldset'); }
connectedCallback() {
this.validate();
this.fieldset.addEventListener('input', this.validate.bind(this));
}
validate(event) {
const {a, b} = this.fieldset.elements;
if (a.value === b.value) {
a.setCustomValidity('A and B must be unique.');
b.setCustomValidity('A and B must be unique.');
event?.target.reportValidity();
} else {
a.setCustomValidity('');
b.setCustomValidity('');
}
}
}
window.customElements.define('unique-values', UniqueValues);
</script>