Using Module.decorator() In AngularJS 1.4
In the past, we've been able to decorate AngularJS services by using the $provide service in the configuration phase of the application bootstrap. As of AngularJS 1.4, however, the concept of a decorator has been promoted to the Module API, finally living alongside .service(), .factory(), .run(), and the rest of the module methods. And, while this isn't truly new functionality, it does come with a caveat.
Run this demo in my JavaScript Demos project on GitHub.
I really like the idea of promoting the decorator functionality to the module-level because it allows us to isolate the act of decorating. Previously, decoration was just mixed into the general configuration phase and it was, therefore, left up to the developer to decide whether or not it should get its own configuration block; or, if it should be right along side the configuration for, say, $http interceptors. By adding a .decorator() method to the Module, it points the developer in the direction of isolating decorators within their own blocks, the same way we do most everything else in AngularJS.
With this change, however, there is a small caveat that didn't exist before: the service that you are decorating has to be defined before you try to decorate it. Previously, decorators were hidden behind the "configuration phase," which was queued up internally to execute after all of your AngularJS services had already been defined. As such, the order of module method invocation didn't matter - .config() before .service(), .service() before .config() - it was all good.
Now that .decorator() is part of the module API, however, this is no longer the case. Internally, the .decorator() method depends on the $get() method of the target service, as it always did. But, now that decoration is no longer forced to be part of the configuration phase, it means that you have to be explicit in the order of operations. You have to define your target service before your .decorator() call so that the underlying $get() method is available.
I assume that this can be fixed by queuing the .decorator() calls and deferring them until the configuration phase. But, I don't have a strong enough grasp of the AngularJS bootstrap internals to try and make that happen. That said, I'd be somewhat shocked if the AngularJS team doesn't make this change in one of the upcoming dot-releases.
With that said, I put together a quick demo to showcase the Module.decorator() method. I also took this an opportunity to demonstrate that you could decorate the same service more than once. In the following code, we're decorating a simple greeting service to append more text to the return value.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Using Module.decorator() In AngularJS 1.4
</title>
</head>
<body>
<h1>
Using Module.decorator() In AngularJS 1.4
</h1>
<p>
<em>See the console</em>.
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.2.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I run when the AngularJS application is bootstrapped.
angular.module( "Demo" ).run(
function runBlock( greeting ) {
console.log( greeting( "Joanna" ) );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I generate a greeting for the given.
angular.module( "Demo" ).factory(
"greeting",
function greetingFactory() {
return( greeting );
// I return a greeting for the given name.
function greeting( name ) {
return( "Hello " + name + "." );
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I decorate the "greeting" service, altering the return value.
// --
// CAUTION: Unlike all other module methods, this decorator() has to be defined
// AFTER the service that it is decorating as it relies on the existence of the
// $get() method on the target service.
angular.module( "Demo" ).decorator(
"greeting",
function greetingDecorator( $delegate ) {
// Return the decorated service.
return( decoratedGreeting );
// I append a new message to the existing greeting.
function decoratedGreeting( name ) {
return( $delegate( name ) + " How are you doing?" );
}
}
);
// I decorate the "greeting" service, altering the return value.
// --
// NOTE: I am purposefully not combining this with the .decorator() above in
// order to demonstrate that you can decorate the same service more than once.
// --
// CAUTION: Unlike all other module methods, this decorator() has to be defined
// AFTER the service that it is decorating as it relies on the existence of the
// $get() method on the target service.
angular.module( "Demo" ).decorator(
"greeting",
function greetingDecorator( $delegate ) {
// Return the decorated service.
return( decoratedGreeting );
// I append a new message to the existing greeting.
function decoratedGreeting( name ) {
return( $delegate( name ) + " Is there anything I can get for you?" );
}
}
);
</script>
</body>
</html>
As you can see, we are taking the greeting() service and decorating it twice, adding new text with each proxy. And, when we run the above code, we get the following console output:
Hello Joanna. How are you doing? Is there anything I can get for you?
This is a welcome change in the AngularJS API and helps us developers continue to move in the direction of small, cohesive blocks of code.
Want to use code from this post? Check out the license.
Reader Comments
I submitted the order-of-operations scenario in a Pull Request. https://github.com/angular/angular.js/issues/12382
... I mean an "Issue"... I don't know how to make a PR for this project... yet :D
Been using decorators at work, just haven't had a use-case where I decorate my own services. Of course, now I can think of a few.
I'm also curious how your code would handle returning _added_ methods. I wrote a decorator method to completely strip out HTML and leave plain-text in the `$sce` decorator but I had to add it to the existing provider since I wasn't overwriting an existing method. Trying the module-revealer pattern left me with an empty Provider.
@Eric,
This doesn't have to be about decorating your own services. You might be decorating a 3rd-party service, which still puts you in the problem where you have to declare the 3rd-party service *before* you try to decorate it. As far as decorating your own services, things can get interesting.
Imagine that you want to make "lodash" an injectable, so you create a service like this:
module().factory( "_", function( $window ) {
. . . . return( $window._ );
});
Now, you can inject "_" into any of your angular modules without having to reference it on the global scope. But, let's say you also want to decorate lodash, adding new custom functions. You could create a decorator():
module().decorator( "_", function( $delegate ) { .... } );
Due to the timing problem, this would throw an unknown provider error if you attempted to run the .decorator() module function before the .factory() module function.
As far as returning a new methods, with the module pattern, I think it should work as long as you return an object (as opposed to omitting a return statement). I'd have to know more about your specific problem; but, I don't see there being an inherent issue. In fact, if you look at this blog post, that's basically what I'm doing - return an entirely new function, but making use of the existing function though the decorator closure.
Hi Ben,
I'm using Angular 1.4.7, and there is no decorator on module API level:
"Uncaught TypeError: angular.module(...).decorator is not a function"
I looked at the version of Angular you're using, and it's 1.4.2. Official documentation does mention decorator as angular.Module method:
https://code.angularjs.org/1.4.2/docs/api/ng/type/angular.Module ... However it still points $provide.decorator().
No idea how to use decorator the way you do it :) Using
angular.module('myModule').decorator() gives me Uncaught TypeError I mentioned above.
What am I missing?
Big thanks!
What am I missing.
@Lucas,
That's really odd. Even in the 1.4.7 docs, it still says that the method exists:
https://code.angularjs.org/1.4.7/docs/api/ng/type/angular.Module#decorator
I'll have to try and run something locally. Even in my R&D, I am still only using 1.4.5. I'll download the newest ones and give it a try. Very odd!
@Lucas,
I just dropped 1.4.7 into this demo and it worked perfectly. Maybe something else is going wrong in your code? Maybe a syntax error that is messing something up?
I see no syntax error :( there's just no .decorator() method on a module ...
When I use $provide.decorator everything works just fine.
I do load my directive before I apply the decorator like you said.
Still I would expect to see the .decorator() method on module level. Maybe something is wrong with my version of Angular? I've installed it from Bower.
I will try to use an older one from CDN.
Thanks for a swift answer, Ben :)
Ben, I got it!
It was AngularJS Batarang plugin.
I have already reported the issue:
https://github.com/angular/batarang/issues/275
All good, thanks! :)
@Lucas,
Very interesting! I wonder how Batarang works. Maybe the wrap the various methods or something, and don't expose that one. Anyway, good detective work!
Spend hours finding out why my unit tests were failing... I had karma setup to run, and added a decorator for some of my directives. Say my decorator was named b.decorator.js, If I decorated a.directive.js it worked, if i decorated c.directive.js it broke. Took me hours of debugging and searching until I hit this comment of this caveat of loading order... I am now changing the setup of karma to always load the .decorator.js files after the other files, but this really should be solved within angular... Anyway, thanks!
Hey thanks again, your explanation is always help young developer like me. Who trying to make a cool module :D
why we use $decorator()