AngularJS Services for Accessing the DOM API Global Objects

The simplest built-in services expose aspects of the browser’s DOM API in a way that is consistent with the rest of AngularJS or with jqLite. Table 19-2 describes these services.

1. Why and When to Use the Global Object Services

The main reason that AngularJS includes these services is to make testing easier. I get into testing in Chapter 25, but an important facet of unit testing is the need to isolate a small piece of code and test its behavior without testing the components it depends on—in essence, creating a focused test. The DOM API exposes functionality through global objects such as document and window. These objects make it hard to isolate code for unit testing without also testing the way that the browser implements its global objects. Using services such as $document allows AngularJS code to be written without directly using the DOM API global objects and allows the use of AngularJS testing services to configure specific test scenarios.

2. Accessing the Window Object

The $window service is simple to use, and declaring a dependency on it gives you an object that is a wrapper around the global window object. AngularJS doesn’t enhance or change the API provided by this global object, and you access the methods that the window object defines ust as you would if you were working directly with the DOM API. To demonstrate this service—and the others in this category—I have added an HTML file called domApi.html to the angularjs folder, the contents of which are shown in Listing 19-1.

Listing 19-1. The Contents of the domApi.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>DOM API Services</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $window) {

$scope.displayAlert = function(msg) {

$window.alert(msg);

}

});

</script>

</head>
<body ng-controller=”defaultCtrl” class=”well”>

<button class=”btn btn-primary” ng-click=”displayAlert(‘Clicked!’)”>Click Me</button>

</body>

</html>

I have declared a dependency on the $window service in order to define a controller behavior that calls the alert method. The behavior is invoked by the ng-click directive when a button element is clicked, as shown in Figure 19-1.

3. Accessing the Document Object

The $document service is a jqLite object containing the DOM API global window.document object. Since the service is presented via jqLite, you can use it to query the DOM using the methods I described in Chapter 15. In Listing 19-2, you can see the $document service applied.

Listing 19-2. Using the $document Service in the domApi.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>DOM API Services</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $window, $document) {

$document.find(“button”).on(“click”, function (event) {

$window.alert(event.target.innerText);

});

});

</script>

</head>

<body ng-controller=”defaultCtrl” class=”well”>

<button class=”btn btn-primary”>Click Ne</button>

</body>

</html>

4. Using Intervals and Timeouts

The $interval and $timeout services provide access to the window.setInterval and window.setTimeout functions, with some enhancements that make it easier to work with AngularJS. Table 19-3 shows the arguments that are passed to these services.

These functions work in the same way, in that they defer the execution of a function for a specified period of time. The difference is that the $timeout service delays and executes the function only once, whereas $interval does so periodically. Listing 19-3 shows the use of the $interval service.

Listing 19-3. Using the $interval Service in the domApi.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>DOM API Services</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $interval) {

$interval(function () {

$scope.time = new Date().toTimeString();

}, 2000);

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<h4 class=”panel-heading”>Time</h4>

<div class=”panel-body”>

The time is: {{time}}

</div>

</div>

</body>

</html>

Tip Exceptions thrown by the function passed to these services are passed to the $exceptionHandier service, which I describe in the “Dealing with Exceptions” section.

I use the $interval service to execute a function that updates a scope variable with the current time every two seconds. I omitted the final two arguments, which means the default values are applied.

5. Accessing the URL

The $location service is a wrapper around the location property of the global window object and provides access to the current URL. The $location service operates on the part of the URL following the first # character, which means it can be used for navigation within the current document but not to navigate to new documents. This may seem odd, but you rarely want the user to navigate away from the main document because it unloads your web application and discards your data and state. Consider the following URL, which is typical of an AngularJS application:

http://mydomain.com/app.html#/cities/london?select=hotels#north

The $location service lets you change the part of the URL I have emphasized, which it calls the URL and is the combination of three components: the path, the search term, and the hash. These are all terms that refer to parts of the URL before the # character, which is unfortunate but understandable because AngularJS is re-creating a complete URL after the # so that we can navigate within the application—something that is made easier using the service I describe in Chapter 22. Here is the same URL with the path emphasized:

http://mydomain.com/app.html#/cities/london?select=hotels#north

And here it is with the search term emphasized:

http://mydomain.com/app.html#/cities/london?select=hotels#north

And, finally, here it is with the hash emphasized:

http://mydomain.com/app.html#/cities/london?select=hotels#north

In Table 19-4, I have described the methods that the $location service provides.

Tip These are messy URLs. I’ll show you how to enable support for HTML5 features in the “Using HTML5 URLs” section.

In addition to the methods shown earlier, the $location service defines two events that you can use to receive notification when the URL changes, either because of user interaction or programmatically. I have described the events in Table 19-5. Handler functions for these events are registered using the scope $on method (which I described in Chapter 15) and are passed an event object, the new URL, and the old URL.

In Listing 19-4, you can see how I have updated the domApi.html file to demonstrate the use of the $location service. This example uses all of the read-write methods so that you can see how the changes to the URL are applied.

Listing 19-4. Using the $location Service in the domApi.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>DOM API Services</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $location) {

$scope.$on(“$locationChangeSuccess”, function (event, newUrl) {

$scope.url = newUrl;

});

$scope.setUrl = function (component) {

switch (component) {

case “reset”:

$location.path(“”);

$location.hash(“”);

$location.search(“”);

break;

case “path”:

$location.path(“/cities/london”);

break;

case “hash”:

$location.hash(“north”);

break;

case “search”:

$location.search(“select”, “hotels”);

break;

case “url”:

$location.url(“/cities/london?select=hotels#north”);

break;

}

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<h4 class=”panel-heading”>URL</h4>

<div class=”panel-body”>

<p>The URL is: {{url}}</p>

<div class=”btn-group “>

<button class=”btn btn-primary” ng-click=”setUrl(‘reset’)”>Reset</button>

<button class=”btn btn-primary” ng-click=”setUrl(‘path’)”>Path</button>

<button class=”btn btn-primary” ng-click=”setUrl(‘hash’)”>Hash</button>

<button class=”btn btn-primary”

ng-click=”setUrl(‘search’)”>Search</button>

<button class=”btn btn-primary” ng-click=”setUrl(‘url’)”>URL</button>

</div>

</div>

</div>

</body>

</html>

This example contains buttons that let you set the four writable components of the URL: the path, the hash, the query string, and the URL. You can see how each component is changed and how, since the changes happen after the # character, the navigation doesn’t cause the browser to load a new document.

5.1. Using HTML5 MURLs

The standard URL format that I showed you in the previous section is messy because the application is essentially trying to duplicate the component parts of a URL after the # character so that the browser doesn’t load a new HTML document.

The HTML5 History API provides a more elegant approach to dealing with this, and the URL can be changed without causing the document to reload. All recent versions of the mainstream browsers support the History API, and support for it can be enabled in AngularJS applications through the provider for the $location service, $locationProvider. In Listing 19-5, you can see how I have enabled the History API in the domApi.html file.

Listing 19-5. Enabling the HTML5 History API in the domApi.html File

<script>

angular.module(“exampleApp”, [])

.config(function($locationProvider) {

$locationProvider.html5Mode(true);

});

.controller(“defaultCtrl”, function ($scope, $location) {

$scope.$on(“$locationChangeSuccess”, function (event, newUrl) {

$scope.url = newUrl;

});

$scope.setUrl = function (component) {

switch (component) {

case “reset”:

$location.path(“”);

$location.hash(“”);

$location.search(“”);

break;

case “path”:

$location.path(“/cities/london”);

break;

case “hash”:

$location.hash(“north”);

break;

case “search”:

$location.search(“select”, “hotels”);

break;

case “url”:

$location.url(“/cities/london?select=hotels#north”);

break;

}

}

});

</script>

Caution The History API is relatively new and is not consistently implemented by browsers. Use this feature with caution and test thoroughly.

Calling the html5Mode method with true as the argument enables the use of the HTML5 features, which has the effect of changing the parts of the URL that the methods of the $location service operate on. In Table 19-6, I have summarized the changes that the buttons in the example have on the URL displayed in the browser’s navigation bar, pressed in sequence.

This is a much cleaner URL structure, but, of course, it relies on HTML5 features that are not available on older browsers, and your application will fail to work if you enable the $location HTML5 mode for a browser that doesn’t support the History API. You can work around this by testing for the History API, either using a library like Modernizr or manually, as I have shown in Listing 19-6.

Listing 19-6. Testing for the Presence of the History API in the domApi.html File

<script>

angular.module(“exampleApp”, [])

.config(function ($locationProvider) {

if (window.history && history.pushState) {

$locationProvider.html5Mode(true);

}

})

.controller(“defaultCtrl”, function ($scope, $location) {

I have to access two global objects directly because only constant values and providers can be injected into config functions, which means I can’t use the $window service. If the browser has defined the window.history object and the history.pushState method, then I enable the HTML5 mode for the $location service and benefit from the improved URL structure. For other browsers, the HTML5 mode will be disabled, and the more complex URL structure will be used.

5.2. Scrolling to the $location Hash Location

The $anchorScroll service scrolls the browser window to display the element whose id corresponds to the value returned by the $location.hash method. Not only is the $anchorScroll service convenient to use, but it means you don’t have to access the global document object in order to locate the element to display or the global window object to perform the scrolling. Listing 19-7 shows the $anchorScroll service being used to display an element in a long document.

Listing 19-7. Using the $anchorScroll Service in the domApi.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>DOM API Services</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $location, $anchorScroll) {

$scope.itemCount = 50;

$scope.items = [];

for (var i = 0; i < $scope.itemCount; i++) {

$scope.items[i] = “Item ” + i;

}

$scope.show = function(id) {

$location.hash(id);

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<h4 class=”panel-heading”>URL</h4>

<div class=”panel-body”>

<p id=”top”>This is the top</p>

<button class=”btn btn-primary” ng-click=”show(‘bottom’)”>

Go to Bottom</button>

<p>

<ul>

<li ng-repeat=”item in items”>{{item}}</li>

</ul>

</p>

<p id=”bottom”>This is the bottom</p>

<button class=”btn btn-primary” ng-click=”show(‘top’)”>Go to Top</button>

</div>

</div>

</body>

</html>

In this example, I use the ng-repeat directive to generate a series of li elements so that one of the p elements with the id values top and bottom cannot be seen on the screen. The button elements use the ng-click directive to invoke a controller behavior called show, which accepts an element id as an argument and uses it to call the $location.hash method.

The $anchorScroll service is unusual because you don’t have to use the service object; you just declare a dependency. When the service object is created, it starts to monitor the $location.hash value and scrolls automatically when it changes. You can see the effect in Figure 19-2.

You can disable the automatic scrolling through the service provider, which allows you to selectively scroll by invoking the $anchorScroll service as a function, as shown in Listing 19-8.

Listing 19-8. Selectively Scrolling in the domApi.html File

<script>

angular.module(“exampleApp”, [])

.config(function ($anchorScrollProvider) {

$anchorScrollProvider.disableAutoScrolling();

});

.controller(“defaultCtrl”, function ($scope, $location, $anchorScroll) {

$scope.itemCount = 50;

$scope.items = [];

for (var i = 0; i < $scope.itemCount; i++) {

$scope.items[i] = “Item ” + i;

}

$scope.show = function(id) {

$location.hash(id);

if (id == “bottom”) {

$anchorScroll();

}

}

});

</script>

I use a call to the Module.config method (as described in Chapter 9) to disable automatic scrolling, which I do by calling the disableAutoScrolling method on the $anchorScrollProvider. Changes to the $location.hash value will no longer trigger automatic scrolling. To explicitly trigger scrolling, I invoke the $anchorScroll service function, which I do when the argument passed to the show behavior is bottom. The effect is that the browser scrolls when the Go to Bottom button, but not the Go to Top button, is clicked.

6. Performing Logging

I built my own simple logging service in Chapter 18, but AngularJS provides the $log service, which is a wrapper around the global console object. The $log service defines debug, error, info, log, and warn methods that correspond to those defined by the console object. As the examples in Chapter 18 demonstrate, you don’t have to use the $log service, but it does make unit testing easier. In Listing 19-9, you can see how I have modified my custom logging service to make use of the $log service to write its messages out.

Listing 19-9. Using the $log Service in the services.js File

angular.module(“customServices”, [])

.provider(“logService”, function () {

var counter = true;

var debug = true; return {

messageCounterEnabled: function (setting) {

if (angular.isDefined(setting)) {

counter = setting;

return this;

} else {

return counter;

}

},

debugEnabled: function (setting) {

if (angular.isDefined(setting)) {

debug = setting;

return this;

} else {

return debug;

}

},

$get: function ($log) {

return {

messageCount: 0, log: function (msg) {

if (debug) {

$log.log(“(LOG”

+ (counter ? ” + ” + this.messageCount++ + “) ” : “) “)

+ msg);

}

}

};

}

}

});

Notice that I declare the dependency on the service on the $get function. This is a peculiarity of using the provider function and something you don’t encounter when working with the service or factory methods. To demonstrate this, Listing 19-10 shows the $log service used in the version of the custom service I created using the factory method in Chapter 18.

Listing 19-10. Consuming $log in a Service Defined Using the Factory Method in the services.html File

angular.module(“customServices”, [])

.factory(“logService”, function ($log) {

var messageCount = 0;

return {

log: function (msg) {

$log.log(“(LOG + ” + this.messageCount++ + “) ” + msg);

}

};

});

Tip The default behavior of the $log service is not to call the debug method to the console. You can enable debugging by setting the $logProvider.debugEnabled property to true. See Chapter 18 for details of how to set provider properties.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *