Decorator for Scope - angularjs

Is it possible, and if so how, to decorate $scope so all scopes have some extra function / property?
I'm trying to do this:
$provide.decorator('$scope', function($scope)
{
$scope.cakes = true;
return $scope;
});
But it explodes with:
Unknown provider: $scopeProvider from App.
I know I can add properties and functions to the $rootScope and it will prototypically inherit, but I want isolated scopes in directives to also have access to these added things.

I had the same problem.
Just extend $rootScope prototype.
Then isolated scopes will also have this method.
This is my attempt to use lodash debounce function as native scope method:
angular.module('Test', [])
.config(function($provide) {
$provide.decorator('$rootScope', function ($delegate) {
$delegate.__proto__.$$busy = 0;
$delegate.__proto__.$watchDebounce = function (watchExpression, listener, objectEquality){
var _scope = this;
var debouncedListener = _.debounce(function (newValue, oldValue, scope){
listener(newValue, oldValue, scope);
_scope.$$busy = 0;
scope.$digest();
}, 1000);
var wrappedListener = function (newValue, oldValue, scope){
_scope.$$busy = 1;
debouncedListener(newValue, oldValue, scope);
}
return this.$watch(watchExpression, wrappedListener, objectEquality);
}
return $delegate;
})
})
Working example here http://jsfiddle.net/3ncct/

It doesn't seem possible, but I'd say that's the whole point of an isolate scope.
What you can do is access decorated stuff via scope.$root, however for many use cases it will defeat the purpose because the added functions will still only access the $rootScope instead of your isolated one.

Related

Intercept the creation of all scopes

I want to add a destroy listener to all my scopes so I can do some custom cleanup things. I don't want to add $scope.$on('$destroy') to all my controllers so I found the $provide.decorator method and tried this:
angular.module('app').config(['$provide', function($provide) {
$provide.decorator('$rootScope', ['$delegate', decorateScope]);
function decorateScope(scope) {
var oldNew = scope.$new.bind(scope);
scope.$new = function() {
var newScope = oldNew(arguments);
console.log('Scope created');
newScope.$on('$destroy', function() {
console.log('Scope destroyed');
});
return decorateScope(newScope);
};
return scope;
}
}
]);
this will recursively intercept the creation of all scopes. The console logs the creation and destruction of all scopes. but does not work as before, the scope inheritance chain seems somehow broken.
How could this be solved?

How to add a function to all scopes..

I have a function on rootScope that I defined.
I later use it in controllers on the controller's scope - as it inherits the function from the rootScope.
now, I would like to do the same in directives with isolatedScopes.
since they don't prototypically inherit from rootScope, I need to find a another way to place that function on each isolated scope.
is there a way to customize isolated scope on creation, or some other hook?
update
Clarification: i need the function on the scope - not rootScope.
the reason is that we use this.$on('$destroy'...). so we basically want to execute code each time my scope is destroyed.
Current implementation: currently i override $rootScope.$new and check, if isolated i simply add the function on the scope. however this feels hackish and i would like to find a better solution.
here is my current code
$rootScope.registerTask = function(){
....
this.$on("$destroy", function() {
...
});
};
$rootScope.origNew = $rootScope.$new;
$rootScope.$new = function(isolate, parent ){
var newScope = this.origNew(isolate, parent );
if ( isolate ){
newScope.unregisterTask = ...;
newScope.registerTask = $rootScope.registerTask;
}
return newScope;
};
$rootScope.unregisterTask = ...;
Ahh.. the answer has been in front of me all along.. because the question was wrong!
All I wanted to do is assign functions on each and every scope (including isolated).
Well, scopes have prototypical inheritance, then just use that!
var Scope = Object.getPrototypeOf($rootScope);
Scope.registerTask = function(){
....
this.$on("$destroy", function() {
...
});
};
Scope.unregisterTask = ...;
should do the charm.. anywhere on load..
just goes to show - don't write code under pressure, you will work harder and get crappy results.. :)
I think you can inject the $rootScope in to the directive as,
app.directive('testDirective', ['$rootScope', function($rootScope) {
return {
scope : {},
link : function() {
},
controller : ['$scope', function($scope) {
$rootScope.rootScopeFunc();
}]
}
}]);
here is a DEMO

How to access another scope with controller?

I know that you can create binding with a directive but is it possible with a controller? Maybe this is stupidly simple but I couldn't figure out.
For example I would like to click on a text in x scope's view and execute a function in another scope.
function firstCtrl() {
var vm = this;
vm.data = 'First scope stuff!';
}
function secondCtrl() {
var vm = this;
vm.data = 'Second scope stuff!';
}
vm.clicked = function() {
alert('firstScope is clicked!');
};
Here is what I have: http://plnkr.co/edit/CpPKsEGPZWzufB2K1FK3
There are multiple ways how to communicate between controllers.
One way is emitting an event on scope:
function FirstCtrl($scope)
{
$scope.$on('someEvent', function(event, args) {});
// another controller or even directive
}
function SecondCtrl($scope)
{
$scope.$emit('someEvent', args);
}
Another way sharing a service.
Hope it helps. Also check this https://stackoverflow.com/a/9407953/4782034

Spy on scope function that executes when an angular controller is initialized

I want to test that the following function is in fact called upon the initialization of this controller using jasmine. It seems like using a spy is the way to go, It just isn't working as I'd expect when I put the expectation for it to have been called in an 'it' block. I'm wondering if there is a special way to check if something was called when it wasn't called within a scope function, but just in the controller itself.
App.controller('aCtrl', [ '$scope', function($scope){
$scope.loadResponses = function(){
//do something
}
$scope.loadResponses();
}]);
//spec file
describe('test spec', function(){
beforeEach(
//rootscope assigned to scope, scope injected into controller, controller instantiation.. the expected stuff
spyOn(scope, 'loadResponses');
);
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function(){
expect(scope.loadResponses).toHaveBeenCalled();
});
});
You need to initialise the controller yourself with the scope you've created. The problem is, that you need to restructure your code. You can't spy on a non-existing function, but you need to spyOn before the function gets called.
$scope.loadResponses = function(){
//do something
}
// <-- You would need your spy attached here
$scope.loadResponses();
Since you cannot do that, you need to make the $scope.loadResponses() call elsewhere.
The code that would successfully spy on a scoped function is this:
var scope;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', {$scope: scope});
scope.$digest();
}));
it("should have been called", function() {
spyOn(scope, "loadResponses");
scope.doTheStuffThatMakedLoadResponsesCalled();
expect(scope.loadResponses).toHaveBeenCalled();
});
Setting the spy before controller instantiation (in the beforeEach) is the way to test controller functions that execute upon instantiation.
EDIT: There is more to it. As a comment points out, the function doesn't exist at the time of ctrl instantiation. To spy on that call you need to assign an arbitrary function to the variable (in this case you assign scope.getResponses to an empty function) in your setup block AFTER you have scope, but BEFORE you instantiate the controller. Then you need to write the spy (again in your setup block and BEFORE ctrl instantiation), and finally you can instantiate the controller and expect a call to have been made to that function. Sorry for the crappy answer initially
The only way I have found to test this type of scenarios is moving the method to be tested to a separate dependency, then inject it in the controller, and provide a fake in the tests instead.
Here is a very basic working example:
angular.module('test', [])
.factory('loadResponses', function() {
return function() {
//do something
}
})
.controller('aCtrl', ['$scope', 'loadResponses', function($scope, loadResponses) {
$scope.loadResponses = loadResponses;
$scope.loadResponses();
}]);
describe('test spec', function(){
var scope;
var loadResponsesInvoked = false;
var fakeLoadResponses = function () {
loadResponsesInvoked = true;
}
beforeEach(function () {
module('test', function($provide) {
$provide.value('loadResponses', fakeLoadResponses)
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', { $scope: scope });
});
});
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
expect(loadResponsesInvoked).toBeTruthy();
});
});
For real world code you will probably need extra work (for example, you may not always want to fake the loadResponses method), but you get the idea.
Also, here is a nice article that explains how to create fake dependencies that actually use Jasmine spies: Mocking Dependencies in AngularJS Tests
EDIT: Here is an alternative way, that uses $provide.delegate and does not replace the original method:
describe('test spec', function(){
var scope, loadResponses;
var loadResponsesInvoked = false;
beforeEach(function () {
var loadResponsesDecorator = function ($delegate) {
loadResponsesInvoked = true;
return $delegate;
}
module('test', function($provide) {
$provide.decorator('loadResponses', loadResponsesDecorator);
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', { $scope: scope });
});
});
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
expect(loadResponsesInvoked).toBeTruthy();
});
});
I didn't quite understand any of the answers above.
the method I often use - don't test it, instead test the output it makes..
you have not specified what loadResponses actually does.. but lets say it puts something on scope - so test existence of that..
BTW - I myself asked a similar question but on an isolated scope
angular - how to test directive with isolatedScope load?
if you still want to spy - on an unisolated scope, you could definitely use a technique..
for example, change your code to be
if ( !$scope.loadResponses ){
$scope.loadResponses = function(){}
}
$scope.loadResponses();
This way you will be able to define the spy before initializing the controller.
Another way, is like PSL suggested in the comments - move loadResponses to a service, spy on that and check it has been called.
However, as mentioned, this won't work on an isolated scope.. and so the method of testing the output of it is the only one I really recommend as it answers both scenarios.

bindToController in unit tests

I'm using bindToController in a directive to have the isolated scope directly attached to the controller, like this:
app.directive('xx', function () {
return {
bindToController: true,
controller: 'xxCtrl',
scope: {
label: '#',
},
};
});
Then in the controller I have a default in case label is not specified in the HTML:
app.controller('xxCtrl', function () {
var ctrl = this;
ctrl.label = ctrl.label || 'default value';
});
How can I instantiate xxCtrl in the Jasmine unit tests so I can test the ctrl.label?
describe('buttons.RemoveButtonCtrl', function () {
var ctrl;
beforeEach(inject(function ($controller) {
// What do I do here to set ctrl.label BEFORE the controller runs?
ctrl = $controller('xxCtrl');
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
Check this to test the issue
In Angular 1.3 (see below for 1.4+)
Digging into the AngularJS source code I found an undocumented third argument to the $controller service called later (see $controller source).
If true, $controller() returns a Function with a property instance on which you can set properties.
When you're ready to instantiate the controller, call the function and it'll instantiate the controller with the properties available in the constructor.
Your example would work like this:
describe('buttons.RemoveButtonCtrl', function () {
var ctrlFn, ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrlFn = $controller('xxCtrl', {
$scope: scope,
}, true);
}));
it('should have a label', function () {
ctrlFn.instance.label = 'foo'; // set the value
// create controller instance
ctrl = ctrlFn();
// test
expect(ctrl.label).toBe('foo');
});
});
Here's an updated Plunker (had to upgrade Angular to make it work, it's 1.3.0-rc.4 now): http://plnkr.co/edit/tnLIyzZHKqPO6Tekd804?p=preview
Note that it's probably not recommended to use it, to quote from the Angular source code:
Instantiate controller later: This machinery is used to create an
instance of the object before calling the controller's constructor
itself.
This allows properties to be added to the controller before the
constructor is invoked. Primarily, this is used for isolate scope
bindings in $compile.
This feature is not intended for use by applications, and is thus not
documented publicly.
However the lack of a mechanism to test controllers with bindToController: true made me use it nevertheless.. maybe the Angular guys should consider making that flag public.
Under the hood it uses a temporary constructor, we could also write it ourselves I guess.
The advantage to your solution is that the constructor isn't invoked twice, which could cause problems if the properties don't have default values as in your example.
Angular 1.4+ (Update 2015-12-06):
The Angular team has added direct support for this in version 1.4.0. (See #9425)
You can just pass an object to the $controller function:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
}, {
label: 'foo'
});
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
See also this blog post.
Unit Testing BindToController using ES6
If using ES6,you can import the controller directly and test without using angular mocks.
Directive:
import xxCtrl from './xxCtrl';
class xxDirective {
constructor() {
this.bindToController = true;
this.controller = xxCtrl;
this.scope = {
label: '#'
}
}
}
app.directive('xx', new xxDirective());
Controller:
class xxCtrl {
constructor() {
this.label = this.label || 'default value';
}
}
export default xxCtrl;
Controller Test:
import xxCtrl from '../xxCtrl';
describe('buttons.RemoveButtonCtrl', function () {
let ctrl;
beforeEach(() => {
xxCtrl.prototype.label = 'foo';
ctrl = new xxCtrl(stubScope);
});
it('should have a label', () => {
expect(ctrl.label).toBe('foo');
});
});
see this for more information:
Proper unit testing of
Angular JS
applications with ES6 modules
In my view, this controller is not meant to be tested in isolation, because it will never work in isolation:
app.controller('xxCtrl', function () {
var ctrl = this;
// where on earth ctrl.lable comes from???
ctrl.newLabel = ctrl.label || 'default value';
});
It is tightly coupled with the directive relying on receiving its scope properties. It is not re-usable. From looking at this controller, I have to wonder where this variable is coming from. It is no better than a leaky function internally using a variable from outside scope:
function Leaky () {
... many lines of code here ...
// if we are here we are too tired to notice the leakyVariable:
importantData = process(leakyVariable);
... mode code here ...
return unpredictableResult;
}
Now I have a leaky function whose behaviour is highly unpredictable based on the variable leakyVariable present (or not) in whatever scope the function is called.
Unsurprisingly this function is nightmare to test. Which is actually a good thing, perhaps to force the developer to rewrite the function into something more modular and re-usable. Which is not hard really:
function Modular (outsideVariable) {
... many lines of code here ...
// no need to hit our heads against the wall to wonder where the variable comes from:
importantData = process(outsideVariable);
... mode code here ...
return predictableResult;
}
No leaky issues and really easy to test and re-use. Which to me tells that using the good old $scope is a better way:
app.controller('xxCtrl', function ($scope) {
$scope.newLabel = $scope.label || 'default value';
});
Simple, short and easy to test. Plus no bulky directive object definition.
The original reasoning behind the controllerAs syntax was the leaky scope inherited from the parent. However, directive's isolated scope already solves this problem. Thus I don't see any reason to use the bulkier leaky syntax.
I've found a way that is not particulary elegant but works at least (if there's a better option leave a comment).
We set the value that "comes" from the directive, and then we call the controller function again to test whatever it does. I've made a helper "invokeController" to be more DRY.
For example:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
});
}));
it('should have a label', function () {
ctrl.label = 'foo'; // set the value
// call the controller again with all the injected dependencies
invokeController(ctrl, {
$scope: scope,
});
// test whatever you want
expect(ctrl.label).toBe('foo');
});
});
beforeEach(inject(function ($injector) {
window.invokeController = function (ctrl, locals) {
locals = locals || {};
$injector.invoke(ctrl.constructor, ctrl, locals);
};
}));

Resources