Hey there, I'm a geek.
I'm also obsessed with tackling problems that "can't" be solved, and don't believe in sitting still.

I'm currently fascinated by the Internet of Things (IOT), Quantified Self, and ways to help people utilize the mass of data around them in productive ways to power their decisions and lives. If you have any questions, a problem you want to wrestle with, or a critique you'd like to send my way, hit me up via email or any of the other links on the left.

 

Using Decorators in Angular

Angular Decorators can be an extremely helpful way of overriding or enhancing a provider without requiring you to modify the original code. These are most commonly useful when doing unit testing or trying to extend an existing service to work with custom code when you don't want to worry about accidentally overriding your customizations the next time your third-party library updates.

What?

Angular decorators are a handy way to override (or enhance) existing functionality within a factory or directive without needing to modify the original code. Once you've decorated a provider of any sort, your new or updated code will be injected into any service that requests it. This can be especially useful if you're using a third-party library and don't want to worry about your changes being undone every time you upgrade the library (or are using something like bower to manage all your external libraries). As with much of Angular, however, it's confusing in definition but can be made significantly clearer with a couple examples, so let's dive into those.

How?

$delegate

Most commonly you'll want to decorate either a factory or a directive, but in both cases the syntax is similar:

angular.module('myApp',[])
  .config(function($provide) {
    $provide.decorator('somethingIWantToChange', ['$delegate', function ($delegate) {
      // make my changes to $delegate, or replace entirely
    }]);
  });

The first thing to be aware of is that you can only inject $provide into a config block. This logically makes sense since you want every instance of your factory or directive to behave the same, but is easy to forget about. Additionally, it can often lead to difficulties if you depend on your other services within your overriding function (since actually instantiated services are only available after config inside run blocks). If that's an issue for you, see the directive example below for one way of working around that. The other thing to be aware of is that you can inject $delegate into your override function, which is the original implementation of the factory/directive you're trying to decorate. This is awesome if you only want to tweak one small part of the original feature rather than replacing it wholesale. (For those of you who read this blog consistently, you might note that I don't usually use the array notation for my dependency-injection. That's because I usually rely on ngMin to handle it for me, but at the time of writing it does not recognize decorators as something that needs to be annotated, so you need to specify it manually.)

With that general model in front of us, let's look as some specific examples for factories and directives.

Factory

The most common time I find myself decorating a factory is within my Karma tests, so let's use that as an example. I'm a big fan of optional callbacks, and since we're working within Angular, I like to make sure that any callback is executed within the Angular $digest cycle, but only if one isn't already running. The standard way to do this within Angular is to wrap your callback in $timeout. For testing, however, $timeout can be really frustrating, because Karma will often run my expectation before the nextTick has processed and executed my callback. When I'm testing that code, then, I use a decorator to replace the way $timeout operates to make it synchronous and execute instantly. The following is stripped directly out of my tests for the angular-tunnels module:

beforeEach(function () {
  module('mvd.tunnels');
    
  // Make our timeout fn synchronous for testing
  module(function($provide) {
    $provide.decorator('$timeout',function () {
      return function (fn) {
        fn()
      }
    });
  });
  ...
});

In this case I've completely replaced the way $timeout will operate within my test code, so anytime my code requests it, I'll get my synchronously executing function instead, making sure that by the time Karma runs my expectation, those changes have actually been made.

Directive

Overriding a directive is a bit more complicated, since Angular makes it's own modifications to a directive when it first bootstraps your application that often go unnoticed behind the scenes. Again, this is best explained by an example, so let's check out how we might decorate one of the angular-bootstrap directives (the accordion) in order to fire an analytics tracking service whenever someone expands/collapses the accordion:

$provide.decorator('accordionGroupDirective', ['$delegate', function($delegate) {
  var directive = $delegate[0]
    , origLink = directive.link;

  var newLink = function($scope, $element, $attrs, ctrl) {
    // Snag the injector for our app
    var injector = $element.injector();
    // Grab the analytics service
    var analytics = injector.get('analytics');
    $scope.$watch('isOpen', (isOpen, lastOpen) {
      if isOpen && isOpen != lastOpen {
        analytics.event('direct-interactions', 'opened-accordion', $attrs.accordionGroup);
      }
    });
    origLink.apply(this, arguments);
    return
  }

  // Since this has already been built via directive provider
  // need to put this on compile, not link, property
  directive.compile = function () {
    return newLink
  }

  return $delegate
}]);

There are a couple key things to notice here. The first is that when decorating directives, you receive an array, rather than the directive config directly, so you need to access $delegate[0] to get the actual config. Secondly, as mentioned before, because we have to set up our decorator within a config block, we can't just automatically inject the analytics service through standard Dependency Injection. Instead, we need to take advantage of the fact that Angular adds a method to every angular wrapped element to retrieve the application's injector, and then use that to get our service. Finally, note that we grab the original link function, but we update the compile property. This is because specifying a link function is really just a shortcut in Angular to specifying an compile function that returns a link function, and when Angular reads in a directive, it automatically rewrites it to wrap the link method within a compile function.

Recap

Hopefully this helps shed some light on some of the intricacies of Angular's decorator method, as well as some of the usefulness of it. Truthfully these are far more commonly used within your test code than live app, as those live modifications are only optimal when you're utilizing a third-party plugin but need to change some of the inner workings in how it functions. That said, for those times when it is necessary, decorators can be a much more viable solution than trying to change the actual plugin code and risk being overridden with every update (or even worse, never updating your plugins at all). 

Post by on