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/
Related
I am familiarizing myself with AngularJS and making calls to a Web API backend and I get the error of
angular.js:13424 TypeError: Cannot read property 'query' of undefined
I have a productListController
(function ()
{
"use strict";
angular.module("productManagement").controller("ProductListController", ["productResource", ProductListController]);
function ProductListController($scope, productResource) {
productResource.query(function (data) {
$scope.products = data
});
}
})();
and I created a service called productResource
(function () {
"use strict";
angular.module("common.services").factory("productResource", ["$resource", "appSettings", productResource])
function productResource($resource, appSettings) {
return $resource(appSettings.serverPath + "/api/products/:id").query();
}
}());
The appSettings is a constant that specifies a path.
Why is query undefined?
Your controller is expecting the following dependencies to be injected:
$scope, productResource
and actually you are injecting only "productResource" instead of "$scope", "productResource"
Your controller should be initialised like this:
angular.module("productManagement").controller("ProductListController", ["$scope", "productResource", ProductListController]);
function ProductListController($scope, productResource) {
// ...
}
The names of the dependencies passed in need to be in sync with the parameters in the controller function declaration - at the moment when you are injecting "productResource" it is going into the $scope parameter.
I had this issue a while ago and it took me hours to figure it out.
In certain cases you need to make sure that you inject "productResource" and "$scope" and follow a specific order. Let me demonstrate:
angular.module("productManagement").controller("ProductListController", ["$scope", "productResource", ProductListController]);
function ProductListController($scope, productResource) {
// ...
}
The above code will always work, but if you switch positions as seen below, in some cases (as I mentioned above), it won't work.
angular.module("productManagement").controller("ProductListController", ["productResource","$scope", ProductListController]);
function ProductListController($scope, productResource) {
// ...
}
I never had this issue of making sure dependencies are injected in the correct order until today. If after all this your code is still not working then the issue is probably elsewhere.
I hope this helps.
I've looked at the documentation for angular.mock.module and a couple of examples of others using it but I seem to be running into an issue in my use-case that I don't understand.
I'm running Jasmine (2.4.1) tests with angular (1.4.9) and I have my angular app separated into multiple modules. When I attempt to mock out certain parts of my app for unit testing I want to mock out entire modules (or providers) so that I only expose the pieces I use.
Here is a very simple app that has a main module plunker which depends on plunker.service. plunker.service depends on plunker.constant.
var app = angular.module('plunker', ['plunker.service']);
app.controller('MainCtrl', function($scope, valueService, appService) {
$scope.init = function() {
$scope.appValue = valueService.getValue();
$scope.appIsRunning = appService.getStatus();
};
});
angular.module('plunker.service', ['plunker.constant'])
.service('appService', function(appSettings) {
var vm = this;
vm.getStatus = function () {
if (appSettings.isRunning) {
return true;
} else {
return false;
}
};
})
.service('valueService', function(valueSettings) {
var vm = this;
vm.getValue = function () {
return valueSettings.value;
}
});
angular.module('plunker.constant', [])
.constant('appSettings', { isRunning: true })
.constant('valueSettings', { value: 10 });
In my Jasmine tests I have a beforeEach() that registers my modules using module (aka angular.mock.module).
I have seen 3 ways of using module
string
function with $provide
object
You can see below that I use the module('plunker') (string) to register my main module and I have 3 ways of mocking out my appSettings constant (A, B, C). You will notice that the function with $provide.constant works fine but function with $provide.value does not and object does not.
beforeEach(function() {
module('plunker');
function useFunction(typeofProvider) {
module(function($provide) {
$provide[typeofProvider]('appSettings', { isRunning: false });
});
}
function useObject() {
module({
appSettings: { isRunning: false }
});
}
// A. THIS WORKS! //
useFunction('constant');
// B. THIS DOES NOT //
// useFunction('value');
// C. THIS ALSO DOES NOT!! //
// useObject();
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope
});
});
});
I have also seen people use the following syntax...
beforeEach(function() {
var mockService = function () {
var mockValue = 10;
this.value = mockValue;
};
// D.
module('a.module.name', function newProviders($provide){
$provide.service('realService', mockService);
});
});
My questions
In my test code, why does A. work but B. and C. do not?
Is D. equivalent to calling module('a.module.name'); followed by module(function newProviders($provide) { ... });? Does placing both in the same module() call have any special effects on how things are registered or is it just a shorthand? (based on the documentation it should be a shorthand)
Related to Jasmine, specifically, do all beforeEach() calls run in the same top-to-bottom order with every execution?
Here is my plunker for the above app and jasmine code
Thanks
This happens because of how Angular injector works. In fact, there are two different injectors in Angular. The one (available as $injector in config blocks) deals with service providers. Another one (available as $injector anywhere else) deals with service instances. Providers and instances are cached and stored internally.
$provide.constant('service') creates both provider and instance of name 'service' at call time.
All other types of services are lazily instantiated. They create 'serviceProvider' provider at call time, but 'service' instance is created on the first injection.
Since Angular service instance is a singleton, it refers to instance cache before the instantiation. If the instance is in the cache, it is reused and not instantiated. constant service instance is eagerly instantiated, so only another constant can override the instance.
Object properties in angular.mock.module are shortcuts for $provide.value, and useObject() equals to useFunction('value') in this example.
As long as module order stays the same,
module('a.module.name', function ($provide) { ... });
is indeed a shortcut for
module('a.module.name');
module(function ($provide) { ... });
Due to the fact that appSettings object isn't used in config blocks (the primary use of constant service), it is more convenient to make it value.
I'm running a single test on my controller to determine if it's properly defined but I keep getting a TypeError: undefined on the controller object. Here's the complete error:
Search Controller
should have the controller defined <<< FAILURE!
* TypeError: 'undefined' is not an object (evaluating 'myMenuDataLoad.then')
* Expected undefined to be defined.
And here is the controller to be tested:
myAppControllers.controller('VisibilitySearchController', ['$scope', 'headerService', 'menuService', 'navigationService', function($scope, headerService, menuService, navigationService ){
headerService.setTitle('My title');
var myMenuDataLoad = menuService.loadData('partials/common/components/menu-bar/json/menu-bar.json');
myMenuDataLoad.then(function(dataResult){
menuService.setData(dataResult.data);
});
var myNavDataLoad = navigationService.loadData('partials/common/components/navigation-bar/json/navigation-bar.json');
myNavDataLoad.then(function(dataResult){
navigationService.setData(dataResult.data);
});
}]);
I've initialized the controller by passing it everything it needs in its parameters i.e. scope, headerService, menuService and navigationService - I mock these services using the jasmine.createSpyObj method and pass in all the relevant methods ( the ones used on the controller ):
// Mock our services
beforeEach(function() {
// Methods are accepted as the 2nd second parameter
headerService = jasmine.createSpyObj('headerService', ['setTitle']);
module(function($provide) {
$provide.value('headerService', headerService);
});
menuService = jasmine.createSpyObj('menuService', ['loadData', 'setData']);
module(function($provide) {
$provide.value('menuService', menuService);
});
navigationService = jasmine.createSpyObj('navigationService', ['loadData', 'setData']);
module(function($provide) {
$provide.value('navigationService', navigationService);
});
});
And the actual initialization of the controller happens here:
beforeEach(inject(function($rootScope, $injector, $controller, _headerService_, _menuService_, _navigationService_) {
scope = $rootScope.$new();
// Instantiate the controller
searchController = $controller('VisibilitySearchController', {
$scope : scope,
headerService : headerService,
menuService : menuService,
navigationService : navigationService
});
}));
So what am I doing wrong here? Why isn't the test (see below) passing?
it("should have the controller defined", function() {
expect(searchController).toBeDefined();
});
Have I mocked the services correctly? What action needs to be done on a local controller variable in order to properly initialize them and the methods they are used in?
Thanks!
UPDATE
I've looked further into this but am unfortunately still receiving the same undefined error. When you create a mock object of a service do you have to provide that service with all of its dependencies and methods you make use of? For example:
menuService = jasmine.createSpyObj('menuService', ['$parse','$q', 'dataService', 'loadData', 'then']);
module(function($provide) {
$provide.value('menuService', menuService);
});
Here when I create the mock object I provide it with all the dependencies it would expect plus I added in two functions that I make use of in the controller.
So how do I go about mocking a function in a mocked object? I tried this but I'm still getting the same error:
menuService.loadData = jasmine.createSpy( 'loadData()' ).andReturn( data );
As mentioned in the comment your menuService.loadData() will always return undefined so evaluating expression myMenuDataLoad.then will always fail as mentioned in the error. What you must do is to provide an implementation of menuService.loadData which will return a promise. You can do the mocking the way you did it in case you want these method to be called but you don't rely on any return value of it. If you need the method to return something you can do define menuService this way:
var menuService = {
loadData: function() {
var deferred = $q.defer();
var data = []; //put any data you need here to be returned within the promise
deferred.resolve{data);
return deferred.promise;
}
}
module(function($provide) {
$provide.value('menuService', menuService);
});
You will need instance of $q which you can get in your inject call similarly to $rootScope, $injector etc.
In case you wanted to spy on menuService.load function you can do it this way:
spyOn(menuService, "loadData").andCallThrough()
That will keep your mocked implementation of the method but still allow you to assert it was called etc. I don't think you need it.
I have a decorator in Angular that is going to extend the functionality of the $log service and I would like to test it, but I don't see a way to do this. Here is a stub of my decorator:
angular.module('myApp')
.config(function ($provide) {
$provide.decorator('$log', ['$delegate', function($delegate) {
var _debug = $delegate.debug;
$delegate.debug = function() {
var args = [].slice.call(arguments);
// Do some custom stuff
window.console.info('inside delegated method!');
_debug.apply(null, args);
};
return $delegate
}]);
});
Notice that this basically overrides the $log.debug() method, then calls it after doing some custom stuff. In my app this works and I see the 'inside delegated method!' message in the console. But in my test I do not get that output.
How can I test my decorator functionality??
Specifically, how can I inject my decorator such that it actually decorates my $log mock implementation (see below)?
Here is my current test (mocha/chai, but that isn't really relevant):
describe('Log Decorator', function () {
var MockNativeLog;
beforeEach(function() {
MockNativeLog = {
debug: chai.spy(function() { window.console.log("\nmock debug call\n"); })
};
});
beforeEach(angular.mock.module('myApp'));
beforeEach(function() {
angular.mock.module(function ($provide) {
$provide.value('$log', MockNativeLog);
});
});
describe('The logger', function() {
it('should go through the delegate', inject(function($log) {
// this calls my mock (above), but NOT the $log decorator
// how do I get the decorator to delegate the $log module??
$log.debug();
MockNativeLog.debug.should.have.been.called(1);
}));
});
});
From the attached plunk (http://j.mp/1p8AcLT), the initial version is the (mostly) untouched code provided by #jakerella (minor adjustments for syntax). I tried to use the same dependencies I could derive from the original post. Note tests.js:12-14:
angular.mock.module(function ($provide) {
$provide.value('$log', MockNativeLog);
});
This completely overrides the native $log Service, as you might expect, with the MockNativeLog implementation provided at the beginning of the tests because angular.mock.module(fn) acts as a config function for the mock module. Since the config functions execute in FIFO order, this function clobbers the decorated $log Service.
One solution is to re-apply the decorator inside that config function, as you can see from version 2 of the plunk (permalink would be nice, Plunker), tests.js:12-18:
angular.mock.module('myApp', function ($injector, $provide) {
// This replaces the native $log service with MockNativeLog...
$provide.value('$log', MockNativeLog);
// This decorates MockNativeLog, which _replaces_ MockNativeLog.debug...
$provide.decorator('$log', logDecorator);
});
That's not enough, however. The decorator #jakerella defines replaces the debug method of the $log service, causing the later call to MockNativeLog.debug.should.be.called(1) to fail. The method MockNativeLog.debug is no longer a spy provided by chai.spy, so the matchers won't work.
Instead, note that I created an additional spy in tests.js:2-8:
var MockNativeLog, MockDebug;
beforeEach(function () {
MockNativeLog = {
debug: MockDebug = chai.spy(function () {
window.console.log("\nmock debug call\n");
})
};
});
That code could be easier to read:
MockDebug = chai.spy(function () {
window.console.log("\nmock debug call\n");
});
MockNativeLog = {
debug: MockDebug
};
And this still doesn't represent a good testing outcome, just a sanity check. That's a relief after banging your head against the "why don't this work" question for a few hours.
Note that I additionally refactored the decorator function into the global scope so that I could use it in tests.js without having to redefine it. Better would be to refactor into a proper Service with $provider.value(), but that task has been left as an exercise for the student... Or someone less lazy than myself. :D
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.