AngularJS: Forms & Validation
Introduction
The following article assumes you have a basic understanding of AngularJS. Take a look at AngularJS: The Essentials if you need to get up to speed. Ready? Let's crack on then. We are going to make a Create Your Own Pizza form to illustrate how to bind, and in some cases build different HTML form fields. We will also cover validation of these fields and of course how to handle the form submission. Let's throw in some Bootstrap too!
The entire code used here for our example form (pizza-form.html) and AngularJS module (app.js) has been shared on GitHub. Go ahead and download it now if you would like to read alongside the finished example.
The Controller
Let's start by creating a new file called app.js and defining an AngularJS module along with the beginnings of our controller.
'use strict'
var app = angular.module('myApp', [ ]);
app.controller('PizzaFormController', ['$scope', function($scope) {
// Initialise order object
$scope.order = {
customer: {},
pizza: {
toppings: [],
quantity: 1
},
total: 0,
complete: false
};
// Initialise form submitted state
$scope.formSubmitted = false;
// Initialise option arrays
$scope.pizzaSizes = [
{ name: "Small", val: "S", price: 3.99 },
{ name: "Medium", val: "M", price: 5.99 },
{ name: "Large", val: "L", price: 7.99 }
];
$scope.pizzaBases = [
{ name: "Thin Crust", val: "thinCrust", price: 0 },
{ name: "Original", val: "original", price: 0 },
{ name: "Deep Pan", val: "deepPan", price: 0 },
{ name: "Stuffed", val: "stuffed", price: 1.99 }
];
$scope.pizzaToppings = [
{ name: "Extra Cheese", val: "extraCheese", price: 0.50 },
{ name: "Bacon", val: "bacon", price: 0.50 },
{ name: "Beef", val: "beef", price: 0.99 },
{ name: "Chicken", val: "chicken", price: 0.99 },
{ name: "Jalapeños", val: "jalapeños", price: 0.50 },
{ name: "Mixed Peppers", val: "mixedPeppers", price: 0.50 },
{ name: "Mushrooms", val: "mushrooms", price: 0.50 },
{ name: "Olives", val: "olives", price: 0.50 },
{ name: "Pineapple", val: "pineapple", price: 0.50 },
{ name: "Sweetcorn", val: "sweetcorn", price: 0.50 },
{ name: "Tuna", val: "tuna", price: 0.99 }
];
// Various methods. We'll get to these later.
}]);
Firstly we initialise the base $scope.order
object which will store our customer details, pizza selections, order total and the status of the order. This is followed by initialising the form state with $scope.formSubmitted
. The three items after this are arrays of objects which will be used to build our pizza options in the form.
The Form
Next we create our form page pizza-form.html and pull in the required files, setup a little CSS (to add an asterisk to required fields), and bind our controller to a <div>
wrapper.
<!DOCTYPE html>
<html ng-app="myApp" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="language" content="en" />
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" />
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js"></script>
<script type="text/javascript" src="app.js"></script>
<style>
.form-group.required .control-label:after {
content:"*";
color:red;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12" ng-controller="PizzaFormController">
<h1>Welcome to the Pizza Palace</h1>
...
</div>
</div>
</div>
</body>
</html>
Let's add the form element just below our main heading.
<form novalidate class="col-xs-6" name="pizzaForm" ng-submit="submit()" ng-hide="order.complete">
...
</form>
Several things to point out here. Since HTML5, modern browsers can perform some form validation without the need for any one of a host of JavaScript plugins and libraries created over the years. However, if you need any type of validation not covered by HTML5 (or want better coverage than just modern browsers) and want to have one consistent look and feel to how you handle all validation, then we need to tell the browser to back off! Say hi to the novalidate
attribute which does exactly this. The name
attribute might seem trivial but note we will need this for validation later so don't skip it.
Next up are a couple of AngularJS directives. The first, ng-submit
will be covered later, but in short it hijacks the form submission and passes it to the given function. The latter, ng-hide
tells AngularJS to hide the whole form if the order status is complete. Remember we intialised this to false
in our controller earlier.
Customer Details
Now the stage is set, let's get to adding some form fields.
<div class="form-group required">
<h2>Your Details</h2>
<label for="firstName" class="control-label">First Name</label>
<input type="text" name="firstName" class="form-control input-sm" ng-model="order.customer.firstName" required />
<div class="alert alert-danger" ng-show="( formSubmitted || pizzaForm.firstName.$dirty ) && pizzaForm.firstName.$invalid">
<small ng-show="pizzaForm.firstName.$error.required">
Please enter your first name.
</small>
</div>
...
</div>
Scattered throughout, you will see lots of Bootstrap CSS classes. One to note is the wrapper <div class="form-group required">
. These classes relate to the CCS we added in the head of the page. This will append an asterisk to the text of any elements also tagged with the form-control
class.
We next need to bind, using ng-model
, our input field to a model within our controller order.customer.firstName
. Also note the HTML5 required
attribute denoting a mandatory field. This would normally be used by the browser's built-in validation but will also be used by our AngularJS validation. The next block contains an error message that is conditionally shown when the ng-show
directive's expression evaluates to true
. To understand the expression, we will later delve deeper into AngularJS validation. The above also includes the customer's last name which is more or less the same as first name so let's move on.
Pizza Size
The next section of the form is where we actually build our pizza with the use of several other HTML elements. This is where our option arrays from the AngularJS controller come into play. We could have just created static HTML elements, but the option arrays of objects allow us more control and the abiltity to store more information for each option such as price (which will come in handy when we calculate the order total later).
<h2>Build Your Own Pizza</h2>
<p>Please select from the options below:</p>
<div class="form-group required">
<h3 class="control-label">Size</h3>
<div class="radio" ng-repeat="pizzaSize in pizzaSizes">
<label>
<input type="radio" name="size" class="input-md" required ng-model="order.pizza.size" ng-change="updateOrderTotal()" value="{{ pizzaSize.val }}" /> {{ pizzaSize.name }} (£{{ pizzaSize.price }})
</label>
</div>
<div class="alert alert-danger" ng-show="( formSubmitted || pizzaForm.size.$dirty ) && pizzaForm.size.$invalid">
<small ng-show="pizzaForm.size.$error.required">
Please pick which size pizza you would like.
</small>
</div>
</div>
This time we are using ng-repeat
to iterate over pizzaSizes
. Note that the directive has been placed on the wrapper <div>
so that we get the whole block including <label>
and <input type"radio" />
repeated for each pizza size option. If you need a reminder of ng-repeat
and how it works, skip over to AngularJS: The Essentials.
The eagle eyed amongst you may have noticed the use of the ng-change
directive on the input. Again we will cover this later but in essence the directive calls the specified function immediately on change of the value. I say immediately to make it clear that the directive does not wait until focus has moved from the field. If this were an <input type"text" />
, the function would be called after every keystroke.
Pizza Base
When it comes to HTML <select>
elements we could use ng-repeat
again to create each <option>
element. However, AngularJS provides a directive especially made for this purpose, ng-options
.
<div class="form-group required">
<h3 class="control-label">Base</h3>
<select name="base" ng-model="order.pizza.base" ng-options="pizzaBase.val as pizzaBase.name + ' +(£' + pizzaBase.price + ')' for pizzaBase in pizzaBases" ng-change="updateOrderTotal()" required>
<option value="">Please select a base</option>
</select>
<div class="alert alert-danger" ng-show="( formSubmitted || pizzaForm.base.$dirty ) && pizzaForm.base.$invalid">
<small ng-show="pizzaForm.base.$error.required">
Please select which pizza base you would like.
</small>
</div>
</div>
There a quite a few different comprehension expressions for this directive depending on whether we are iterating over arrays or object properties. The one used here is select as label for value in array
. The first two of these represent the option's value and the text to be displayed, respectively. In our case that is pizzaBase.val
and the expression pizzaBase.name + ' +(£' + pizzaBase.price + ')'
which concatenates the pizza base name with the price. We also see the ng-change
directive, again used to update the order total.
Note we have also added a default option with an empty value as we do not want to default to any of the existing options.
Pizza Toppings
Next up we have the pizza toppings which are all optional and so represented by checkboxes. Checkboxes can be approached several different ways depending on how you wish to use them. If we had a straight forward bunch of options to pick from and each was represented by a different variable or property in the scope, then we could just use ng-model
to bind each of them. In our case however we want to capture just the ordered items in an array of pizza toppings (we aren't interested in what our customers don't want on their pizza).
<div class="form-group">
<h3>Toppings</h3>
<div class="checkbox" ng-repeat="pizzaTopping in pizzaToppings">
<label class="control-label">
<input type="checkbox" name="topping" ng-click="updateToppings( pizzaTopping.val )" value="{{ pizzaTopping.val }}" /> {{ pizzaTopping.name }} ({{ pizzaTopping.price | currency : "£" }})
</label>
</div>
</div>
As we are capturing our toppings in an array, we need to add (or remove) toppings when they are clicked. Therefore we use the ng-click
directive to call our updateToppings()
funtion in the controller.
/*
* On click of a pizza topping checkbox, remove the topping
* if already added, otherwise add it. Then update the order total.
*
* @method updateToppings
* @param {String} pizzaTopping The selected pizza topping.
*/
$scope.updateToppings = function ( pizzaTopping ) {
var addTopping = true;
if( $scope.order.pizza.toppings.length > 0 ) {
for( var i=0; i < $scope.order.pizza.toppings.length; i++ ) {
if( pizzaTopping === $scope.order.pizza.toppings[ i ] ) {
$scope.order.pizza.toppings.splice( i, 1 );
addTopping = false;
}
}
}
if( addTopping === true ) {
$scope.order.pizza.toppings.push( pizzaTopping );
}
$scope.updateOrderTotal();
};
This function will remove a topping from the array if it already exists (clearing a checkbox). Otherwise it will be added to the array.
Pizza Quantity
This would probably have been better suited to a <input type="number" />
but I have used a <input type="text" />
to give me an excuse to use ng-pattern
. All revealed later.
<div class="form-group required">
<h3 class="control-label">Quantity</h3>
<input type="text" name="quantity" ng-model="order.pizza.quantity" ng-pattern="/^[1-9]{1}[0-9]*$/" ng-change="updateOrderTotal()" required />
<div class="alert alert-danger" ng-show="( formSubmitted || pizzaForm.quantity.$dirty ) && pizzaForm.quantity.$invalid">
<small ng-show="pizzaForm.quantity.$error.required">
Please enter how many pizzas you would like.
</small>
<small ng-show="pizzaForm.quantity.$error.pattern">
Please enter a valid number of pizzas you would like.
</small>
</div>
</div>
That only leaves the submit button <input type="submit" value="Let's Cook!" />
which is nothing unusual. Although remember we put our ng-submit
directive up in the <form>
element to hijack the submission.
Order Total
As mentioned several times above, we are constantly updating the order total when either a value changes on a text, radio or select type input (ng-change
) or a checkbox is clicked (ng-click
). All of these call the same function in the controller.
/*
* Recalculate the order total from scratch for all ordered options.
*
* @method updateOrderTotal
*/
$scope.updateOrderTotal = function () {
$scope.order.total = 0;
if( $scope.order.pizza.hasOwnProperty( 'size' ) ) {
$scope.order.total += $scope.lookUpOption( $scope.pizzaSizes, $scope.order.pizza.size, 'price' );
}
if( $scope.order.pizza.hasOwnProperty( 'base' ) ) {
$scope.order.total += $scope.lookUpOption( $scope.pizzaBases, $scope.order.pizza.base, 'price' );
}
if( $scope.order.pizza.toppings.length > 0 ) {
angular.forEach( $scope.order.pizza.toppings, function( orderedTopping ) {
$scope.order.total += $scope.lookUpOption( $scope.pizzaToppings, orderedTopping, 'price' );
});
}
$scope.order.total *= $scope.order.pizza.quantity;
};
This function recalculates the total by checking each option type to firstly see if anything has been ordered and if it has, looks up the price in the respective option array (see helper function below). The price is then added to the total which is presented at the bottom of the form.
/*
* Helper function to retrieve an option property from a provided option array.
*
* @method lookUpOption
* @param {Array} itemList The option array to be searched.
* @param {String} optionVal The option value to be looked up.
* @param {String} requiredVal The option value to be retrieved.
* @return The retrieved value found in the option array.
*/
$scope.lookUpOption = function ( itemList, optionVal, requiredVal ) {
var foundVal;
angular.forEach( itemList, function( item ) {
if( optionVal === item.val ) {
foundVal = item[ requiredVal ];
}
});
return foundVal;
};
The Validation
AngularJS provides several object validation properties that are automatically added to both our form and all the inputs. Each property also has an associated CSS class (shown in braces after each property below) so we can change style according to the current validation status of the form or inputs.
$pristine
{ng-pristine
} - True if the item has not yet been used (excludes focus).$dirty
{ng-dirty
} - True if the item has been used (excludes focus).$valid
{ng-valid
} - True if the item has satisfied all validation rules/criteria set.$invalid
{ng-invalid
} - True if the item has failed any validation rules/criteria set.
To reference the form and any inputs within, we use the form's name
attribute. So for example we can check if the whole of our form is valid (i.e. all inputs within are valid) by checking if pizzaForm.$valid
is true
. Or we could check if the customer's first name had failed validation by checking if pizzaForm.firtName.$invalid
is true
.
But how do we create rules? Each rule is declared as either a HTML attribute or an AngularJS directive within the field element that is to be tested. Let's look at the most commonly used types of validation rules that can be set for each field type:
- input[text] -
required
,ng-required
,ng-minlength
,ng-maxlength
,pattern
,ng-pattern
. - input[number] - Same as input[text] with the addition of
min
andmax
. - textarea - Same as input[text] without
pattern
althoughng-pattern
still applies.
Let's take a closer look at ng-pattern
as the rest are pretty self explanatory. The value of this directive can be either a regular expression string or a JavaScript RegExp object. In our pizza quantity above we used ng-pattern="/^[1-9]{1}[0-9]*$/"
which restricts the input to one or more numbers only and the first number must be between 1 and 9. This is a very basic example but hopefully you get the idea that this could be an extremely powerful custom validation tool. You can find more details on all of these in the docs.
Failing one or more of these rules will firstly add the $invalid
property to both the form and the failing field. AngularJS will also add an $error
property to the field along with the error type i.e. pizzaForm.firstName.$error.required
with a value of true
if no value was entered.
In our pizza quantity example above we want to show an error block only if the field is invalid (pizzaForm.quantity.$invalid
) and either the form has been submitted already (formSubmitted
) or the field has been used (pizzaForm.quantity.$dirty
). If a field is required
for example then it would fail validation immediately and show the error block which wouldn't be nice at all ("I just got here and they are already telling me I have done something wrong!"). We only want to show our error block when either someone has entered an invalid value or has tried to submit the form without entering anything.
Within the error block we also have the specific validation checks and their respective error messages which are of course only shown if the error being checked is true
:
<small ng-show="pizzaForm.quantity.$error.required">
Please enter how many pizzas you would like.
</small>
The Submission
Finally, we look at the submission of the form. Waaaaay back near the top of this article we saw that the <form>
element contained ng-submit="submit()"
which hijacks the normal http form submission and calls the enclosed function.
/*
* Form submit
*/
$scope.submit = function () {
$scope.formSubmitted = true;
if( $scope.pizzaForm.$valid ) {
$scope.order.complete = true;
}
};
Nothing ground breaking here. We state that the form has now been submitted which is then used by our validation. We also check if the whole form is valid, and if so we set the order to complete. Again, waaaaaay back up top we also added ng-hide="order.complete"
to the <form>
element. So on a successful submission of an order, the whole form will be hidden. In its place we'll show a confirmation (which we add just below the form).
<div ng-if="order.complete">
<h2>Order Complete</h2>
<p>Thank you {{ order.customer.firstName }} {{ order.customer.lastName }} for your order. Your <ng-pluralize count="order.pizza.quantity" when="{'1': 'pizza is', 'other': 'pizzas are'}"></ng-pluralize> in the oven!</p>
<h3>You ordered:</h3>
<p>{{ order.pizza.quantity }} x {{ lookUpOption( pizzaSizes, order.pizza.size, 'name' ) }} {{ lookUpOption( pizzaBases, order.pizza.base, 'name' ) }} <ng-pluralize count="order.pizza.quantity" when="{'1': 'Pizza', 'other': 'Pizzas'}"></ng-pluralize> with
<span ng-if="order.pizza.toppings.length === 0"> no extra toppings.</span>
<span ng-if="order.pizza.toppings.length > 0" ng-repeat="pizzaTopping in order.pizza.toppings">{{ $index !== 0 ? ", " : ""}}{{ $index === order.pizza.toppings.length - 1 ? "and " : ""}}{{ lookUpOption( pizzaToppings, pizzaTopping, 'name' ) }}</span>.
</p>
<h3>Enjoy! Come again soon.</h3>
</div>
The Wrap Up
Well done if you made it this far! I appreciate we haven't looked at what should be done with the submitted order (other than display a confirmation). We could email it, trigger an alert on another system or maybe within an admin interface of the same application. We will be looking at Services in a later article which will help us to link our applications to APIs.
Remember the complete code used here can be downloaded from GitHub so if you haven't already, go help yourself.