Client-side form validation
Client-side validation is an initial check and an important feature of good user experience; by catching invalid data on the client-side, the user can fix it straight away. If it gets to the server and is then rejected, a noticeable delay is caused by a round trip to the server and then back to the client-side to tell the user to fix their data.
However, client-side validation should not be considered an exhaustive security measure! Your apps should always perform validation, including security checks, on any form-submitted data on the server-side as well as the client-side, because client-side validation is too easy to bypass, so malicious users can still easily send bad data through to your server.
Different types of client-side validation
There are two different types of client-side validation that you'll encounter on the web:
- HTML form validation HTML form attributes can define which form controls are required and which format the user-entered data must be in to be valid.
- JavaScript form validation JavaScript is generally included to enhance or customize HTML form validation.
Client side validation can be accomplished with little to no JavaScript. HTML validation is faster than JavaScript, but is less customizable than JavaScript validation. It is generally recommended to begin your forms using robust HTML features, then enhance the user experience with JavaScript as needed.
Using built-in form validation
One of the most significant features of form controls is the ability to validate most user data without relying on JavaScript. This is done by using validation attributes on form elements. We've seen many of these earlier in the course, but to recap:
required
: Specifies whether a form field needs to be filled in before the form can be submitted.minlength
andmaxlength
: Specifies the minimum and maximum length of textual data (strings).min
,max
, andstep
: Specifies the minimum and maximum values of numerical input types, and the increment, or step, for values, starting from the minimum.type
: Specifies whether the data needs to be a number, an email address, or some other specific preset type.pattern
: Specifies a regular expression that defines a pattern the entered data needs to follow.
If the data entered in a form field follows all of the rules specified by the attributes applied to the field, it is considered valid. If not, it is considered invalid.
When an element is valid, the following things are true:
- The element matches the
:valid
CSS pseudo-class, which lets you apply a specific style to valid elements. The control will also match:user-valid
if the user has interacted with the control, and may match other UI pseudo-classes, such as:in-range
, depending on the input type and attributes. - If the user tries to send the data, the browser will submit the form, provided there is nothing else stopping it from doing so (e.g., JavaScript).
When an element is invalid, the following things are true:
- The element matches the
:invalid
CSS pseudo-class. If the user has interacted with the control, it also matches the:user-invalid
CSS pseudo-class. Other UI pseudo-classes may also match, such as:out-of-range
, depending on the error. These let you apply a specific style to invalid elements. - If the user tries to send the data, the browser will block the form submission and display an error message. The error message will differ depending on the type of error. The Constraint Validation API is described below.
Built-in form validation examples
In this section, we'll test out some of the attributes that we discussed above.
Simple start file
Let's start with a simple example: an input that allows you to choose whether you prefer a banana or a cherry. This example involves a basic text <input>
with an associated <label>
and a submit <button>
.
<form>
<label for="choose">Would you prefer a banana or cherry?</label>
<input id="choose" name="i-like" />
<button>Submit</button>
</form>
input:invalid {
border: 2px dashed red;
}
input:valid {
border: 2px solid black;
}
The required attribute
A common HTML validation feature is the required
attribute. Add this attribute to an input to make an element mandatory. When this attribute is set, the element matches the :required
UI pseudo-class and the form won't submit, displaying an error message on submission, if the input is empty. While empty, the input will also be considered invalid, matching the :invalid
UI pseudo-class.
If any radio button in a same-named group has the required
attribute, one of the radio buttons in that group must be checked for the group to be valid; the checked radio doesn't have to be the one with the attribute set.
Validating against a regular expression
Another useful validation feature is the pattern
attribute, which expects a Regular Expression as its value. A regular expression (regexp) is a pattern that can be used to match character combinations in text strings, so regexps are ideal for form validation and serve a variety of other uses in JavaScript.
Regexps are quite complex, and we don't intend to teach you them exhaustively in this article. Below are some examples to give you a basic idea of how they work.
a
— Matches one character that isa
(notb
, notaa
, and so on).abc
— Matchesa
, followed byb
, followed byc
.ab?c
— Matchesa
, optionally followed bya
singleb
, followed byc
. (ac
orabc
)ab*c
— Matchesa
, optionally followed by any number of bs, followed byc
. (ac
,abc
,abbbbbc
, and so on).abc|xyz
— Matches exactlyabc
or exactlyxyz
(but notabcxyz
ora
ory
, and so on).
Validating forms using JavaScript
If you want to change the text of the native error messages, JavaScript is needed. In this section we will look at the different ways to do this.
The Constraint Validation API
The Constraint Validation API consists of a set of methods and properties available on the following form element DOM interfaces:
HTMLButtonElement
, HTMLFieldSetElement
, HTMLInputElement
, HTMLOutputElement
, HTMLSelectElement
, HTMLTextAreaElement
The Constraint Validation API makes the following properties available on the above elements.
validationMessage
: Returns a localized message describing the validation constraints that the control doesn't satisfy (if any). If the control is not a candidate for constraint validation (willValidate
isfalse
) or the element's value satisfies its constraints (is valid), this will return an empty string.validity
: Returns aValidityState
object that contains several properties describing the validity state of the element. You can find full details of all the available properties in the ValidityState reference page; below is listed a few of the more common ones:valid
: Returnstrue
if the element meets all its validation constraints, and is therefore considered to be valid, orfalse
if it fails any constraint. If true, the element matches the:valid
CSS pseudo-class; the:invalid
CSS pseudo-class otherwise.valueMissing
: Returnstrue
if the element has arequired
attribute, but no value, orfalse
otherwise. If true, the element matches the:invalid
CSS pseudo-class.typeMismatch
: Returnstrue
if the value is not in the required syntax (whentype
isemail
orurl
), orfalse
if the syntax is correct. Iftrue
, the element matches the:invalid
CSS pseudo-class.typeMismatch
: Returnstrue
if the value is not in the required syntax (whentype
isemail
orurl
), orfalse
if the syntax is correct. Iftrue
, the element matches the:invalid
CSS pseudo-class.
Implementing a customized error message
As you saw in the HTML validation constraint examples earlier, each time a user tries to submit an invalid form, the browser displays an error message. The way this message is displayed depends on the browser.
Customizing these error messages is one of the most common use cases of the Constraint Validation API. Let's work through an example of how to do this.
<form>
<label for="mail">
I would like you to provide me with an email address:
</label>
<input type="email" id="mail" name="mail" />
<button>Submit</button>
</form>
Add the following JavaScript to the page:
const email = document.getElementById("mail");
email.addEventListener("input", (event) => {
if (email.validity.typeMismatch) {
email.setCustomValidity("I am expecting an email address!");
} else {
email.setCustomValidity("");
}
});
Here we store a reference to the email input, then add an event listener to it that runs the contained code each time the value inside the input is changed.
Inside the contained code, we check whether the email input's validity.typeMismatch
property returns true
, meaning that the contained value doesn't match the pattern for a well-formed email address. If so, we call the setCustomValidity()
method with a custom message. This renders the input invalid, so that when you try to submit the form, submission fails and the custom error message is displayed.
If the validity.typeMismatch
property returns false, we call the setCustomValidity()
method with an empty string. This renders the input valid, so the form will submit. During validation, if any form control has a customError
that is not the empty string, form submission is blocked.
Example
<form novalidate>
<p>
<label for="mail">
<span>Please enter an email address:</span>
<input type="email" id="mail" name="mail" required minlength="8" />
<span class="error" aria-live="polite"></span>
</label>
</p>
<button>Submit</button>
</form>
This form uses the novalidate
attribute to turn off the browser's automatic validation. Setting the novalidate
attribute on the form stops the form from showing its own error message bubbles, and allows us to instead display the custom error messages in the DOM in some manner of our own choosing. However, this doesn't disable support for the constraint validation API nor the application of CSS pseudo-classes like :valid
, etc. That means that even though the browser doesn't automatically check the validity of the form before sending its data, you can still do it yourself and style the form accordingly.
Our input to validate is an <input type="email">
, which is required
, and has a minlength
of 8 characters. Let's check these using our own code, and show a custom error message for each one.
We are aiming to show the error messages inside a <span>
element. The aria-live
attribute is set on that <span>
to make sure that our custom error message will be presented to everyone, including it being read out to screen reader users.
Now onto some basic CSS to improve the look of the form slightly, and provide some visual feedback when the input data is invalid:
body {
font: 1em sans-serif;
width: 200px;
padding: 0;
margin: 0 auto;
}
p * {
display: block;
}
input[type="email"] {
appearance: none;
width: 100%;
border: 1px solid #333;
margin: 0;
font-family: inherit;
font-size: 90%;
box-sizing: border-box;
}
/* invalid fields */
input:invalid {
border-color: #900;
background-color: #fdd;
}
input:focus:invalid {
outline: none;
}
/* error message styles */
.error {
width: 100%;
padding: 0;
font-size: 80%;
color: white;
background-color: #900;
border-radius: 0 0 5px 5px;
box-sizing: border-box;
}
.error.active {
padding: 0.3em;
}
Now let's look at the JavaScript that implements the custom error validation. There are many ways to pick a DOM node; here we get the form itself and the email input box, as well as the span element into which we will place the error message.
Using event handlers, we check if the form fields are valid each time the user types something. If there is an error, we show it. If there is no error, we remove any error messaging.
const form = document.querySelector("form");
const email = document.getElementById("mail");
const emailError = document.querySelector("#mail + span.error");
email.addEventListener("input", (event) => {
if (email.validity.valid) {
emailError.textContent = ""; // Remove the message content
emailError.className = "error"; // Removes the `active` class
} else {
// If there is still an error, show the correct error
showError();
}
});
form.addEventListener("submit", (event) => {
// if the email field is invalid
if (!email.validity.valid) {
// display an appropriate error message
showError();
// prevent form submission
event.preventDefault();
}
});
function showError() {
if (email.validity.valueMissing) {
// If empty
emailError.textContent = "You need to enter an email address.";
} else if (email.validity.typeMismatch) {
// If it's not an email address,
emailError.textContent = "Entered value needs to be an email address.";
} else if (email.validity.tooShort) {
// If the value is too short,
emailError.textContent = `Email should be at least ${email.minLength} characters; you entered ${email.value.length}.`;
}
// Add the `active` class
emailError.className = "error active";
}
Every time we change the value of the input, we check to see if it contains valid data. If it has then we remove any error message being shown. If the data is not valid, we run showError()
to show the appropriate error.
Every time we try to submit the form, we again check to see if the data is valid. If so, we let the form submit. If not, we run showError()
to show the appropriate error, and stop the form submitting with preventDefault()
.