Click here to Skip to main content
15,885,188 members
Articles / Programming Languages / Javascript
Tip/Trick

Interception using Decorator and Lazy-Loading with AngularJS

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
13 Jan 2014CPOL5 min read 12.2K   7  
Learn how to perform interception of services using Angular's decorator feature.

Introduction

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:

HTML
<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 <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.

JavaScript
 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:

JavaScript
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:

HTML
<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).

JavaScript
 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: 

JavaScript
 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: 

JavaScript
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: 

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

JavaScript
 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:

JavaScript
$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!

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Program Manager Microsoft
United States United States
Note: articles posted here are independently written and do not represent endorsements nor reflect the views of my employer.

I am a Program Manager for .NET Data at Microsoft. I have been building enterprise software with a focus on line of business web applications for more than two decades. I'm the author of several (now historical) technical books including Designing Silverlight Business Applications and Programming the Windows Runtime by Example. I use the Silverlight book everyday! It props up my monitor to the correct ergonomic height. I have delivered hundreds of technical presentations in dozens of countries around the world and love mentoring other developers. I am co-host of the Microsoft Channel 9 "On .NET" show. In my free time, I maintain a 95% plant-based diet, exercise regularly, hike in the Cascades and thrash Beat Saber levels.

I was diagnosed with young onset Parkinson's Disease in February of 2020. I maintain a blog about my personal journey with the disease at https://strengthwithparkinsons.com/.


Comments and Discussions

 
-- There are no messages in this forum --