angular.module(...).decorator is not a function - angularjs

I'm about to tear my eyes out over this really confusing issue, I'm trying to apply a simple decorator to the $route service but I keep getting an error saying that angular.module(...).decorator is not a function, which doesn't make sense because I've used this in other projects and it has worked just fine..
How can the decorator no longer be exposed on the angular.Module? Doing $provide.decorator doesn't work either because $provide isn't defined, but how am I supposed to inject it when the decorator is written outside of any other code..
What can be wrong here?
angular.module('core').decorator('$route', ['$delegate', function($delegate) {
$delegate.getRouteProp = function(path, prop) {
var result = null;
angular.forEach($delegate.routes, function(config, route) {
if (path === route) {
result = config[prop];
}
});
return result;
};
return $delegate;
}]);

Related

What type of spy to use for testing

I know when you use spyOn you can have different forms like .and.callFake or .andCallThrough. I'm not really sure which one I need for this code I'm trying to test...
var lastPage = $cookies.get("ptLastPage");
if (typeof lastPage !== "undefined") {
$location.path(lastPage);
} else {
$location.path('/home'); //TRYING TO TEST THIS ELSE STATEMENT
}
}
Here is some of my test code:
describe('Spies on cookie.get', function() {
beforeEach(inject(function() {
spyOn(cookies, 'get').and.callFake(function() {
return undefined;
});
}));
it("should work plz", function() {
cookies.get();
expect(location.path()).toBe('/home');
expect(cookies.get).toHaveBeenCalled();
expect(cookies.get).toHaveBeenCalledWith();
});
});
I've tried a lot of different things, but I'm trying to test the else statement. Therefore I need to make cookies.get == undefined.
Everytime I try to do that though, I get this error:
Expected '' to be '/home'.
The value of location.path() never changes when cookies.get() is equal to undefined. I think I'm using the spyOn incorrectly?
Follow-up on my mock values:
beforeEach(inject(
function(_$location_, _$route_, _$rootScope_, _$cookies_) {
location = _$location_;
route = _$route_;
rootScope = _$rootScope_;
cookies = _$cookies_;
}));
Follow-up on the functions:
angular.module('buildingServicesApp', [
//data
.config(function($routeProvider) {
//stuff
.run(function($rootScope, $location, $http, $cookies)
No names on these functions, therefore how do I call the cookies.get?
Right now, you're testing that the location.path() function works as designed. I'd say you should leave that testing to the AngularJS team :). Instead, verify that the function was called correctly:
describe('Spies on cookie.get', function() {
beforeEach((function() { // removed inject here, since you're not injecting anything
spyOn(cookies, 'get').and.returnValue(undefined); // As #Thomas noted in the comments
spyOn(location, 'path');
}));
it("should work plz", function() {
// cookies.get(); replace with call to the function/code which calls cookies.get()
expect(location.path).toHaveBeenCalledWith('/home');
});
});
Note that you shouldn't be testing that your tests mock cookies.get, you should be testing that whatever function calls the first bit of code in your question is doing the right thing.

dependency injection into decorator functions in angularJS

When using angularJS you can register a decorating function for a service by using the $provide.decorator('thatService',decoratorFn).
Upon creating the service instance the $injector will pass it (the service instance) to the registered decorating function and will use the function's result as the decorated service.
Now suppose that thatService uses thatOtherService which it has injected into it.
How I can I get a reference to thatOtherService so that I will be able to use it in .myNewMethodForThatService() that my decoratorFN wants to add to thatService?
It depends on the exact usecase - more info is needed for a definitive answer.
(Unless I've misunderstood the requirements) here are two alternatives:
1) Expose ThatOtherService from ThatService:
.service('ThatService', function ThatServiceService($log, ThatOtherService) {
this._somethingElseDoer = ThatOtherService;
this.doSomething = function doSomething() {
$log.log('[SERVICE-1]: Doing something first...');
ThatOtherService.doSomethingElse();
};
})
.config(function configProvide($provide) {
$provide.decorator('ThatService', function decorateThatService($delegate, $log) {
// Let's add a new method to `ThatService`
$delegate.doSomethingNew = function doSomethingNew() {
$log.log('[SERVICE-1]: Let\'s try something new...');
// We still need to do something else afterwards, so let's use
// `ThatService`'s dependency (which is exposed as `_somethingElseDoer`)
$delegate._somethingElseDoer.doSomethingElse();
};
return $delegate;
});
});
2) Inject ThatOtherService in the decorator function:
.service('ThatService', function ThatServiceService($log, ThatOtherService) {
this.doSomething = function doSomething() {
$log.log('[SERVICE-1]: Doing something first...');
ThatOtherService.doSomethingElse();
};
})
.config(function configProvide($provide) {
$provide.decorator('ThatService', function decorateThatService($delegate, $log, ThatOtherService) {
// Let's add a new method to `ThatService`
$delegate.doSomethingNew = function doSomethingNew() {
$log.log('[SERVICE-2]: Let\'s try something new...');
// We still need to do something else afterwatds, so let's use
// the injected `ThatOtherService`
ThatOtherService.doSomethingElse();
};
return $delegate;
});
});
You can see both approaches in action in this demo.

Angularjs - Decorate Controllers

I am trying to set up a decorator for my controllers. My intention is to introduce some common behaviour across all the controllers in my app.
I have it configured to work in Angular 1.2.x, but there are some breaking changes from 1.3.x onwards that is breaking the code. The error one now gets is "controller is not a function".
Below is the code for the decorator:
angular.module('myApp', ['ng'], function($provide) {
$provide.decorator('$controller', function($delegate) {
return function(constructor, locals) {
//Custom behaviour code
return $delegate(constructor, locals);
}
})
});
Angular 1.2.x - http://jsfiddle.net/3v17w364/2/ (Working)
Angular 1.4.x - http://jsfiddle.net/tncquyxo/2/ (Broken)
In Angular 1.4.x modules have decorator method, $provide.decorator is no longer needed.
For monkey-patching APIs it is always preferable to use arguments instead of enumerating them explicitly, the chance that it will break is much lesser.
angular.module('myApp', ['ng']).decorator('$controller', function ($delegate) {
return function (constructor, locals) {
var controller = $delegate.apply(null, arguments);
return angular.extend(function () {
locals.$scope.common = ...;
return controller();
}, controller);
};
});
Answering my own question.
Had to dig in to Angular source code to figure out whats going on.
The $controller instance is created using below code. The fix lay in the parameter 'later'. This needs to be set to true.
return function(expression, locals, later, ident) {
// PRIVATE API:
// param `later` --- indicates that the controller's constructor is invoked at a later time.
// If true, $controller will allocate the object with the correct
// prototype chain, but will not invoke the controller until a returned
// callback is invoked.
Above taken from: https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js
Updated provider code:
angular.module('myApp', ['ng'], function($provide) {
$provide.decorator('$controller', function($delegate) {
return function(constructor, locals) {
//Custom behaviour code
return $delegate(constructor, locals, true);
}
})
});
Updated fiddle: http://jsfiddle.net/v3067u98/1/

Can the Angular $injector be decorated with $provide.decorator?

Perhaps this is a terrible idea, but if it is then please tell me why and then pretend that it's an academic exercise that won't see the light of day in production.
I'd like to add some logic to the Angular $injector service, to monitor when certain services are injected into other services. Since it seems that Angular provides a mechanism for decorating services, I thought this would be the way to go. However, the following code throws an error.
(function () {
'use strict';
var app = angular.module('app');
app.config(['$provide', function ($provide) {
$provide.decorator('$injector', ['$log', '$delegate', addLoggingToInjector]);
}]);
function addLoggingToInjector($log, $delegate) {
var baseInstantiate = $delegate.instantiate;
var baseInvoke = $delegate.invoke;
$delegate.instantiate = function (type, locals) {
// $log.debug('Calling $injector.instantiate');
baseInstantiate(type, locals);
};
$delegate.invoke = function (fn, self, locals) {
// $log.debug('Calling $injector.invoke');
baseInvoke(fn, self, locals);
};
return $delegate;
};
})();
The specific error is:
Uncaught Error: [$injector:modulerr] Failed to instantiate module app
due to: Error: [$injector:unpr] Unknown provider: $injectorProvider
The answer is: no.
$provide.decorator is used to intercept service creation -- that is why it is called from .config block, when there is still time to configure all services, as none of them has been created. $provide.decorator basically gets the Provider of the service and swaps its $get with newly delivered decorFn.
$injector is not like other services. It is created, as the very first step of bootstrapping an application -- way before app.config is called. [look at functions: bootstrap and createInjector in angular source code]
But hey, you can achieve your goal quite easily by tweaking the source code just a bit :-) Particularly look at function invoke(fn, self, locals).
UPDATE I got some inspiration from #KayakDave. You actually do not have to dig in the source-code itself. You can use the following pattern to observe each call to any of $injector methods:
app.config(['$injector', function ($injector) {
$injector.proper =
{
get : $injector.get,
invoke : $injector.invoke,
instantiate : $injector.instantiate,
annotate : $injector.annotate,
has : $injector.has
}
function getDecorator(serviceName)
{
console.log("injector GET: ", serviceName);
return this.proper.get(serviceName);
}
function invokeDecorator(fn, self, locals)
{
console.log("injector INVOKE: ", fn, self, locals);
return this.proper.invoke(fn, self, locals);
}
function instantiateDecorator(Type, locals)
{
console.log("injector INSTANTIATE: ", Type, locals);
return this.proper.instantiate(Type, locals);
}
function annotateDecorator (fn)
{
console.log("injector ANNOTATE: ", fn);
return this.proper.annotate(fn);
}
function hasDecorator(name)
{
console.log("injector HAS: ", name);
return this.proper.has(name);
}
$injector.get = getDecorator;
$injector.invoke = invokeDecorator;
$injector.instantiate = instantiateDecorator;
$injector.annotate = annotateDecorator;
$injector.has = hasDecorator;
}]);
PLNKR
You can't use the Angular decorator service on $injector. As Artur notes $injector is a bit different from other services. But we can create our own decorator.
Why we can't use Angular's decorator
At the code level the issue is that $injector doesn't have a constructor function- there's no $injectorProvider.
For example both of these return true:
$injector.has('$location');
$injector.has('$locationProvider')
However, while this returns true:
$injector.has('$injector')
this returns false:
$injector.has('$injectorProvider')
We see the importance when we look at the Angular decorator function:
function decorator(serviceName, decorFn) {
var origProvider = providerInjector.get(serviceName + providerSuffix),
orig$get = origProvider.$get;
origProvider.$get = function() {
var origInstance = instanceInjector.invoke(orig$get, origProvider);
return instanceInjector.invoke(decorFn, null, {$delegate: origInstance});
};
}
And
providerSuffix = 'Provider'
So the Angular decorator expects to operate on the service's constructor (serviceName + providerSuffix). Pragmatically, since we don't have an $injectorProvider we can't use decorator.
Solution
What we can do is override the Angular injector's get function ourselves by replacing the injector's default get with one that calls the original, Angular defined, get followed by our function.
We'll apply this to $injector rather than the nonexistent $injectorProvider like so:
app.config(['$provide','$injector', function ($provide,$injector) {
// The function we'll add to the injector
myFunc = function () {
console.log("injector called ", arguments);
};
// Get a copy of the injector's get function
var origProvider = $injector,
origGet = origProvider.get;
//Override injector's get with our own
origProvider.get = function() {
// Call the original get function
var returnValue = origGet.apply(this, arguments);
// Call our function
myFunc.apply(this,arguments);
return returnValue;
}
}]);
You'll see the provider being injected is the first augment, so app.value('aValue', 'something'); yields the following log statement:
injector called ["aValueProvider"]
Demo fiddle

How to check if an angularjs controller has been defined

I've got an app defined this way:
angular.module("myApp", [...])
.config(function ($stateProvider, $controllerProvider) {
if (isControllerDefined(controllerName)) {
do_stuff();
}
})
The controllers are defined this way:
angular.module("myApp")
.controller("myController", function ($scope) { ... });
How can I define isControllerDefined() (in the config above) to check whether a given controller exists if I have the name of the controller? I feel like I should be able to do something like one of these:
var ctrl = angular.module("myApp").getController("myController");
var ctrl = $controllerProvider.get("myController");
or something like that... but I can't find any functionality for this. Help?
An example of a service that can check if a controller exists. Note that it looks for a global function with specified name as well as a controller in the $controller provider.
angular.service('ControllerChecker', ['$controller', function($controller) {
return {
exists: function(controllerName) {
if(typeof window[controllerName] == 'function') {
return true;
}
try {
$controller(controllerName);
return true;
} catch (error) {
return !(error instanceof TypeError);
}
}
};
}]);
See the fiddle for usage.
I came across this exact same issue the other day. I had a few issues with the currently accepted answer, namely because one of my controllers was performing an initialization call out to the server upon instantiation to populate some data (i.e):
function ExampleController($scope, ExampleService) {
ExampleService.getData().then(function(data) {
$scope.foo = data.foo;
$scope.bar = data.bar
});
}
As it stands, the currently accepted answer will actually instantiate the controller, before discarding it. This lead to multiple API calls being made on each request (one to verify that the controller exists, one to actually use the controller).
I had a bit of a dig around in the $controller source code and found that there's an undocumented parameter you can pass in called later which will delay instantiation. It will, however, still run all of the checks to ensure that the controller exists, which is perfect!
angular.factory("util", [ "$controller", function($controller) {
return {
controllerExists: function(name) {
try {
// inject '$scope' as a dummy local variable
// and flag the $controller with 'later' to delay instantiation
$controller(name, { "$scope": {} }, true);
return true;
}
catch(ex) {
return false;
}
}
};
}]);
UPDATE: Probably a lot easier as a decorator:
angular.config(['$provide', function($provide) {
$provide.delegate('$controller', [ '$delegate', function($delegate) {
$delegate.exists = function(controllerName) {
try {
// inject '$scope' as a dummy local variable
// and flag the $controller with 'later' to delay instantiation
$delegate(controllerName, { '$scope': {} }, true);
return true;
}
catch(ex) {
return false;
}
};
return $delegate;
}]);
}]);
Then you can simply inject $controller and call exists(...)
function($controller) {
console.log($controller.exists('TestController') ? 'Exists' : 'Does not exist');
}
There is currently no easy way of fetching a list of controllers. That is hidden for internal use only. You would have to go to the source code and add a public method that return the internal controllers variable (in $ControllerProvider function)
https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L16
this.getControllers = function() {
return controllers;
// This will return an object of all the currently defined controllers
};
Then you can just do
app.config(function($controllerProvider) {
var myCtrl = $controllerProvider.getControllers()['myController'];
});
Since angular 1.5.1 (not released yet at the time of writing), there is a new way to check whether a controller exists or not through the $ControllerProvider.has('MyCtrlName') method.
Github issue: https://github.com/angular/angular.js/issues/13951
Github PR: https://github.com/angular/angular.js/pull/14109
Commit backported in 1.5.1 directly: https://github.com/angular/angular.js/commit/bb9575dbd3428176216355df7b2933d2a72783cd
Disclaimer: Since many people were interested by this feature, I made a PR, because I also need it is some of my projects. Have fun! :)
This PR has been based on #trekforever answer, thanks for the hint :)
You could use the $controller service and do $controller('myController') and wrap a try-catch arround it so you know if it fails.

Resources