Angular provides it’s own dependency injection that supports everything from annotations to decorators. Interception is a feature that allows you to extend, intercept, or otherwise manipulate existing services. It makes it easy to monkey-patch existing APIs to suite the specific needs of your application. You can build an app that relies on the built-in services for common functionality such as logging and still apply your own custom behavior as needed.
To illustrate this, consider a simple Angular app. The markup simply displays a title:
The controller is a bit more interesting because it configures the title and then logs a few messages to the console. By default, Angular’s $log
service provides a safe way to log information to the browser’s console. I say “safe” because it will check if the console is present before attempting to use it so you won’t throw exceptions on older browsers. Let’s look at a simple controller that is injected its $scope
to set up the title and the <code>$log
service to log a warning and an error. Notice how the service uses the $injector
property (array) to annotate its dependencies – you can verify this by changing the name of the constructor parameters to see they will still be injected correctly.
var MyController = (function () {
function MyController($scope, $log) {
this.$scope = $scope;
this.$log = $log;
$scope.title = 'Decorator example';
$log.warn('This is a warning.');
$log.error('This is an error.');
}
MyController.$injector = ['$scope', '$log'];
return MyController;
})();
Wiring the app is then simple:
var app = angular.module('myApp', []);
app.controller('MyController', MyController);
When you run the app in a browser with the console open, you’ll see the warning and error written to the console. Of course, some systems may wish to capture errors and warnings in a different way. You may want to present a debug console to the user regardless of the browser they are in, or even wire up a service that can record exceptions on the server. Either way, instead of writing your own logging service, you can use Angular’s decorator to monkey-patch the $log
service and extend it with your own functionality. This supports the open/closed principle, to keep your components open to extension but closed to direct modification.
The Custom Service
First I’ll extend the markup to provide a console area. I wouldn't normally do this using the $rootScope
but it will keep the example simple. It also shows how we can set up a global area outside of any of the controllers:
<div data-ng-app="myApp">
<div data-ng-controller='MyController'>{{title}}</div>
<hr />
<div data-ng-repeat="line in console">
<pre>{{line}}</pre>
</div>
</div>
Now let’s create a service that simply picks up anything generated as a warning or an error and keeps track of it. (If you wanted to, you could take what I wrote about providers in my last post and use that to configure how much history the service keeps).
var MyConsole = (function () {
function MyConsole() {
this.lines = [];
this.writeLn = this.pushFn;
}
MyConsole.prototype.pushFn = function (message) {
this.lines.push(message);
};
return MyConsole;
})();
Notice this simply tracks an array internally and exposes a method to write to it. By itself, it won’t do much good because there is no way to see what’s actually being written. In the HTML we’re expecting something called console that contains a collection. Normally you’d wire this in your app’s run section or similar, but for fun let’s have the service itself set up the root scope.
Lazy-Loading a Dependency
The problem is that in using the service to monkey-patch the logger, we’ll have to instantiate it before the $rootScope
is created by Angular. If we try to inject it as a dependency, we’ll get an exception. To fix this, we’ll keep track of whether we have the root scope or not, and ask the $injector
if it is a available. Here is the updated service:
var MyConsole = (function () {
function MyConsole($injector) {
this.lines = [];
this.rootScope = false;
this.writeLn = this.lazyRootCheckFn;
this.injector = $injector;
}
MyConsole.prototype.pushFn = function (message) {
this.lines.push(message);
};
MyConsole.prototype.lazyRootCheckFn = function (message) {
this.pushFn(message);
if (!this.rootScope && this.injector.has('$rootScope')) {
this.rootScope = true;
this.injector.get('$rootScope').console = this.lines;
this.writeLn = this.pushFn;
}
};
MyConsole.$inject = ['$injector'];
return MyConsole;
})();
Notice that there are two internal functions that can be exposed as the outer writeLn
function. At first, a function called lazyRootCheckFn
that checks for the $rootScope
is wired in. It asks the $injector
if it has the $rootScope
yet, and when it does, wires it up. It then swaps the external function with the simpler function called pushFn that simply adds the message to the list. This prevents it from checking again every time it is called because the $rootScope
wire-up is a one-time event. This is also how you can lazy-load dependencies when they are not available to your app, because the $injector
is the first thing Angular wires up as it handles everything else and allows you to query whether something has been configured yet using the has function. Now the console on the root scope is wired to the collection of messages and ready for data-binding. Of course, you won’t see anything yet because the logger needs to be intercepted.
Interception using Decorator
To intercept a service, request the $provide
service during your app’s configuration:
app.config([
'$provide', function ($provide) {
}]);
This service exposes a function named decorator that allows you to intercept a service. You pass the decorator the service you wish to intercept, then an annotated function that you use for decoration. That function should request a dependency named $delegate
. The $delegate
dependency passed in is the service you wish to intercept (in this case, the $log
service). At the end of the function, you return the service to take it’s place. You could return an entirely new service that mimics the API of the original, or in the case of our example simply monkey-patch the existing service and return it “as is.” Here I just return the original service:
$provide.decorator('$log', [
'$delegate',
'myConsole',
function ($delegate, myConsole) {
return $delegate;
}]);
I want to intercept calls to warn
and error
, so I created a function to reuse for patching:
var swap = function (originalFn) {
return function () {
var args = [].slice.call(arguments);
angular.forEach(args, function (value, index) {
myConsole.writeLn(value);
});
originalFn.apply(null, args);
}
};
The function returns a new function that effectively parses out the arguments into an array, sends them to my own version of the console service, then calls the original function with the arguments list. Now I can monkey-patch the existing methods to call my own:
$delegate.warn = swap($delegate.warn);
$delegate.error = swap($delegate.error);
That’s it! My controller has no clue anything changed and is still faithfully calling the $log
service. However, as a result of intercepting that service and adding a call to myConsole
, and the fact that myConsole
lazy-loads the $rootScope
and wires up, it now will display errors and warnings on the page itself. In this post I've attempted to further demonstrate just how powerful and flexible Angular is for client-side development. I’ve created a fiddle with full source code for you to experiment with on your own. Enjoy!