Angular.js

Interception using Decorator and Lazy Loading with AngularJS

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:

<div data-ng-app='myApp'>
    <div data-ng-controller='MyController'>{{title}}</div>     
</div>

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 $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 up 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:

console_thumb1

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.

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.

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.

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; // this is the $log service
}]);

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:

Decorator example

This is a warning.
This is an error.

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!

Jeremy Likness

Jeremy Likness is a principal architect at iVision, Inc. He has been building enterprise applications using the Microsoft stack for 20 years with a focus on web-based solutions for the past 15. A prolific author and speaker, Jeremy's mission is to empower developers to create success in their careers through learning and growth.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button