Monday, 23 June 2014

AngularJS - Developing a Sample Application (with a RESTful service)

Introduction

AngularJS is a MVC framework for building dynamic web apps. This means your web application is comprised of Models, Views and Controllers. Models are typically javascript objects storing your application data, Views are html pages providing a visual representation of said data, and Controllers are javascript functions that handle data (i.e. retrieving from server; make it available to the view) and respond to events. AngularJS offers a number of benefits including but not limited to:

  • Easy to get started within minutes. You can knock up a simple AngularJS application very quickly by executing a small number of steps.
  • 2 way data-binding. The Model and View are kept in sync without having to write wrappers, getters/setters. This is done automatically so you don't have to do anything.
  • Dependency Injection. The Angular injector module is responsible for creating components, resolving their dependencies and providing them to other components when requested.
  • Extends html through the use of directives. This offers a lot of power by triggering new behaviour based on newly created syntax that you write.
  • Simplifies calls to the server via REST. In a single line you can invoke a call to the server.
  • Easy to test.


To better acquaint myself with angular, I built a very simple 4 page application, flights 'R' us, which enables a user to book a return flight to various destinations in Europe. 

Figure 1: Form to specify Customer requirements

Figure 1 shows the first page. It presents a form to capture basic information such as destination and departure locations, dates for the flights and number of passengers. Note that the drop downs for Destination and Departure are chained, so that selecting a specific destination will ensure that the Departure drop down is populated with valid locations. Hitting the 'Submit' button causes the form to be submitted and takes the user to page 2, shown below.


Figure 2: List of possible flights

Page 2 shows a list of possible flights to the required destination from the specified departure airport. The list is generated by querying a RESTful service when the form on page 1 is submitted. The user makes a selection by hitting the 'Select' button in the appropriate row. This leads to the next page as shown in figure 3.




Figure 3: Flight Summary, Personal & Payment details form


Figure 3 shows summary details for the selected flight and presents a form to capture personal and payment details.


Figure 4: Order Confirmation Page


Figure 4 shows the final page, once the user has submitted the form on page 3 by hitting the 'Book Now' button.

The following sections will provide a discussion on how this application was built using AngularJS. This will not provide a step-by-step account on how to build the application, but rather an autopsy to highlight key elements of an AngularJS application.

Developing a Sample Application

The sample application, flights'R'us was built as 2 projects, one to provide the UI and the other a RESTful service. The UI project was developed using AngularJS (1.3.0) and Bootstrap (3.1.1) to make it more visually appealing. The RESTful service was developed using RESTeasy 3.0.5, java 1.6 and was deployed under JBOSS AS 7.3. The source code for both are available here. The discussion in this post focusses mainly on the AngularJS application and does not rely much on knowing REST.

As part of the learning experience of building an AngularJS application from scratch, I managed to identify key points which I felt are important in developing such an application. This list is by no means complete but should provide enough understanding to help you (hopefully) with your projects. The key steps are detailed in the following sections.

Bootstrapping

One of the most attractive features of AngularJS, is just how quickly you can knock up a sample application with very little effort. The flights'R'us application comprises of a single javascript file (app.js) and a number of html files, the main one being FlightsRus.html. This file defines the main layout for the application and looks something like this,

//FlightsRus.html
< html ng-app="myApp">

  
  
< /head>

< body>
   
   

Flights 'R' Us

Number 1 in Flight booking
< /body> < /html>

The main things to note here are,
  1. The use of 'ng-app' directive. This identifies the application as being an AngularJS application and makes available all resources and services provided by AngularJS. 
  2. The declaration to import the AngularJS library (angular.min.js) and a javascript file (app.js) containing all our code.
  3. The use of 'ng-view' directive. This is used to inject html templates at this location. We can use this in conjunction with the ng-routes module (more on this later) to replicate a multi-page application.
The ng-app directive mentioned above specifies a value of 'myApp'. This is a reference to the root module of our application. A module is effectively a container for the different components of our application such as controllers, services, directives etc. It offers many benefits, including,
  • providing a declarative way to bootstrap an application. Typically most applications have a main method that is responsible for wiring up all dependencies. With AngularJS you have more flexibility.
  • a way to package code as reusable units.
  • ability to load modules in any order.
  • faster testing since tests only need to load relevant modules.
The 'myApp' module is declared in the app.js file and is defined as follows

var app = angular.module('myApp', []);

The module constructor can take 3 arguments. In this instance we supply 2 arguments, the name of the module ('myApp'), and a list of modules upon which the myApp module depends on. This has been left empty for now but will be populated with relevant entries as we explain various aspects of the application in later sections.

One thing to note about the module constructor (from the docs):
When passed two or more arguments, a new module is created. If passed only one argument, an existing module (the name passed as the first argument to module) is retrieved.

DataBinding - Introducing $scope

A common problem with web applications involves binding data captured in html forms to domain objects. With many frameworks you have to write extra code to handle this. Fortunately AngularJS offers a simple but effective mechanism, the $scope object,  for binding data objects to form fields. To help explain this, I'm going to focus on the text field to capture the number of Adult passengers in the form shown in figure 1.

The View is defined as,
      

This looks like a typical HTML form except that the input field has an extra attribute, ng-model which is assigned a value of "formDetails.numAdults". AngularJS will ensure that any text entered in to the input field is stored in the numAdults property of the object literal formDetails. This data object is defined within the controller managing this view, which is defined as,

app.controller('FlightsFormController', ['$scope', function($scope) {
   console.log('Form Details # adults: ' + $scope.formDetails.numAdults);
}

The .controller() method configures a controller for the module. It takes a number of arguments, the first of which specifies the name of the controller. The following array of values include a list of services to be injected into the controller ($scope in the present case) and a function defining the implementation of the controller. $scope is an object that refers to the application model and enables objects and functions to be shared between the controller and view. In this instance, I want to share the formDetails object literal (and all it's properties) between my Controller and View. For now I'm not doing anything with the numAdults field other than printing it's value to the console log.

Calling the Server - $http

Staying with figure 1, notice the 2 drop down boxes for Destination and Departure. As expected, these are populated with Destination and Departure values. There are 2 points to be made here. Firstly the app is data driven so these values are retrieved from a server. Secondly they form a chained select control, so that making a selection on the Destination drop down, will cause the values on the Departure drop down to change accordingly.

To populate the drop downs we're going to have to make a call to a RESTful service. Again with AngularJS, this is so easy. Looking inside the FlightsFormController definition, we find

   
$http.get('http://localhost:8080/RESTflightsRus/flightsRusService/allroutes').
  success(function(data) {
    $scope.destinations = data;
    console.log('allRoutes: ' + data);
 });

This statement uses the $http service provided by AngularJS to invoke a HTTP GET request to my dummy RESTful service (deployed at the specified URL). It provides 2 callback functions, success() and error(). Only the former is shown. These are invoked when the Server returns a response. In the event everything is ok, the success() function is executed and conversely if something goes wrong, the error() function will be invoked. In this instance, if the server successfully returns our data, we assign the results to the destinations property which is attached to the $scope object and therefore available to our views. The data is in json format and looks something like:


[
  { "name" : "Majorca",
    "departures" : [{"name" : "London"}] 
  },
  {
     "name" : "Algarve",
     "departures" : [{"name" : "Newcastle"}]
  },
  {
     "name" : "Costa Del Sol",
    "departures" : [{"name" : "Birmingham"}]
  },
  {
     "name" : "Barcelona",
    "departures" : [{"name" : "East Midlands"},{"name" : "Bristol"}]
  },
  {
     "name" : "Tenerife",
    "departures" : [{"name" : "London"},{"name" : "Newcastle"},{"name":"East Midlands"}]  }
]

To access the $http service, we need to get AngularJS to inject it into the controller. This is done by modifying the controller declaration as follows:

app.controller('FlightsFormController', ['$scope', '$http', function($scope, $http) {

It's clear from this example, that Angular is also very efficient since it injects only those services that we require in the current context rather than bloating our code with a whole bunch of unnecessary dependencies.

To populate the Destination drop down in the view, we implement the select control in the following way
 


In this example, the select control displays a list of options dynamically generated by the ng-options attribute which contains the statement:
c.name for c in destinations

Recall that destinations was previously retrieved from our RESTful service as discussed above and subsequently attached to the $scope parameter thus making it available to the view. The expression involves iterating through the destinations array and extracting the name property for each entry. The resulting collection is then displayed in the drop-down. When the user selects a value, this will be stored in the property formDetails.destination which is also attached to the $scope object.

In order to populate the second dropdown (Departures) based on the selection of the Destination drop down we do the following:
 


In this instance the ng-options expression is
p.name for p in formDetails.destination.departures
The expression takes the value selected by the user in the first drop-down (destination) and loops through it's corresponding set of departure values. For each entry it simply extracts the name of the departure location. The resulting collection of departure values are then displayed in the drop down. The user's selection is stored in the variable, formDetails.departure.

One final point before we move onto the remaining pages of the application. The date fields, fromDate and toDate will render a popup calendar if the application is viewed in Chrome, but fail to materialise in FF. This is because FF does not support the input fields of type 'date', hence it reverts to behaving like a regular text field.

Multiple Pages - ngRoute

Although AngularJS is often referred to as a Single Page Application (SPA), it is still possible to develop a multi page application. This is done using the <ng-view> directive mentioned above and the ngRoute service provided by AngularJS.

Before we can use the ngRoute module, we need to declare the dependency in the declaration for the module,

var app = angular.module('myApp', ['ngRoute']);

The ngRoute module makes available a service object, $routeProvider which enables us to wire together controllers and views and map them to the URL location. Inspecting the app.js file, you'll find the following lines of code,

//configure routing
app.config(function($routeProvider) {
 $routeProvider.when("/",
   {
    templateUrl: 'FlightForm.html',
    controller: 'FlightsFormController'
   }
 )
 .when("/flightOptions",
   {
    templateUrl: 'FlightOptionsList.html',
    controller: 'flightOptionsController'
   }
 ) 
 .when("/order",
   {
    templateUrl: 'FlightOrder.html',
    controller: 'flightOrderController'
   }
 ) 
 .when("/confirmation",
   {
    templateUrl: 'OrderConfirmation.html',
    controller: 'OrderConfirmationController'
   }
 ); 

});


This snippet shows a chained set of when() methods being invoked on the $routeProvider object. Each when() method links the URL to it's corresponding pair of View template and Controller.

To force a page change, we use the $location service and invoke it's path(URL) method, i.e.
$location.path('/flightOptions');

When the .path(URL) method is invoked, the $routeProvider uses the URL to work out which pair of controller & view components need to be deployed. The template is then rendered inside the main layout (FlightsRus.html) file using the <ng-view> directive mentioned previously. In this way each time the URL changes, the included view changes with it based on the configuration of the $routeProvider object.

The $location service is accessed by getting AngularJS to inject it into the Controller. This is achieved by modifying the controller declaration to:

app.controller('FlightsFormController', ['$scope', '$http',  '$location', function($scope, $http, $location) {


Sharing Data between controllers - .factory()

A common problem in web application development is sharing data across widgets and pages. AngularJS offers a very simple but effective mechanism to achieve this in the form of Services. A Service is an encapsulation of methods that relate to a specific function. In our case, we want our service to cache data between pages.

The simplest way to create a service is to use the module.factory() method. Let's illustrate this with some code. At the top of the app.js file, you will find the following declaration:

//configure service for passing data between controllers
app.factory('dataService', function() {

 var flightOptions = [];

 var addFlights = function(newObj) {
  flightOptions = newObj;

 };

 var getFlights= function() {
  return flightOptions;
 };

 return {
  addFlights : addFlights,
  getFlights : getFlights,
 };
});


Here we present a simplified version of the code. We pass 2 arguments into the factory method, the first being the name of the service followed by it's implementation. In this instance our service declares an array literal and exposes methods to add/retrieve entries from that literal. Once this service is available, we can push and pull data from this common data store at different points in our application. By defining the Service in this way, we have registered it with the AngularJS compiler so that it can reference it and load it as a dependency at runtime.

To demonstrate how this service is employed, we focus on the switch between pages one (figure 1) and two (figure 2) of the application. On page one, the user fills in a form specifying trip details. On hitting the submit button, the managing controller (FlightsFormController) calls the RESTful service for a list of possible flights as shown below.

//define controller for main form
app.controller('FlightsFormController', ['$scope', '$http', 'dataService', '$location', function($scope, $http, dataService, $location) {
 .................
 .................

     $scope.submit = function() {

     $http.get("http://localhost:8080/RESTflightsRus/flightsRusService/flightoptions/?" + "departure=" + $scope.formDetails.departure.name +
                "&destination="+$scope.formDetails.destination.name+
                "&fromDate="   +$scope.formDetails.fromDate+
                "&toDate="     +$scope.formDetails.toDate+     
                "&numAdults="     +$scope.formDetails.numAdults+     
                "&numChildren="     +$scope.formDetails.numChildren).     
                success(function(data) {
                     dataService.addFlights(data);
                     $location.path('/flightOptions');
              });

 .................
 .................

 };
}

The RESTful service replies by returning a list of all possible flights. At this point, we want to cache the results of the call to the RESTful service. This is achieved by injecting the 'dataService' object into the controller as seen in line 2. The data is then added to the cache by invoking the addFlights() method on 'dataService' (line 15). The form submission process completes by navigating to page 2 (/flightOptions) where the list of all possible flights is to be displayed.

Page two is managed by the 'flightOptionsController' as declared below.

//define controller for flightOptions
app.controller('flightOptionsController', ['$scope', 'dataService', '$location', function ($scope, dataService, $location) {

 $scope.dataService = dataService;

 $scope.flightOptions = dataService.getFlights();

 $scope.selectedFlight = function(flightDetails) {
  dataService.setFlightDetails(flightDetails);
  $location.path('/order');
 };

}]);


As before, we have to tell AngularJS to inject in our 'dataService'. Then within the body of declaration, we simply invoke the getter method on 'dataService' to retrieve the cached data and set this in $scope variable, enabling the View to access the data.

The View subsequently displays this data using the <ng-repeat> directive as shown below,
 
  <tr ng-repeat='flightOption in flightOptions'>
      <td> {{flightOption.flightNumber}} 
       ...........................................
       ...........................................
  </tr>    



Validation

Another key requirement for any form based web application is to provide client side validation. Fortunately with AngularJS, it is possible to create forms that are interactive and immediately responsive leading to enhanced usability.

The implementation of any form begins with the <form> declaration:
 

The tag has 2 attributes, name and novalidate. The latter is required to disable the browser's native form validation. The former in conjunction with the name attribute on form elements is required to enable AngularJS to correctly bind validation messages to their parent form fields.

AngularJS offers 2 levels of validation, out-of-the-box and custom. In the first case, an input element can be configured to validate a number of things,

  
    

The attributes ng-minlength, and ng-maxlength specify the minimum and maximum lengths respectively of any value entered into this field. If the user enters a value that does not meet these criteria they will be prompted with an appropriate validation message. The ng-pattern attribute forces the input field to raise a validation error if the entered value does not match the REGEX expression provided. Lastly the required field indicates to AngularJS that this is a mandatory field and requires a value. Note that this input field is specified as having a type 'text'. It is possible to define other types such as date and email. In the latter case, AngularJS will perform the check that the entered value resembles the format expected from an email address and will display a validation message if it is not.

Before we go any further, it's helpful to point out that input fields in AngularJS can occupy any one of the following states based on user interaction:
  • $valid : The value entered into the input field is valid
  • $invalid : The value entered into the input field is not valid
  • $pristine: no value has been entered into the input field.
  • $dirty : Indicates if the user has interacted with the input field
  • $touched : The user has focussed on the input element and then shifted focus away from the element.
Note that each state is represented by a boolean value.

The next step is to configure how the validation messages will be displayed and this has been facilitated by the introduction of the ng-messages module in the latest release of Angular. To use this package, we have to identify it as a dependency module by adding another entry to the module declaration like so,

var app = angular.module('myApp', ['ngRoute', 'ngMessages']);

Following this, we have to attach the validation messages to each input field. To illustrate this, we'll focus on the email field (emailInput),

 

* required
invalid email

The main points to note from this snippet is that the input field has an ancestor div that can be styled to highlight validation problems (e.g. adding a red border around the element)  and a sibling div which holds the validation messages. Let's discuss these in turn, beginning with the ancestor div.

The outermost div has an attribute ng-class, which allows us to dynamically apply a style based on the state of the input field. In this instance we apply the class 'has-error' (a bootstrap style) if the statement

personalDetailsForm.emailInput.$invalid && personalDetailsForm.emailInput.$dirty

evaluates to true. Here we are testing two conditions. The first of these checks whether the value entered into the input field has rendered it in an invalid state. The second conditional tests if the email Input field has been modified in anyway; this ensures that a validation message is not displayed until after the User has interacted with it. The end result is that the User is not presented with misleading validation messages even before they've had a chance to fill in the form.

Now let's move on to discussing the sibling div containing the validation messages. This div has an attribute ng-messages which takes the value

personalDetailsForm.emailInput.$error.

The value is formed by appending the names of the form (personalDetailsForm) and input field (emailInput) to the state of the field ($error). AngularJS uses the value assigned to the ng-messages attribute to correctly link Validation messages with their parent field(s) when a validation error occurs.

This div contains a number of other divs, each of which relates to an individual validation error. The child divs have content which forms the validation message and an ng-message attribute that identifies the type of validation error.

 For example, in our case we have 2 possible error states,
  1. When the user fails to provide an email (required). In this case AngularJS will display the message '* required'. 
  2. User provides an email that does not conform to the expected format (email). If this error state is triggered, AngularJS will display the message 'invalid email'.
If the set of validation checks listed is not sufficient, it is always possible to roll your own Validation checks using the directive feature of AngularJS.

Building a custom validator is reasonably straightforward. The first step is to create a directive. This is essentially an AngularJS construct that allows us to extend html. The following shows the custom validator built for the mobile phone field. The restriction on this field is an imaginary one and revolves around constraining phone numbers to only those with a prefix of '0781'.  Note that even though the validation performed by the custom validator is simple and could have been implemented using the ng-pattern attribute, we use it here simply to demonstrate the power of directives.

//create directive to perform custom validation
var MOBILE_REGEXP = /^0781/;
app.directive('mobile', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function(inputValue) {
        if (MOBILE_REGEXP.test(inputValue)) {
          // it is valid
          ctrl.$setValidity('mobile', true);
          return inputValue;
        } else {
          // it is invalid
          ctrl.$setValidity('mobile', false);
          return undefined;
        }
      });
    }
  };
});

In this case we are defining a directive, 'mobile' which will be used as an attribute on the input field. This directive performs a test on the value entered into the input field (inputValue) against the regex expression defined by MOBILE_REGEXP. The outcome of the test, whether successful or not is used to set the validity of the field (via  $setValidity()) and to notify the form of this.

To use the new validator, simply add it as an attribute to the relevant input field,
 


The corresponding validation message can be declared in the following way.
 
Needs to start with 0781

On a final note, AngularJS offers a way to disable form submission until all validation issues have been resolved by the user. This is achieved using the ng-disabled attribute as show below
  

The ng-click attribute will ensure that AngularJS invokes the submit() method defined in the parent controller when the button is clicked. The ng-disabled attribute will disable the button if the form with the name personalDetailsForm is rendered in an invalid state. Once the user has corrected any erroneous entries, the form will be in a valid state and the button will resume an active state.

This completes the discussion on building a sample AngularJS application.

Conclusion

In this post, I've covered some salient points around developing a sample application using AngularJS. There are many other topics I could have included such as filters, project structure, etc. but lack of time prevents this. My initial impressions of AngularJS are favourable. Some of the key-points are

  • Databinding. I love not having to write extra code to ensure the data is sync'ed between View and Controller
  • Dependency Injection. It's great to be able to inject your dependencies in a declarative manner.
  • Access to services such as $http. This removes the need to roll your own.
  • The basic core features and functionality make it so easy and such a pleasure to work with.
  • Everything seems so intuitive.

Any comments relating to corrections, omissions, etc are welcome.

No comments:

Post a Comment