I have a directive which does something like this in its link function
angular.module('myApp')
.directive('barFoo', function() {
return {
restrict: 'E',
link: function (scope, element) {
element.on('click', ....);
}
};
});
Now I would like to verify in my unit test that it calls on correctly
element = angular.element('<bar-foo></bar-foo>');
$compile(element)(scope);
spyOn(element, 'on');
...
expect(element.on).toHaveBeenCalled();
It tells me the spy is not called. From what I've found on the web, angular/jquery creates a new wrapper around the DOM element every time. Meaning the the element inside my directive is not the same as the element in my spec file. Most likely (not verified) the element[0] probably are the same. I've also tried to spy on angular.element
var mockEl = { on: angular.noop };
spyOn(angular, 'element').andReturn(mockEl);
spyOn(mockEl, 'on');
but that seems to break more than it fixes (I also need functions like isloateScope for example).
Anyway, is there some easy way I can spy on the on function of the element used inside a directive?
Link function can be tested separately
element = angular.element('<bar-foo');
spyOn(element, 'on');
BarFooDirective[0].link(scope, element);
expect(element.on).toHaveBeenCalled();
if it is simple enough (stays away from attrs, required controllers, dependencies), otherwise the spec will cause more problems than it can solve.
Otherwise, it can be tested like it is done by the framework:
element = angular.element('<bar-foo');
expect(angular.element._data(element[0]).events.click).toBeDefined();
For real-world directive which may have more than one click listener defined in either itself or child directives it is not enough to make sure that listeners exist. Generally you may want to encapsulate internal functions, but anonymous click handler can also be exposed to scope for the purpose of testing:
element = angular.element('<bar-foo');
expect(angular.element._data(element[0]).events.click).toContain(scope.onClick);
Well, it's not necessary to test on method. You can test event bindings with the help of triggerHandler()
link: function (scope, element) {
element.on('click', 'something to heppen');
}
Test:
element = angular.element('<bar-foo></bar-foo>');
$compile(element)(scope);
$(element[0]).triggerHandler('click');
expect('something binded to click event to happen');
If you've compiled your element in in a beforeEach like this:
myEl = '<div my-el>MY EL</div>';
scope = $rootScope.$new();
directiveElement = $compile(myEl)(scope);
scope.$digest();
Try replacing mousedown with the event you are testing like this:
expect(directiveElement.on.mousedown).toBeDefined;
Related
I had to add a $destroy event listener to the element object in a directive from what I found with this answer Why isn't $destroy triggered when I call element.remove?
Resulting in a link function made with a scope/element....
controller: "MyCtrl",
link: function(scope, element) {
element.on("$destroy", function() {
scope.func();
});
}
Where func is a function defined in MyCtrl.
This works for what I want...but I'm having trouble testing the element.on("$destroy" event.
After injecting/mocking in my directive test, I create the element such like...
this.$compile = $injector.get("$compile");
this.$rootScope = $injector.get("$rootScope");
this.$scope = this.$rootScope.$new();
this.template = "<my-dir></my-dir>";
this.initElement = function() {
this.element = this.$compile(this.template)(this.$scope);
return this.element;
};
Trying to write a unit test, with destroying the scope. The element destroy event isn't triggered...and my this.element does not have a $destroy function it to call. So I'm not sure exactly how I trigger the element's $destroy event.
it("when element destroyed, call scope.func", function() {
this.$httpBackend.whenGET("app/my-dir.tpl.html").respond(200);
this.$scope.unsubscribeToMapMoveEvents = jasmine.createSpy("func");
this.initElement();
this.$scope.$destroy();
expect(this.$scope.func).toHaveBeenCalled();
});
I think the problem I'm facing in this unit test is the same reason why I moved this logic from the ctrl to the directives link function
Any help on how I can test this element on destroy workflow?
My solution was the directive that is defined using this controller is using a $rootScope passed in, and that was breaking it to where the destroy wouldn't kick off.
I instead changed the directive to be initialized with it's own empty scope like....
controller: "MyCtrl",
scope: {}
Currently working on a project where we found huge memory leaks when not clearing broadcast subscriptions off destroyed scopes. The following code has fixed this:
var onFooEventBroadcast = $rootScope.$on('fooEvent', doSomething);
scope.$on('$destroy', function() {
//remove the broadcast subscription when scope is destroyed
onFooEventBroadcast();
});
Should this practice also be used for watches? Code example below:
var onFooChanged = scope.$watch('foo', doSomething);
scope.$on('$destroy', function() {
//stop watching when scope is destroyed
onFooChanged();
});
No, you don't need to remove $$watchers, since they will effectively get removed once the scope is destroyed.
From Angular's source code (v1.2.21), Scope's $destroy method:
$destroy: function() {
...
if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
...
this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
...
So, the $$watchers array is emptied (and the scope is removed from the scope hierarchy).
Removing the watcher from the array is all the unregister function does anyway:
$watch: function(watchExp, listener, objectEquality) {
...
return function deregisterWatch() {
arrayRemove(array, watcher);
lastDirtyWatch = null;
};
}
So, there is no point in unregistering the $$watchers "manually".
You should still unregister event listeners though (as you correctly mention in your post) !
NOTE:
You only need to unregister listeners registered on other scopes. There is no need to unregister listeners registered on the scope that is being destroyed.
E.g.:
// You MUST unregister these
$rootScope.$on(...);
$scope.$parent.$on(...);
// You DON'T HAVE to unregister this
$scope.$on(...)
(Thx to #John for pointing it out)
Also, make sure you unregister any event listeners from elements that outlive the scope being destroyed. E.g. if you have a directive register a listener on the parent node or on <body>, then you must unregister them too.
Again, you don't have to remove a listener registered on the element being destroyed.
Kind of unrelated to the original question, but now there is also a $destroyed event dispatched on the element being destroyed, so you can hook into that as well (if it's appropriate for your usecase):
link: function postLink(scope, elem) {
doStuff();
elem.on('$destroy', cleanUp);
}
I would like to add too #gkalpak's answer as it lead me in the right direction..
The application I was working on created a memory leak by replacing directives whom had watches. The directives were replaced using jQuery and then complied.
To fix i added the following link function
link: function (scope, elem, attrs) {
elem.on('$destroy', function () {
scope.$destroy();
});
}
it uses the element destroy event to in turn destroy the scope.
I was looking at one of the custom implementations of ng-blur (I know it's already available in the standard AngularJS now). The last line is what I don't understand.
.controller('formController', function($scope){
$scope.formData = {};
$scope.myFunc = function(){
alert('mew');
console.log(arguments.length);
}
})
.directive('mew', function($parse){
return function(scope, element, attr){
var fn = $parse(attr['mew']);
element.bind('blur', function(event){
scope.$apply(function(){
fn(scope);
});
});
}
});
In the view there's a simple mew="myFunc()" applied to inputs.
My question is why are we passing the scope to the function in the very last line of the directive. I tried to make it work without that but it doesn't. What's actually happening?
Also this too works scope.$apply(attr.mew). Same reason or something different?
$parse only does just that, it parses the string passed in, you need to call the resulting function with the current scope because otherwise how else would it know which function to call?
scope.$apply works in the following manner:
The expression is executed using the $eval() method.
Any exceptions from the execution of the expression are forwarded to the $exceptionHandler service.
The watch listeners are fired immediately after the expression was executed using the $digest() method.
The reason scope.$apply(attr.mew) is due to the fact that it's doing all of the above. It is parsing, and then applying the result of the parse to the scope.
Another option is to use an isolate scope to bind your directive to the mew attr.
return {
scope: {
mew: '&'
},
link: function (scope, element, attr) {
var fn = scope.mew;
element.bind('blur', function (event) {
scope.$apply(function () {
fn();
});
});
}
}
Example
For this specific example it will work, but as you said, the blur is out of the digest loop. In most of the use cases the function will change data on one scope or another, and the digest loop should run and catch those changes.
I'm trying to make a unit test for angular directive. So this is the code:
describe('Test', function () {
var element,
scope;
// load the service's module
beforeEach(module('app'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope.$new();
element = angular.element('<div id="wow"></div>');
$compile(element)(scope);
element.scope().$apply();
}));
it('should', function () {
console.log(document.getElementById('wow'));
});
});
I got null in the console.
Of course this is just a test case. In real code getElementById is used by plugin which is wrapped with my directive, which is I'm trying to test.
So my question is: why the DOM element which is just compiled not be found by getElementById. If this is correct behavior, how can I avoid this mistake.
Thanks
You need to append the element in the document somewhere. Here's working plnk.
Only thing thing that I added:
angular.element(document.body).append(element);
The DOM element isn't found because jasmine never actually attaches it to the DOM. If you want to get the element, just refer to it as element. For example:
it('should', function() {
expect(element.attr('id')).toBe('wow');
});
You can attach the element to the DOM manually, but remember to remove it when you're done with your test (I use the afterEach blocks for this). Jasmine uses the same DOM for your entire test suite, so if you forget to remove an element, you could get false positives or other unexpected results in your tests from elements being in the DOM that you didn't expect to be there. In general, you shouldn't have to attach your element to the DOM unless you're testing certain interactions (blur works without attaching to the DOM, but focus doesn't, for example).
Is there a way to call an Angular function from a JavaScript function?
function AngularCtrl($scope) {
$scope.setUserName = function(student){
$scope.user_name = 'John';
}
}
I need the following functionality in my HTML:
jQuery(document).ready(function(){
AngularCtrl.setUserName();
}
The problem here is my HTML code is present when page is loaded and hence the ng directives in the html are not compiled. So I would like to $compile(jQuery("PopupID")); when the DOM is loaded.
Is there a way to call a Angular function on document ready?
Angular has its own function to test on document ready. You could do a manual bootstrap and then set the username:
angular.element(document).ready(function () {
var $injector = angular.bootstrap(document, ['myApp']);
var $controller = $injector.get('$controller');
var AngularCtrl = $controller('AngularCtrl');
AngularCtrl.setUserName();
});
For this to work you need to remove the ng-app directive from the html.
The answer above although correct, is an anti-pattern. In most cases when you want to modify the DOM or wait for the DOM to load and then do stuff (document ready) you don't do it in the controller but in he link function.
angular.module('myModule').directive('someDirective', function() {
return {
restrict: 'E',
scope: {
something: '='
},
templateUrl: 'stuff.html',
controller: function($scope, MyService, OtherStuff) {
// stuff to be done before the DOM loads as in data computation, model initialisation...
},
link: function (scope, element, attributes)
// stuff that needs to be done when the DOM loads
// the parameter element of the link function is the directive's jqlite wraped element
// you can do stuff like element.addClass('myClass');
// WARNING: link function arguments are not dependency injections, they are just arguments and thus need to be given in a specific order: first scope, then element etc.
}
};
});
In all honesty, valid use of $document or angular.element is extremely rare (unable to use a directive instead of just a controller) and in most cases you're better of reviewing your design.
PS: I know this question is old but still had to point out some best practices. :)