AngularJS Services for Managing Injection

The $injector service is responsible for determining the dependencies that a function declares and resolving those dependencies. Table 24-3 lists the methods supported by the $injector service.

The $injector service is right at the core of the AngularJS library, and there is rarely a need to work directly with it, but it can be useful for understanding and customizing how AngularJS works. However, these are the kind of customizations that should be considered carefully and tested thoroughly.

Tip AngularJS includes a related service called $controller, which creates instances of controllers. The only time you need to create controllers directly is when writing unit tests, and I demonstrate the use of the $controller service in Chapter 25.

1. Determining Function Dependencies

JavaScript is a fluid and dynamic language, and there is a lot to recommend it, but it lacks the ability to annotate functions to manage their execution and behavior. Other languages, such as C#, support features such as attributes that are used to express instructions or metadata about a function.

The lack of annotations means that AngularJS has to go to some extraordinary lengths to implement dependency injection, which is handled by matching the names of function arguments to services. Usually the person writing a function gets to decide the names of arguments, but in AngularJS the names take on a special significance. The annotate method defined by the $injector service is used to get the set of dependencies that a function has declared, as shown in Listing 24-2.

Listing 24-2. Getting Function Dependencies in the components.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Components</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, $injector) {

var counter = 0;

var logClick = function ($log, $exceptionHandler, message) {

if (counter == 0) {

$log.log(message);

counter++;

} else {

$exceptionHandler(“Already clicked”);

}

}

$scope.handleClick = function () {

var deps = $injector.annotate(logClick);

for (var i = 0; i < deps.length; i++) {

console.log(“Dependency: ” + deps[i]);

}

};

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well”>

<button class=”btn btn-primary” ng-click=”handleClick()”>Click Me!</button>

</div>

</body>

</html>

In this example, I have defined a function called logClick that depends on the $log and $exceptionHandler services as well as a regular argument called message. Neither of the services is declared as dependencies by the controller factory function, and my goal in this part of the chapter will be to provide the logClick function with its dependencies so that I can execute it.

Note This is not something you are likely to need to do in a real project, and I am demonstrating the use of the $injector service solely so you can see how AngularJS works internally. You can readily skip these examples if you want to stay focused on day-to-day techniques.

My first step is to get the set of dependencies from the function itself, which I do using the $injector.annotate method, like this:

var deps = $injector.annotate(logClick);

for (var i = 0; i < deps.length; i++) {

console.log(“Dependency: ” + deps[i]);

}

The argument to the annotate method is the function that you want to analyze, and the result is an array of the function’s arguments, which I write to the JavaScript console in this example, producing the following output:

Dependency: $log

Dependency: $exceptionHandler

Dependency: message

As the output demonstrates, I have received a list of all of the arguments that the function takes. Of course, not all of these are service dependencies, but I can use the $injector.has method to check whether a given service has been registered, as shown in Listing 24-3.

Listing 24-3. Filtering Function Arguments to Find Services in the components.html File

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $injector) { var counter = 0;

var logClick = function ($log, $exceptionHandler, message) {

if (counter == 0) {

$log.log(message);

counter++;

} else {

$exceptionHandler(“Already clicked”);

}

}

$scope.handleClick = function () {

var deps = $injector.annotate(logClick);

for (var i = 0; i < deps.length; i++) {

if ($injector.has(deps[i])) {

console.log(“Dependency: ” + deps[i]);

}

}

};

});

</script>

The calls I make to the has method tell me that there are $log and $exceptionHandler services available but that the message argument is not a service dependency, as illustrated by the following output:

Dependency: $log

Dependency: $exceptionHandler

2. Obtaining Service Instances

I can obtain the service objects that I need through the $injector.get method, which takes the name of a service and returns the service object. Using the objects that I obtain through the get method and providing a value for the nonservice argument, I am able to execute the logClick function, as shown Listing 24-4.

Listing 24-4. Obtaining Service Objects and Executing the Function in the components.html File

<script>

angular.module(“exampleApp”, [])

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

var counter = 0;

var logClick = function ($log, $exceptionHandler, message) {

if (counter == 0) {

$log.log(message);

counter++;

} else {

$exceptionHandler(“Already clicked”);

}

}

$scope.handleClick = function () {

var deps = $injector.annotate(logClick);

var args = [];

for (var i = 0; i < deps.length; i++) {

if ($injector.has(deps[i])) {

args.push($injector.get(deps[i]));

} else if (deps[i] == “message”) {

args.push(“Button Clicked”);

}

}

logClick.apply(null, args);

};

});

</script>

I build up an array of the arguments I need to execute the function, combining services and a value for the message argument. I then use the handy JavaScript apply method, which allows a function to be called using an array of its arguments.

Tip You may not have encountered the apply method before because it is not widely used, despite being quite handy. The first argument is the object that will be assigned to this when the function is executed, and the second argument is the array of arguments that will be passed to the function.

If you load the components.html file into the browser and click the button twice, you will see output from the $log and $exceptionHandler services written to the JavaScript console, like this:

Button Clicked

Already Clicked

3. Simplifying the Invocation Process

I have taken the long way around to the point where I can invoke the function because the $injector.invoke method will take care of locating the services and managing the additional values that I need to provide to the function. In Listing 24-5, you can see how I have used the invoke method in the example.

Listing 24-5. Using the invoke Method in the components.html File

<script>

angular.module(“exampleApp”, [])

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

var counter = 0;

var logClick = function ($log, $exceptionHandler, message) {

if (counter == 0) {

$log.log(message);

counter++;

} else {

$exceptionHandler(“Already clicked”);

}

}

$scope.handleClick = function () {

var localVars = { message: “Button Clicked” };

$injector.invoke(logClick, null, localVars);

};

});

</script>

The arguments to the invoke method are the function that will invoked, the value for this, and an object whose properties correspond to the function arguments that are not service dependencies.

4. Getting the $injector Service from the Root Element

The $rootElement service provides access to the HTML element to which the ng-app directive is applied and which is the root of the AngularJS application. The $rootElement service is presented as a jqLite object, which means you can use jqLite to locate elements or modify the DOM using the jqLite methods I described in Chapter 15. Of interest in this chapter, the $rootElement service object has an additional method called injector, which returns the $injector service object. You can see how I replaced the dependency on the $injector service with the $rootElement service in Listing 24-6.

Listing 24-6. Using the SrootElement Service in the components.html File

<script>

angular.module(“exampleApp”, [])

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

var counter = 0;

var logClick = function ($log, $exceptionHandler, message) {

if (counter == 0) {

$log.log(message);

counter++;

} else {

$exceptionHandler(“Already clicked”);

}

}

$scope.handleClick = function () {

var localVars = { message: “Button Clicked” };

$rootElement.injector().invoke(logClick, null, localVars);

};

});

</script>

Tip I have yet to find a compelling reason to access the $injector service via the $rootElement service, and I am including the information in this chapter just for completeness.

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 *