Angular Form Validation Basics
Form validation refers to the ability to check the status of the inputs your user gave you to make sure everything is as expected. For example, did they try to submit a phone number of "1"
instead of a valid phone number? Does the email address have the required @
symbol? Have they filled out all of the required fields?
Form validation outside of Angular can be a bit daunting. Lots of your code-writing time is spent checking the length of the input they gave you, making sure the data they gave you is valid, etc. Then you have to add some kind of message or error class to show the user they did it wrong, then remove that class/message when they fix it, etc.
In Angular, we're given some excellent tools to help make this process a lot smoother. This post will cover some of those tools.
To start, you may want to check out Scotch.io's excellent follow-alongs on AngularJS Form Validation and Form Validation w/ ngMessages. Much of what we cover here is adapted from their content.
What we're building
We'll be building a basic sign-up form. It will have the following fields and requirements:
- First name - optional
- Last name - optional
- Email address - required, must be valid email address
- Phone Number - optional
- Password - required, must be longer than 7 characters
- Password confirmation - required, must match the first password
- Button for submitting the info, which should just log to the console all of the person's info. (Obviously not what you'd really want to do with this info, but that's the topic for another post.)
We'll also be using bootstrap for our styling and layout so our form looks good.
Starting Point
Let's get some code set up as a starting point. Everything in here is basic HTML form stuff and the generic Angular setup. If you're unfamiliar with anything in this boilerplate code, you should probably study up on HTML forms and/or Angular first before moving on.
index.html:
<!DOCTYPE html>
<html lang="en" ng-app="ValidationPractice">
<head>
<meta charset="UTF-8">
<title>Signup Form</title>
<!-- Bring in Bootstrap from the CDN -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<!-- Add our own styles -->
<link rel="stylesheet" href="styles.css">
</head>
<body ng-controller="FormController" class="container">
<section class="row">
<div class="col-md-4 col-md-offset-4 well">
<form name="validateForm" novalidate> <!-- We add "novalidate" so HTML won't validate for us automatically. -->
<div class="form-group col-md-12">
<input type="text" name="firstName" class="form-control" placeholder="First Name" ng-model="person.firstName">
</div>
<div class="form-group col-md-12">
<input type="text" name="lastName" class="form-control" placeholder="Last Name" ng-model="person.lastName">
</div>
<div class="form-group col-md-12">
<input type="email" name="email" class="form-control" placeholder="Email" ng-model="person.email" required>
</div>
<div class="form-group col-md-12">
<input type="tel" name="phone" class="form-control" placeholder="Phone Number" ng-model="person.phone">
</div>
<div class="form-group col-md-12">
<input type="password" name="password" class="form-control" placeholder="Password" ng-model="person.password" required>
</div>
<div class="form-group col-md-12">
<input type="password" name="passwordRepeat" class="form-control" placeholder="Password Repeat" ng-model="person.passwordRepeat" required>
</div>
<button class="btn btn-primary btn-lg center-block" ng-click="addBadge(person)">Submit</button>
</form>
</div>
</section>
<!-- AngularJS from the CDN -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script>
<!-- Our own JS -->
<script src="app.js"></script>
</body>
</html>
######app.js:
var app = angular.module("ValidationPractice", []);
app.controller("FormController", ["$scope", function($scope) {
$scope.logPersonInfo = function(person) {
// More to come
}
}]);
A few important points about the above code:
- We added
novalidate
to the form. This prevents the default HTML5 built-in validations from running, since we'll be making our own. ng-model
is already applied to each input. It is building aperson
object with the appropriate properties on the fly for us.- The
email
,password
, andpasswordRepeat
fields are required with the built-in HTMLrequired
attribute added to the element.
Important Angular form and input properties
Angular automagically sets properties on each input type that allows us to access more information about the state of that input and the state of the form based on the user's interaction with the form. In other words, Angular gives us a way to know more information about how the user has interacted with the form, such as "is the current input valid according to your rules?", "has the user typed anything into this input box yet?", and "has the user entered and left this input box already?". These give us tons of flexibility to set our styles and messages at exact the times we want.
Here are the properties Angular adds to the form and each input, along with their descriptions:
$valid
- Boolean that tells you if the form or input is valid based on the rules you've set$invalid
- Boolean that tells you if the form or input isn't valid based on the rules you've set. Simply the exact opposite of$valid
.$dirty
- Boolean that tells you if the user has already interacted with the form or a individual input (typed something in, checked a checkbox, etc.). Remains true even if they delete what they typed or uncheck the box, etc.$pristine
- Boolean that tells you if the user hasn't yet interacted with the form or individual input. It will remain false even if they delete what they typed or uncheck the box, etc. Simply the exact opposite of$dirty
.$touched
- Boolean turns to true if the input undergoes a "blur" event, meaning the user has clicked into the input box and then clicked out of it, whether they type something or not.
Accessing these properties:
In vanilla JS, we're used to using something like document.<form name>
or document.getElementById("my-form")
. In Angular, they make it slightly simpler by allowing you to just access the form with <form name>
and access the input fields with <form name>.<input name>
. So you need to make sure to give your form and inputs a name
property to make this easier.
Once you've accessed the form or input, you just add the property listed above to get the boolean value of that property. For example:
myForm.firstName.$touched
will tell you whether or not the first name field sent off a blur event at some point.
Quick validation rules
Angular gives us some directives to make certain validation rules very easy for us to implement:
ng-minlength
sets the minimum length allowed for an input, meaning as long as the user's input is shorter than the specified minlength, it is not valid. (E.g.: We can addng-minlength="7"
on our password field, which sets the$invalid
property to true (and the$valid
property to false) until 7 or more characters are typed into the input box, at which time it flips those.
<!-- index.html -->
...
<!-- Password input field -->
<input type="password" name="password" class="form-control" placeholder="Password" ng-model="person.password" ng-minlength="7" required>
Cool things you can do now that you have the right tools
Disable the submit button
Because we set some validation rules (email address is required and must be valid, password must be 7 characters or longer), our whole form (and those specific fields) are considered "invalid" until they are fixed. This means that our validateForm.$invalid
is set to true until the user fixes those issues. So we can use the ng-disabled
directive to disable our button until the issues are fixed, preventing the user from submitted incomplete or invalid data.
<!-- index.html -->
...
<!-- Submit Button -->
<button class="btn btn-primary btn-lg center-block" ng-click="addBadge(person)" ng-disabled="validateForm.$invalid">Submit</button>
Add helpful messages when the user did it wrong
Because we can test the current state of the form and the input fields, we can show and hide helpful messages to the user when they're putting in the information incorrectly.
Using Angular's ng-show
directive and the validation properties set on our form and inputs, we can make it so a help message only shows to the user when something seems wrong. For example, if the email field is invalid.
However, it'd be a bit annoying to have the messages show up immediately when you load the page. To avoid this, we can add a conditional to the ng-show
telling it to only show the message if the email field is invalid and if they've already focused in on the field and then blurred away (remember $touched
?), since that would likely indicate they think they did everything right and they're moving on to the next input. Once $touched
flips to true and the email field is invalid, then we feel the obligation to show them the error/help message. And because of our strict &&
adding the text, if they go back and fix the problem (make the input valid), the message will go away.
We can put any kind of conditional checks in the ng-show
directive, including checking that $scope
properties are doing what we want them to. For example, we can check that the user input the password repeat correctly by checking that person.passwordRepeat
is the
<!-- index.html -->
...
<!-- Email Input Field -->
<div class="form-group col-md-12">
<input type="email" name="email" class="form-control" placeholder="Email" ng-model="person.email" required>
<p ng-show="validateForm.email.$invalid && validateForm.email.$touched" class="help-block">Email is required.</p>
</div>
...
<!-- Password and Password Repeat Input Fields -->
<div class="form-group col-md-12">
<input type="password" name="password" class="form-control" placeholder="Password" ng-model="person.password" ng-minlength="7" required>
<p ng-show="validateForm.password.$error.minlength && validateForm.password.$touched" class="help-block">Your password must be at least 7 characters long.</p>
</div>
<div class="form-group col-md-12">
<input type="password" name="passwordRepeat" class="form-control" placeholder="Password Repeat" ng-model="person.passwordRepeat" required>
<p ng-show="person.passwordRepeat !== person.password && validateForm.passwordRepeat.$touched" class="help-block">Passwords don't match</p>
</div>
Change the CSS of the input boxes based on their validation properties
Above we talked about how to access the properties added to the form and input objects, which are accessible by using <form-name>.<input-name>.property
. Beyond adding those properties, Angular also adds corresponding ng-
CSS classes to the HTML element itself, giving us greater flexibility in styling. So when something is $pristine
, that element also has a CSS class of ng-pristine
as well. When $pristine
becomes false, the ng-pristine
class gets removed.
This allows us to make style changes to our input boxes, since those classes get added automatically for us. So we can do something like this:
.ng-invalid.ng-touched {
border-color: red;
}
In other words, if both the invalid and the touched classes are on an element at the same time (which, remember, happens when they've blurred the input box and the stuff inside didn't satisfy our conditions), then add a red border around the input box to make it extra obvious that they have to fix it.
Conditionally adding CSS classes on your own
We don't have to rely solely on the ng-pristine
, etc. classes that Angular adds for us. Instead, we can write our own classes and add them based upon the conditions found in the form or input properties discussed above using Angular's ng-class
directive. The syntax looks like this:
ng-class="{ 'class-name' : conditional expression evaluating to true/truthy or false/falsy }"
If the conditional evaluates to true (or something truthy), it applies the class specified. Otherwise it removes it. So with our email field, we can use the same conditions we used to apply the help text and apply the built-in Bootstrap .has-error
class to the entire div surrounding the input, which will style our input box with a red border and our help text with red colored text:
<div class="form-group col-md-12" ng-class="{'has-error' : validateForm.email.$invalid && validateForm.email.$touched}">
<input type="email" name="email" class="form-control" placeholder="Email" ng-model="person.email" required>
<p ng-show="validateForm.email.$invalid && validateForm.email.$touched" class="help-block">Email is required.</p>
</div>
Awesome! Have you noticed what your JS file looks like so far? We haven't even had to touch the JS file yet because Angular has built in so many great validation features for us. We've done all of our validation in the HTML, which makes sense since the only purpose of our validation is to change what gets displayed to our user in the view based on the interaction they're having with our form. Now, someone reading through our HTML file can get all the contextual information about what should be happening from the HTML page.
Here's what our complete HTML page looks like right now:
<!DOCTYPE html>
<html lang="en" ng-app="ValidationPractice">
<head>
<meta charset="UTF-8">
<title>Signup Form</title>
<!-- Bring in Bootstrap from the CDN -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<!-- Add our own styles -->
<link rel="stylesheet" href="styles.css">
</head>
<body ng-controller="FormController" class="container">
<section class="row">
<div class="col-md-4 col-md-offset-4 well">
<form name="validateForm" novalidate> <!-- We add "novalidate" so HTML won't validate for us automatically. -->
<div class="form-group col-md-12">
<input type="text" name="firstName" class="form-control" placeholder="First Name" ng-model="person.firstName">
</div>
<div class="form-group col-md-12">
<input type="text" name="lastName" class="form-control" placeholder="Last Name" ng-model="person.lastName">
</div>
<div class="form-group col-md-12" ng-class="{'has-error' : validateForm.email.$invalid && validateForm.email.$touched}">
<input type="email" name="email" class="form-control" placeholder="Email" ng-model="person.email" required>
<p ng-show="validateForm.email.$invalid && validateForm.email.$touched" class="help-block">Email is required.</p>
</div>
<div class="form-group col-md-12">
<input type="tel" name="phone" class="form-control" placeholder="Phone Number" ng-model="person.phone">
</div>
<div class="form-group col-md-12">
<input type="password" name="password" class="form-control" placeholder="Password" ng-model="person.password" ng-minlength="7" required>
<p ng-show="validateForm.password.$error.minlength && validateForm.password.$touched" class="help-block">Your password must be at least 7 characters long.</p>
<p ng-show="validateForm.password.$touched && validateForm.password.$invalid" class="help-block">Password is required.</p>
</div>
<div class="form-group col-md-12">
<input type="password" name="passwordRepeat" class="form-control" placeholder="Password Repeat" ng-model="person.passwordRepeat" required>
<p ng-show="person.passwordRepeat !== person.password && validateForm.passwordRepeat.$touched" class="help-block">Passwords don't match</p>
</div>
<button class="btn btn-primary btn-lg center-block" ng-click="addBadge(person)" ng-disabled="validateForm.$invalid">Submit</button>
</form>
</div>
</section>
<!-- AngularJS from the CDN -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script>
<!-- Our own JS -->
<script src="app.js"></script>
</body>
</html>
Conclusion
We'll be writing a second post to continue exploring even more Angular form validation abilities, but for now this should give you an excellent head-start on doing some very useful client-side validation to your forms.