Cohesion is one of the most important and perhaps overlooked concepts of the object-oriented programming paradigm. It refers to the responsibility of each part of the software. No matter which component we talk about, every time it implements behavior different from its responsibilities, cohesion is degraded.
Low cohesion applications contain plenty of duplicated code and are hard to unit test because it is difficult to isolate the behavior, which is usually hidden inside the component. It also reduces the reuse opportunities, demanding much more effort to implement the same thing several times. In the long term, the productivity decreases while the maintenance costs are raised.
With AngularJS, we are able to create services, isolating the business logic of every component of our application. Also, we can use the framework's dependency injection mechanism to easily supply any component with a desired dependency. The framework also comes with a bunch of built-in services, which are very useful in daily development.
In this Lesson, we'll be covering the following topics:
In order to create testable and well-designed applications, we need to take care about the way their components are related to each other. This relationship, which is very famous in the object-oriented world, is known as coupling, and indicates the level of dependency between the components.
We need to be careful about using the operator new
inside a component. It reduces the chances of replacing the dependency, making it difficult for us to test it.
Fortunately, AngularJS is powered by a dependency injection mechanism that manages the life cycle of each component. This mechanism is responsible for creating and distributing the components within the application.
The easiest way to obtain a dependency inside a component is by just declaring it as a parameter. The framework's dependency injection mechanism ensures that it will be injected properly. In the following code, there is a controller with two injected parameters, $scope
and $filter
:
controllers.js parking.controller("parkingCtrl", function ($scope, $filter) { $scope.appTitle = $filter("uppercase")("[Packt] Parking"); });
Unfortunately, this approach will not work properly after the code is minified and obfuscated, which is very common these days. The main purpose of this kind of algorithm is to reduce the amount of code by removing whitespaces, comments, and newline characters, and also renaming local variables.
The following code is an example of our previous code after it is minified and obfuscated:
controllers.min.js x.controller("parkingCtrl",function(a,b){a.appTitle=b("uppercase")("[Packt] Parking");});
The $scope
and $filter
parameters were renamed arbitrarily. In this case, the framework will throw the following error, indicating that the required service provider could not be found:
Error: [$injector:unpr] Unknown provider: aProvider <- a
Because of this, the most recommended way to use the dependency injection mechanism, despite verbosity, is through the inline array annotation, as follows:
parking.controller("parkingCtrl", ["$scope", "$filter", function ($scope, $filter) { $scope.appTitle = $filter("uppercase")("[Packt] Parking"); }]);
This way, no matter what the name of each parameter is, the correct dependency will be injected, resisting the most common algorithms that minify and obfuscate the code.
The dependencies can also be injected in the same way inside directives, filters, and services.
In the following sections, we are going to use these concepts in greater detail while using and creating services.
In AngularJS, a service is a singleton object that has its life cycle controlled by the framework. It can be used by any other component such as controllers, directives, filters, and even other services.
Now, it's time to evolve our application, introducing new features in order to calculate the parking time and also the price.
To keep high levels of cohesion inside each component, we must take care of what kind of behavior is implemented in the controller. This kind of feature could be the responsibility of a service that can be shared across the entire application and also tested separately.
In the following code, the controller is delegating a specific behavior to the service, creating a place to evolve the business rules in the future:
controllers.js parking.controller("parkingCtrl", function ($scope, parkingService) { $scope.appTitle = "[Packt] Parking"; $scope.cars = []; $scope.colors = ["White", "Black", "Blue", "Red", "Silver"]; $scope.park = function (car) { car.entrance = new Date(); $scope.cars.push(car); delete $scope.car; }; $scope.calculateTicket = function (car) { $scope.ticket = parkingService.calculateTicket(car); }; });
The framework allows the creation of a service component in different ways. The most usual way is to create it using a factory. Therefore, we need to register the service in the application module that passes two parameters: the name of the service and the factory function.
A factory function is a pattern used to create objects. It is a simple function that returns a new object. However, it brings more concepts such as the Revealing Module Pattern, which we are going to cover in more detail.
To understand this pattern, let's start by declaring an object literal called car
:
var car = { plate: "6MBV006", color: "Blue", entrance: "2013-12-09T23:46:15.186Z" };
The JavaScript language does not provide any kind of visibility modifier; therefore, there is no way to encapsulate any property of this object, making it possible to access everything directly:
> console.log(car.plate); 6MB006 > console.log(car.color); Blue > console.log(car.entrance); 2013-12-09T23:46:15.186Z
In order to promote encapsulation, we need to use a function instead of an object literal, as follows:
var car = function () { var plate = "6MBV006"; var color = "Blue"; var entrance = "2013-12-09T23:46:15.186Z "; };
Now, it's no longer possible to access any property of the object:
> console.log(car.plate); undefined > console.log(car.color); undefined > console.log(car.entrance); undefined
This happens because the function isolates its internal scope, and based on this principle, we are going to introduce the concept of the Revealing Module Pattern.
This pattern, beyond taking care of the namespace, provides encapsulation. It allows the implementation of public and private methods, reducing the coupling within the components. It returns an object literal from the function, revealing only the desired properties:
var car = function () { var plate = "6MBV006"; var color = "Blue"; var entrance = "2013-12-09T23:46:15.186Z "; return { plate: plate, color: color }; };
Also, we need to invoke the function immediately; otherwise, the variable car
will receive the entire function. This is a very common pattern and is called IIFE, which is also known as Immediately-Invoked Function Expression:
var car = function () {
var plate = "6MBV006";
var color = "Blue";
var entrance = "2013-12-09T23:46:15.186Z ";
return {
plate: plate,
color: color
};
}();
Now, we are able to access the color but not the entrance of the car:
> console.log(car.plate); 6MB006 > console.log(car.color); Blue > console.log(car.entrance); undefined
Beyond that, we can apply another convention by prefixing the private members with _
, making the code much easier to understand:
var car = function () { var _plate = "6MBV006"; var _color = "Blue"; var _entrance = "2013-12-09T23:46:15.186Z "; return { plate: _plate, color: _color }; }();
This is much better than the old-school fashion implementation of the first example, don't you think? This approach could be used to declare any kind of AngularJS component, such as services, controllers, filters, and directives.
In the following code, we have created our parkingService
using a factory function and the Revealing Module Pattern:
services.js parking.factory("parkingService", function () { var _calculateTicket = function (car) { var departHour = new Date().getHours(); var entranceHour = car.entrance.getHours(); var parkingPeriod = departHour – entranceHour; var parkingPrice = parkingPeriod * 10; return { period: parkingPeriod, price: parkingPrice }; }; return { calculateTicket: _calculateTicket }; });
In our first service, we started to create some parking business rules. From now, the entrance hour is subtracted from the departure hour and multiplied by $10.00 to get the parking rate per hour.
However, these rules were created by means of hardcoded information inside the service and might bring maintenance problems in the future.
To figure out this kind of a situation, we can create constants. It's used to store configurations that might be required by any application component. We can store any kind of JavaScript data type such as a string, number, Boolean, array, object, function, null, and undefined.
To create a constant, we need to register it in the application module. In the following code, there is an example of the steps required to create a constant:
constants.js parking.constant("parkingConfig", { parkingRate: 10 });
Next, we refactored the _calculateTicket
method in order to use the settings from the parkingConfig
constant, instead of the hard coded values. In the following code, we are injecting the constant inside the parkingService
method and replacing the hard coded parking rate:
services.js parking.factory("parkingService", function (parkingConfig) { var _calculateTicket = function (car) { var departHour = new Date().getHours(); var entranceHour = car.entrance.getHours(); var parkingPeriod = departHour – entranceHour; var parkingPrice = parkingPeriod * parkingConfig.parkingRate; return { period: parkingPeriod, price: parkingPrice }; }; return { calculateTicket: _calculateTicket }; });
The framework also provides another kind of service called value. It's pretty similar to the constants; however, it can be changed or decorated.
There are other ways to create services with AngularJS, but hold on, you might be thinking "why should we consider this choice if we have already used the factory?"
Basically, this decision is all about design. The service is very similar to the factory; however, instead of returning a factory function, it uses a constructor function, which is equivalent to using the new operator.
In the following code, we created our parkingService
method using a constructor function:
services.js parking.service("parkingService", function (parkingConfig) { this.calculateTicket = function (car) { var departHour = new Date().getHours(); var entranceHour = car.entrance.getHours(); var parkingPeriod = departHour – entranceHour; var parkingPrice = parkingPeriod * parkingConfig.parkingRate; return { period: parkingPeriod, price: parkingPrice }; }; });
Also, the framework allows us to create services in a more complex and configurable way using the provider
function.
Sometimes, it might be interesting to create configurable services. They are called providers, and despite being more complex to create, they can be configured before being available to be injected inside other components.
While the factory works by returning an object and the service with the constructor function, the provider relies on the $get
function to expose its behavior. This way, everything returned by this function becomes available through the dependency injection.
In the following code, we refactored our service to be implemented by a provider. Inside the $get
function, the calculateTicket
method is being returned and will be accessible externally.
services.js parking.provider("parkingService", function (parkingConfig) { var _parkingRate = parkingConfig.parkingRate; var _calculateTicket = function (car) { var departHour = new Date().getHours(); var entranceHour = car.entrance.getHours(); var parkingPeriod = departHour – entranceHour; var parkingPrice = parkingPeriod * _parkingRate; return { period: parkingPeriod, price: parkingPrice }; }; this.setParkingRate = function (rate) { _parkingRate = rate; }; this.$get = function () { return { calculateTicket: _calculateTicket }; }; });
In order to configure our provider, we need to use the config
function of the Module API, injecting the service through its function. In the following code, we are calling the setParkingRate
method of the provider, overwriting the default rate that comes from the parkingConfig
method.
config.js parking.config(function (parkingServiceProvider) { parkingServiceProvider.setParkingRate(10); });
The other service components such as constants, values, factories, and services are implemented on the top of the provider component, offering developers a simpler way of interaction.
Now, it's time to check out the most important and useful built-in services for our daily development. In the following topics, we will explore how to perform communication with the backend, create a logging mechanism, support timeout, single-page application, and many other important tasks.
Every client-side JavaScript application needs to communicate with the backend. In general, this communication is performed through an interface, which is exposed by the server-side application that relies on the HTTP protocol to transfer data through the JSON.
In the past, for many years, the most common way to interact with the backend was through HTTP with the help of the GET
and POST
methods. The GET
method was usually used to retrieve data, while POST
was used to create and update the same data. However, there was no rule, and we were feeling the lack of a good standard to embrace.
The following are some examples of this concept:
GET /retrieveCars HTTP/1.1 GET /getCars HTTP/1.1 GET /listCars HTTP/1.1 GET /giveMeTheCars HTTP/1.1 GET /allCars HTTP/1.1
Now, if we want to obtain a specific car, we need to add some parameters to this URL, and again, the lack of standard makes things harder:
GET /retrieveCar?carId=10 HTTP/1.1 GET /getCar?idCar=10 HTTP/1.1 GET /giveMeTheCar?car=10 HTTP/1.1
Introduced a long time ago by Roy Fielding, the REST
method, or Representational State Transfer, has become one of the most adopted architecture styles in the last few years. One of the primary reasons for all of its success is the rise of the AJAX-based technology and also the new generation of web applications, based on the frontend.
It's strongly based on the HTTP protocol by means of the use of most of its methods such as GET
, POST
, PUT
, and DELETE
, bringing much more semantics and providing standardization.
Basically, the primary concept is to replace the verbs for nouns, keeping the URLs as simple and intuitive as possible. This means changing actions such as retrieveCars
, listCars
, and even getCars
for the use of the resource cars, and the method GET
, which is used to retrieve information, as follows:
GET /cars HTTP/1.1
Also, we can retrieve information about a specific car as follows:
GET /cars/1 HTTP/1.1
The POST
method is reserved to create new entities and also to perform complex searches that involve a large amount of data. This is an important point; we should always avoid transmitting information that might be exposed to encoded errors through the GET
method, as long as it doesn't have a content type.
This way, in order to create a new car, we should use the same resource, cars
, but this time, with the POST
method:
POST /cars HTTP/1.1
The car information will be transmitted within the request body, using the desired format. The major part of the libraries and frameworks works really well with JSON, also known as JavaScript Object Notation, which is a lightweight data interchange format.
The following code shows an object literal after being converted into JSON through the JSON.stringify
function:
{ "plate": "6MBV006", "color": "Blue", "entrance": "2013-12-09T23:46:15.186Z" }
Also, the framework provides a built-in function called angular.toJson
that does the same job of converting an object literal to JSON. To perform the other way round, we can use the angular.fromJson
function, which is equivalent to the JSON.parse
function.
To change any entity that already exists, we can rely on the PUT
method, using the same concepts used by the POST
method.
PUT /cars/1 HTTP/1.1
Finally, the DELETE
method is responsible for deleting the existing entities.
DELETE /cars/1 HTTP/1.1
Another important thing to keep in mind is the status code that is returned in each response. It determines the result of the entire operation and must allow us to implement the correct application behavior in case there is an error.
There are many status codes available in the HTTP protocol; however, we should understand and handle at least the following:
In case of an error, the response must bring the associated message, explaining what's happening and allowing the developers to handle it.
There are many other concepts involving REST
. This is just a brief overview and as it is not the purpose of this book, you can consider studying it from a more specific source.
AJAX, also known as Asynchronous JavaScript and XML, is a technology that allows the applications to send and retrieve data from the server asynchronously, without refreshing the page. The $http
service wraps the low-level interaction with the XMLHttpRequest
object, providing an easy way to perform calls.
This service could be called by just passing a configuration object, used to set a lot of important information such as the method, the URL of the requested resource, the data to be sent, and many others:
$http({method: "GET", url: "/resource"});
It also returns a promise that we are going to explain in more detail in the Asynchronous with a promise-deferred pattern section. We can attach the success and error behavior to this promise:
$http({method: "GET", url: "/resource"}) .success(function (data, status, headers, config, statusText) { }) .error(function (data, status, headers, config, statusText) { });
To make it easier to use, the following shortcut methods are available for this service. In this case, the configuration object is optional:
$http.get(url, [config]) $http.post(url, data, [config]) $http.put(url, data, [config]) $http.head(url, [config]) $http.delete(url, [config]) $http.jsonp(url, [config])
Now, it's time to integrate our parking application with the backend by calling the resource cars with the GET
method. It will retrieve the cars, binding it to the $scope
object. In the case that something goes wrong, we are going to log it to the console:
controllers.js parking.controller("parkingCtrl", function ($scope, parkingService, $http) { $scope.appTitle = "[Packt] Parking"; $scope.colors = ["White", "Black", "Blue", "Red", "Silver"]; $scope.park = function (car) { car.entrance = new Date(); $scope.cars.push(car); delete $scope.car; }; $scope.calculateTicket = function (car) { $scope.ticket = parkingService.calculateTicket(car); }; var retrieveCars = function () { $http.get("/cars") .success(function(data, status, headers, config) { $scope.cars = data; }) .error(function(data, status, headers, config) { switch(status) { case 401: { $scope.message = "You must be authenticated!" break; } case 500: { $scope.message = "Something went wrong!"; break; } } console.log(data, status); }); }; retrieveCars(); });
The success and error methods are called asynchronously when the server returns the HTTP request. In case of an error, we must handle the status code properly and implement the correct behavior.
There are certain methods that require a data parameter to be passed inside the request body such as the POST
and PUT
methods. In the following code, we are going to park a new car inside our parking lot:
controllers.js parking.controller("parkingCtrl", function ($scope, parkingService, $http) { $scope.appTitle = "[Packt] Parking"; $scope.colors = ["White", "Black", "Blue", "Red", "Silver"]; $scope.parkCar = function (car) { $http.post("/cars", car) .success(function (data, status, headers, config) { retrieveCars(); $scope.message = "The car was parked successfully!"; }) .error(function (data, status, headers, config) { switch(status) { case 401: { $scope.message = "You must be authenticated!" break; } case 500: { $scope.message = "Something went wrong!"; break; } } console.log(data, status); }); }; $scope.calculateTicket = function (car) { $scope.ticket = parkingService.calculateTicket(car); }; var retrieveCars = function () { $http.get("/cars") .success(function(data, status, headers, config) { $scope.cars = data; }) .error(function(data, status, headers, config) { switch(status) { case 401: { $scope.message = "You must be authenticated!" break; } case 500: { $scope.message = "Something went wrong!"; break; } } console.log(data, status); }); }; retrieveCars(); });
Now, we have the opportunity to evolve our design by introducing a service that will act as a facade and interact directly with the backend. The mapping of each URL pattern should not be under the controller's responsibility; otherwise, it could generate a huge amount of duplicated code and a high cost of maintenance.
In order to increase the cohesion of our controller, we moved the code responsible to make the calls to the backend of the parkingHttpFacade
service, as follows:
services.js parking.factory("parkingHttpFacade", function ($http) { var _getCars = function () { return $http.get("/cars"); }; var _getCar = function (id) { return $http.get("/cars/" + id); }; var _saveCar = function (car) { return $http.post("/cars", car); }; var _updateCar = function (car) { return $http.put("/cars" + car.id, car); }; var _deleteCar = function (id) { return $http.delete("/cars/" + id); }; return { getCars: _getCars, getCar: _getCar, saveCar: _saveCar, updateCar: _updateCar, deleteCar: _deleteCar }; }); controllers.js parking.controller("parkingCtrl", function ($scope, parkingService, parkingHttpFacade) { $scope.appTitle = "[Packt] Parking"; $scope.colors = ["White", "Black", "Blue", "Red", "Silver"]; $scope.parkCar = function (car) { parkingHttpFacade.saveCar(car) .success(function (data, status, headers, config) { retrieveCars(); $scope.message = "The car was parked successfully!"; }) .error(function (data, status, headers, config) { switch(status) { case 401: { $scope.message = "You must be authenticated!" break; } case 500: { $scope.message = "Something went wrong!"; break; } } console.log(data, status); }); }; $scope.calculateTicket = function (car) { $scope.ticket = parkingService.calculateTicket(car); }; var retrieveCars = function () { parkingHttpFacade.getCars() .success(function(data, status, headers, config) { $scope.cars = data; }) .error(function(data, status, headers, config) { switch(status) { case 401: { $scope.message = "You must be authenticated!" break; } case 500: { $scope.message = "Something went wrong!"; break; } } console.log(data, status); }); }; retrieveCars(); });
By default, the framework adds some HTTP headers to all of the requests, and other headers only to the POST
and PUT
methods.
The headers are shown in the following code, and we can check them out by analyzing the $http.defaults.headers
configuration object:
{ "common":{"Accept":"application/json, text/plain, */*"}, "post":{"Content-Type":"application/json;charset=utf-8"}, "put":{"Content-Type":"application/json;charset=utf-8"}, "patch":{"Content-Type":"application/json;charset=utf-8"} }
In case you want to add a specific header or even change the defaults, you can use the run
function of the Module API, which is very useful in initializing the application:
run.js parking.run(function ($http) { $http.defaults.headers.common.Accept = "application/json"; });
After the header configuration, the request starts to send the custom header:
GET /cars HTTP/1.1 Host: localhost:3412 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:29.0) Accept: application/json Accept-Language: pt-br,pt;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate
The headers can also be configured through the configuration object of each request. It will overwrite the default headers configured here.
To improve the performance of our application, we can turn on the framework's caching mechanism. It will store each response from the server, returning the same result every time the same request is made.
However, take care. Some applications demand updated data, and the caching mechanism may introduce some undesired behavior. In the following code, we are enabling the cache mechanism:
run.js parking.run(function ($http) { $http.defaults.cache = true; });
The framework also provides an incredible HTTP intercepting mechanism. It allows us to create common behaviors for different kinds of situations such as verifying whether a user is already authenticated or to gather information for auditing purposes.
The first is the request
interceptor. This interceptor is called before the request is being sent to the backend. It is very useful when we need to add information such as additional parameters or even headers to the request.
In the following code, we create an interceptor called httpTimestampInterceptor
, which adds the current time in milliseconds to each request that is made by the application:
parking.factory('httpTimestampInterceptor', function(){ return{ 'request' : function(config) { var timestamp = Date.now(); config.url = config.url + "?x=" + timestamp; return config; } } });
Something might happen with the request, causing an error. With the requestError
interceptor, we can handle this situation. It is called when the request is rejected and can't be sent to the backend.
The response
interceptor is called right after the response arrives from the backend and receives a response as a parameter. It's a good opportunity to apply any preprocessing behavior that may be required.
One of the most common intercepting situations is when the backend produces any kind of error, returning a status code to indicate unauthorized access, a bad request, a not found error, or even an internal server error. It could be handled by the responseError
interceptor, which allows us to properly apply the correct behavior in each situation.
This httpUnauthorizedInterceptor
parameter, in the following code, is responsible for handling the unauthorized error and changing the login property of $rootScope
, indicating that the application should open the login dialog:
parking.factory('httpUnauthorizedInterceptor', function($q, $rootScope){ return{ 'responseError' : function(rejection) { if (rejection.status === 401){ $rootScope.login = true; } return $q.reject(rejection); } } });
After defining the interceptors, we need to add them to $httpProvider
using the config
function of the Module API, as follows:
config.js app.config(function ($httpProvider) { $httpProvider.interceptors.push('httpTimestampInterceptor'); $httpProvider.interceptors.push('httpUnauthorizedInterceptor'); });
In the past few years, the single-page application, also known as SPA, has been growing in popularity among frontend developers. It improves customers' experiences by not requiring the page to be constantly reloaded, taking advantage of technologies such as AJAX and massive DOM manipulation.
AngularJS supports this feature through the $route
service. Basically, this service works by mapping URLs against controllers and views, also allowing parameter passing. This service is part of the ngRoute
module and we need to declare it before using it, as follows:
index.html <script src="angular-route.js"></script>
After this, the module should be imported to the parking
module:
app.js var parking = angular.module("parking", ["ngRoute"]);
With the $routeProvider
function, we are able to configure the routing mechanism of our application. This can be done by adding each route through the when
function, which maps the URL pattern to a configuration object. This object has the following information:
controller
: This is the name of the controller that should be associated with the templatetemplateUrl
: This is the URL of the template that will be rendered by the ngView
moduleresolve
: This is the map of dependencies that should be resolved and injected inside the controller (optional)redirectTo
: This is the redirected locationAlso, there is an otherwise
function. It is called when the route cannot be matched against any definition. This configuration should be done through the config
function of the Module API, as follows:
config.js parking.config(function ($routeProvider) { $routeProvider. when("/parking", { templateUrl: "parking.html", controller: "parkingCtrl" }). when("/car/:id", { templateUrl: "car.html", controller: "carCtrl" }). otherwise({ redirectTo: '/parking' }); });
At the same time, we need to move the specific content from the index.html
file to the parking.html
file, and in its place, we introduce the ngView
directive. This directive works with the $route
service and is responsible for rendering each template according to the routing mechanism configuration:
index.html <!doctype html> <html ng-app="parking"> <head> <title>[Packt] Parking</title> <script src="js/lib/angular.js"></script> <script src="js/lib/angular-route.js"></script> <script src="js/app.js"></script> <script src="js/config.js"></script> <script src="js/run.js"></script> <script src="js/controllers.js"></script> <script src="js/directives.js"></script> <script src="js/filters.js"></script> <script src="js/services.js"></script> <link rel="stylesheet" type="text/css" href="css/app.css"> </head> <body> <div ng-view></div> </body> </html> parking.html <input type="text" ng-model="criteria" placeholder="What are you looking for?" /> <table> <thead> <tr> <th></th> <th> <a href=""ng-click="field = 'plate'; order=!order"> Plate </a> </th> <th> <a href=""ng-click="field = 'color'; order=!order"> Color </a> </th> <th> <a href=""ng-click="field = 'entrance'; order=!order"> Entrance </a> </th> </tr> </thead> <tbody> <tr ng-class="{selected: car.selected}" ng-repeat="car in cars | filter:criteria | orderBy:field:order" > <td> <input type="checkbox" ng-model="car.selected" /> </td> <td><a href="#/car/{{car.id}}">{{car.plate}}</a></td> <td>{{car.color}}</td> <td>{{car.entrance | date:'dd/MM/yyyy hh:mm'}}</td> </tr> </tbody> </table> <form name="carForm"> <input type="text" name="plateField" ng-model="car.plate" placeholder="What's the plate?" ng-required="true" ng-minlength="6" ng-maxlength="10" ng-pattern="/[A-Z]{3}[0-9]{3,7}/" /> <select ng-model="car.color" ng-options="color for color in colors" > Pick a color </select> <button ng-click="park(car)" ng-disabled="carForm.$invalid" > Park </button> </form> <alert ng-show="carForm.plateField.$dirty && carForm.plateField.$invalid" topic="Something went wrong!" > <span ng-show="carForm.plateField.$error.required"> You must inform the plate of the car! </span> <span ng-show="carForm.plateField.$error.minlength"> The plate must have at least 6 characters! </span> <span ng-show="carForm.plateField.$error.maxlength"> The plate must have at most 10 characters! </span> <span ng-show="carForm.plateField.$error.pattern"> The plate must start with non-digits, followed by 4 to 7 numbers! </span> </alert>
The route mechanism also allows us to pass parameters. In order to obtain the passed parameter inside the controller, we need to inject the $routeParams
service, which will provide us with the parameters passed through the URL:
controller.js parking.controller("carController", function ($scope, $routeParams, parkingHttpFacade, parkingService) { $scope.depart = function (car) { parkingHttpFacade.deleteCar(car) .success(function (data, status) { $scope.message = "OK"; }) .error(function (data, status) { $scope.message = "Something went wrong!"; }); }; var retrieveCar = function (id) { parkingHttpFacade.getCar(id) .success(function (data, status) { $scope.car = data; $scope.ticket = parkingService.calculateTicket(car); }) .error(function (data, status) { $scope.message = "Something went wrong!"; }); }; retrieveCar($routeParams.id); }); car.html <h3>Car Details</h3> <h5>Plate</h5> {{car.plate}} <h5>Color</h5> {{car.color}} <h5>Entrance</h5> {{car.entrance | date:'dd/MM/yyyy hh:mm'}} <h5>Period</h5> {{ticket.period}} <h5>Price</h5> {{ticket.price | currency}} <button ng-click="depart(car)">Depart</button> <a href="#/parking">Back to parking</a>
There are many ways to navigate, but we need to identify where our resource is located before we decide which strategy to follow. In order to navigate within the route mechanism, without refreshing the page, we can use the $location
service. There is a function called path
that will change the URL after the #
, allowing the application to be a single-page one.
However, sometimes, it might be necessary to navigate out of the application boundaries. It could be done by the $window
service by means of the location.href
property as follows:
controller.js parking.controller("carController", function ($scope, $routeParams, $location, $window, parkingHttpFacade, parkingService) { $scope.depart = function (car) { parkingHttpFacade.deleteCar(car) .success(function (data, status) { $location.path("/parking"); }) .error(function (data, status) { $window.location.href = "error.html"; }); }; var retrieveCar = function (id) { parkingHttpFacade.getCar(id) .success(function (data, status) { $scope.car = data; $scope.ticket = parkingService.calculateTicket(car); }) .error(function (data, status) { $window.location.href = "error.html"; }); }; retrieveCar($routeParams.id); });
Very often, the controller needs to resolve some asynchronous promises before being able to render the view. These promises are, in general, the result of an AJAX call in order to obtain the data that will be rendered. We are going to study the promise-deferred pattern later in this Lesson.
In our previous example, we figured this out by creating and invoking a function called retrieveCars
directly from the controller:
controllers.js
parking.controller("parkingCtrl", function ($scope, parkingHttpFacade) {
var retrieveCars = function () {
parkingHttpFacade.getCars()
.success(function(data, status, headers, config) {
$scope.cars = data;
})
.error(function(data, status, headers, config) {
switch(status) {
case 401: {
$scope.message = "You must be authenticated!"
break;
}
case 500: {
$scope.message = "Something went wrong!";
break;
}
}
console.log(data, status);
});
};
retrieveCars();
});
The same behavior could be obtained by means of the resolve
property, defined inside the when
function of the $routeProvider
function with much more elegance, as follows:
config.js parking.config(function ($routeProvider) { $routeProvider. when("/parking", { templateUrl: "parking.html", controller: "parkingCtrl", resolve: { "cars": function (parkingHttpFacade) { return parkingHttpFacade.getCars(); } } }). when("/car/:id", { templateUrl: "car.html", controller: "carCtrl", resolve: { "car": function (parkingHttpFacade, $route) { var id = $route.current.params.id; return parkingHttpFacade.getCar(id); } } }). otherwise({ redirectTo: '/parking' }); });
After this, there is a need to inject the resolved objects inside the controller:
controllers.js parking.controller("parkingCtrl", function ($scope, cars) { $scope.cars = cars.data; }); parking.controller("parkingCtrl", function ($scope, car) { $scope.car = car.data; });
There are three events that can be broadcasted by the $route
service and are very useful in many situations. The broadcasting mechanism will be studied in the next Lesson, Lesson 5, Scope.
The first event is the $routeChangeStart
event. It will be sent when the routing process starts and can be used to create a loading flag, as follows:
run.js parking.run(function ($rootScope) { $rootScope.$on("$routeChangeStart", function(event, current, previous, rejection)) { $rootScope.loading = true; }); });
After this, if all the promises are resolved, the $routeChangeSuccess
event is broadcasted, indicating that the routing process finished successfully:
run.js parking.run(function ($rootScope) { $rootScope.$on("$routeChangeSuccess", function(event, current, previous, rejection)) { $rootScope.loading = false; }); });
If any of the promises are rejected, the $routeChangeError
event is broadcasted, as follows:
run.js parking.run(function ($rootScope, $window) { $rootScope.$on("$routeChangeError", function(event, current, previous, rejection) { $window.location.href = "error.html"; }); });
This service is very simple and can be used to create a logging strategy for the application that could be used for debug purposes.
There are five levels available:
We just need to inject this service inside the component in order to be able to log anything from it, as follows:
parking.controller('parkingController', function ($scope, $log) { $log.info('Entered inside the controller'); });
Is it possible to turn off the debug logging through the $logProvider
event? We just need to inject the $logProvider
event to our application config
and set the desired configuration through the debugEnabled
method:
parking.config(function ($logProvider) { $logProvider.debugEnabled(false); });
The $timeout
service is really useful when we need to execute a specific behavior after a certain amount of time. Also, there is another service called $interval
; however, it executes the behavior repeatedly.
In order to create a timeout, we need to obtain its reference through the dependency injection mechanism and invoke it by calling the $timeout
service that passes two parameters: the function to be executed and the frequency in milliseconds.
Now, it's time to create an asynchronous search service that will be called by the controller every time the user presses a key down inside the search box. It will wait for 1000 milliseconds until the search algorithm is executed:
parking.factory('carSearchService', function ($timeout) { var _filter = function (cars, criteria, resultCallback) { $timeout(function () { var result = []; angular.forEach(cars, function (car) { if (_matches(car, criteria)) { result.push(car); } }); resultCallback(result); }, 1000); }; var _matches = function (car, criteria) { return angular.toJson(car).indexOf(criteria) > 0; }; return { filter: _filter } });
A very common requirement when creating an instant search is to cancel the previously scheduled timeout, replacing it with a new one. It avoids an unnecessary consumption of resources, optimizing the whole algorithm.
In the following code, we are interrupting the timeout. It can be achieved by calling the cancel
method on the $timeout
object that is passing the promise reference as a parameter:
parking.factory('carSearchService', function ($timeout) { var filterPromise; var _filter = function (cars, criteria, resultCallback) { $timeout.cancel(filterPromise); filterPromise = $timeout(function () { var result = []; angular.forEach(cars, function (car) { if (_matches(car, criteria)) { result.push(car); } }); resultCallback(result); }, 1000); }; var _matches = function (car, criteria) { return angular.toJson(car).indexOf(criteria) > 0; }; return { filter: _filter } });
Nowadays, web applications are demanding increasingly advanced usability requirements, and therefore rely strongly on asynchronous implementation in order to obtain dynamic content from the backend, applying animated visual effects, or even to manipulate DOM all the time.
In the middle of this endless asynchronous sea, callbacks help many developers to navigate through its challenging and confusing waters.
According to Wikipedia:
"A callback is a piece of executable code that is passed as an argument to other code, which is expected to callback, executing the argument at some convenient time."
The following code shows the implementation of the carSearchService
function. It uses a callback to return the results to the controller after the search has been executed. In this case, we can't just use the return
keyword because the $timeout
service executes the search in the future, when its timeout expires. Consider the following code snippet:
services.js parking.factory('carSearchService', function ($timeout) { var _filter = function (cars, criteria, successCallback, errorCallback) { $timeout(function () { var result = []; angular.forEach(cars, function (car) { if (_matches(car, criteria)) { result.push(car); } }); if (result.length > 0) { successCallback(result); } else { errorCallback("No results were found!"); } }, 1000); }; var _matches = function (car, criteria) { return angular.toJson(car).indexOf(criteria) > 0; }; return { filter: _filter } });
In order to call the filter
function properly, we need to pass both callbacks to perform the success, as follows:
controllers.js $scope.searchCarsByCriteria = function (criteria) { carSearchService.filter($scope.cars, criteria, function (result) { $scope.searchResult = result; }, function (message) { $scope.message = message; }); };
However, there are situations in which the numerous number of callbacks, sometimes even dangerously chained, may increase the code complexity and transform the asynchronous algorithms into a source of headaches.
To figure it out, there is an alternative to the massive use of callbacks—the promise and the deferred patterns. They were created a long time ago and are intended to support this kind of situation by returning a promise object, which is unknown while the asynchronous block is processed. As soon as something happens, the promise is deferred and notifies its handlers. It is created without any side effects and returns the promise.
In order to create a new promise, we need to inject the $q
service into our component and call the $q.defer()
function to instantiate a deferred object. It will be used to implement the asynchronous behavior in a declarative way through its API. Some of the functions are as follows:
resolve(result)
: This resolves the promise with the result.reject(reason)
: This rejects the promise with a reason.notify(value)
: This provides updated information about the progress of the promise. Consider the following code snippet:services.js parking.factory('carSearchService', function ($timeout, $q) { var _filter = function (cars, criteria) { var deferred = $q.defer(); $timeout(function () { var result = []; angular.forEach(cars, function (car) { if (_matches(car, criteria)) { result.push(car); } }); if (result.length > 0) { deferred.resolve(result); } else { deferred.reject("No results were found!"); } }, 1000); return deferred.promise; }; var _matches = function (car, criteria) { return angular.toJson(car).indexOf(criteria) > 0; }; return { filter: _filter } });
With the promise object in hand, we can handle the expected behavior of the asynchronous return of any function. There are three methods that we need to understand in order to deal with promises:
then (successCallback, errorCallback, notifyCallback)
: The success callback is invoked when the promise is resolved. In the same way, error callback is called if the promise is rejected. If we want to keep track of our promise, the notify callback is called every time the promise is notified. Also, this method returns a new promise, allowing us to create a chain of promises.catch(errorCallback)
: This promise is just an alternative and is equivalent to .then(null, errorCallback)
.finally(callback)
: Like in other languages, finally
can be used to ensure that all the used resources were released properly:controllers.js $scope.filterCars = function (criteria) { carSearchService.filter($scope.cars, criteria) .then(function (result) { $scope.searchResults = result; }) .catch(function (message) { $scope.message = message; }); };
35.170.81.33