Why compile() runs only when $httpBackend.verifyNoOutstandingExpectation() invoked? - angularjs

I've got a directive which takes part of its template (set via templateUrl) and moves it into $rootElement (so this part becomes the last child of it). And it's defined like this:
.directive(...
return {
restrict: "E",
templateUrl: templUrl,
controller: "MyCtrl",
scope: {
},
compile: function (element, attrs) {
return {
post: function postLink(scope, iElement, iAttrs, controller) {
...
// move modal's div to the end of $rootElement so all modals will be put one after another INSTEAD OF MAKING A HIERARCHY
var modalElement = $("div.modal", iElement);
modalElement.appendTo($rootElement);
// compile and link the selector's modal
$compile(modalElement)(scope);
}
};
}
};
}]);
In my jasmine test I have to invoke $httpBackend.verifyNoOutstandingExpectation() in order the test to work properly:
it("Element must be replaced by the template", function () {
var linkingFn = $compile("<div ng-app><mydir></mydir></div>");
$httpBackend.verifyNoOutstandingExpectation(); // DOES NOT WORK WITHOUT IT
var scope = $rootScope.$new();
var element = linkingFn(scope);
// element - is <mydir></mydir>
var buttonElm = $("div:first-child", element);
expect(buttonElm).not.toBeUndefined();
expect(buttonElm.hasClass("form-group")).toBe(true);
});
If I comment out verifyNoOutstandingExpectation() directive's postLink function gets invoked after all my expect statements. I don't understand why. Any ideas are welcome!

Related

Call method in controller from directive

HTML :
<div id="idOfDiv" ng-show="ngShowName">
Hello
</div>
I would like to call the function which is declared in my controller from my directive.
How can I do this? I don't receive an error when I call the function but nothing appears.
This is my directive and controller :
var d3DemoApp = angular.module('d3DemoApp', []);
d3DemoApp.controller('mainController', function AppCtrl ($scope,$http, dataService,userService,meanService,multipartForm) {
$scope.testFunc = function(){
$scope.ngShowName = true;
}
});
d3DemoApp.directive('directiveName', [function($scope) {
return {
restrict: 'EA',
transclude: true,
scope: {
testFunc : '&'
},
link: function(scope) {
node.on("click", click);
function click(d) {
scope.$apply(function () {
scope.testFunc();
});
}
};
}]);
You shouldn't really be using controllers and directives. Angularjs is meant to be used as more of a component(directive) based structure and controllers are more page centric. However if you are going to be doing it this way, there are two ways you can go about it.
First Accessing $parent:
If your directive is inside the controllers scope you can access it using scope.$parent.mainController.testFunc();
Second (Preferred Way):
Create a service factory and store your function in there.
d3DemoApp.factory('clickFactory', [..., function(...) {
var service = {}
service.testFunc = function(...) {
//do something
}
return service;
}]);
d3DemoApp.directive('directiveName', ['clickFactory', function(clickFactory) {
return {
restrict: 'EA',
transclude: true,
link: function(scope, elem) {
elem.on("click", click);
function click(d) {
scope.$apply(function () {
clickFactory.testFunc();
});
}
};
}]);
Just a tip, any time you are using a directive you don't need to add $scope to the top of it. scope and scope.$parent is all you really need, you will always have the scope context. Also if you declare scope :{} in your directive you isolate the scope from the rest of the scope, which is fine but if your just starting out could make things quite a bit more difficult for you.
In your link function you are using node, which doesn't exist. Instead you must use element which is the second parameter to link.
link: function(scope, element) {
element.on("click", click);
function click(d) {
scope.$apply(function() {
scope.testFunc();
});
}

AngularJS - dynamically created directive's "require" not working

The "require" option does not work if the directive is dynamically created, thus it cannot reference its parents' controllers. How can I make it work?
app.directive('parent', function ($compile) {
return {
controller: function() {},
link: function (scope, el, attrs) {
// "child" is dynamically created
el.append( $compile('<div child>')(scope) );
}
}
})
.directive('child', function () {
return {
require: "?^parent",
link: function(scope, el, attrs, pCtrl) {
// "child" cannot find its parent controller
console.log("pCtrl is undefined: ", pCtrl);
}
}
})
here is a plunker DEMO
you need to add child element to parent element before compiling it.
When directive compiles, its tries to get its parent element. And from parent element it tries to find parent controller. But you are compiling your child directive before appending its element to its parent element.
I have created a plnkr for you. Checkout this
app.directive('parent1', function($compile, $timeout) {
return {
controller: function() {
this.name = 'parent controller 1';
},
link: function(scope, el, attrs) {
// "child1" is dynamically created
var elmChild = angular.element('<div child1>');
el.append(elmChild);
$compile(elmChild)(scope);
}
}
})

AngularJS unit testing directive with ngModel

I have made a directive which uses ngModel:
.directive('datetimepicker', function () {
return {
restrict: 'E',
require: ['datetimepicker', '?^ngModel'],
controller: 'DateTimePickerController',
replace: true,
templateUrl: ...,
scope: {
model: '=ngModel'
},
link: function (scope, element, attributes, controllers) {
var pickerController = controllers[0];
var modelController = controllers[1];
if (modelController) {
pickerController.init(modelController);
}
}
}
});
But when testing...
var scope, element;
beforeEach(module('appDateTimePicker'));
beforeEach(module('templates'));
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile;
scope = $rootScope;
scope.model = new Date();
element = compile(angular.element('<datetimepicker ng-model="model"></datetimepicker>'))(scope);
scope.$digest();
}));
I can't anyhow set value to ng-model.
Fo example, here scope.model is a date, so scope.year and scope.month should be date and year of that model, but it is undefined.
As seen in the directive's code, I'm using this.init on the controller to initialise all the process.
What am I missing?
EDIT
Example of test:
it('should test', function () {
expect(scope.model).toBe(undefined);
expect(scope.year).toBe(undefined);
});
EDIT
This helped to solve the problem: http://jsfiddle.net/pTv49/3/
The '?^ngModel' mean you are asking for the ng-model on parent elements, but the html in your test has the ng-model on the same element as datetimepicker directive.
If the ng-model really have to be on parent elements, you have to change the html in the test, for example:
element = compile(angular.element('<div ng-model="model"><datetimepicker></datetimepicker></div>'))(scope);
But if it should be on the same element, just remove the ^ symbol in the require:
require: ['datetimepicker', '?ngModel'],
The directive has a scope: {} block so it creates an isolate scope. I assume, in the tests scope refers to the outer scope, while element.isolateScope() should be used to reference the inner scope instead.

directive scope variables not accessible in jasmine test

I have a directive like below:
angular.module('buttonModule', []).directive('saveButton', [
function () {
function resetButton(element) {
element.removeClass('btn-primary');
}
return {
restrict: 'E',
replace: 'false',
scope: {
isSave: '='
},
template:
'<button class="btn" href="#" style="margin-right:10px;" ng-disabled="!isSave">' +
'</button>',
link: function (scope, element) {
console.log(scope.isSave);
scope.$watch('isSave', function () {
if (scope.isSave) {
resetButton(scope, element);
}
});
}
};
}
]);
and the jasmine test as below:
describe('Directives: saveButton', function() {
var scope, compile;
beforeEach(module('buttonModule'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
compile = $compile;
}));
function createDirective() {
var elem, compiledElem;
elem = angular.element('<save-button></save-button>');
compiledElem = compile(elem)(scope);
scope.$digest();
return compiledElem;
}
it('should set button clean', function() {
var el = createDirective();
el.scope().isSaving = true;
expect(el.hasClass('btn-primary')).toBeFalsy();
});
});
The issue is the value of isSaving is not getting reflected in the directive and hence resetButton function is never called. How do i access the directive scope in my spec and change the variable values. i tried with isolateScope but the same issue persists.
First note that you are calling the resetButton function with two arguments when it only accepts one. I fixed this in my example code. I also added the class btn-primary to the button element to make the passing of the test clearer.
Your directive is setting up two-way databinding between the outer scope and the isolated scope:
scope: {
isDirty: '=',
isSaving: '='
}
You should leverage this to modify the isSaving variable.
Add the is-saving attribute to your element:
elem = '<save-button is-saving="isSaving"></save-button>';
Then modify the isSaving property of the scope that was used when compiling (you also need to trigger the digest loop to make the watcher detect the change):
var el = createDirective();
scope.isSaving = true;
scope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
Demo: http://plnkr.co/edit/Fr08guUMIxTLYTY0wTW3?p=preview
If you don't want to add the is-saving attribute to your element and still want to modify the variable you need to retrieve the isolated scope:
var el = createDirective();
var isolatedScope = el.isolateScope();
isolatedScope.isSaving = true;
isolatedScope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
For this to work however you need to remove the two-way binding to isSaving:
scope: {
isDirty: '='
}
Otherwise it would try to bind to something non-existing as there is no is-saving attribute on the element and you would get the following error:
Expression 'undefined' used with directive 'saveButton' is
non-assignable!
(https://docs.angularjs.org/error/$compile/nonassign?p0=undefined&p1=saveButton)
Demo: http://plnkr.co/edit/Ud6nK2qYxzQMi6fXNw1t?p=preview

unit testing angularjs directive

I would like to start doing unit testing for my angularjs project. That's far from being straight forward, I find it really difficult. I'm using Karma and Jasmine. For testing my routes and the app dependencies, I'm fine. But how would you test a directive like this one ?
angular.module('person.directives', []).
directive("person", function() {
return {
restrict: "E",
templateUrl: "person/views/person.html",
replace: true,
scope: {
myPerson: '='
},
link: function (scope, element, attrs) {
}
}
});
How would I test for instance that the template was found ?
Here is the way to go https://github.com/vojtajina/ng-directive-testing
Basically, you use beforeEach to create, compile and expose an element and it's scope, then you simulate scope changes and event handlers and see if the code reacts and update elements and scope appropriately. Here is a pretty simple example.
Assume this:
scope: {
myPerson: '='
},
link: function(scope, element, attr) {
element.bind('click', function() {console.log('testes');
scope.$apply('myPerson = "clicked"');
});
}
We expect that when user clicks the element with the directive, myPerson property becomes clicked. This is the behavior we need to test. So we expose the compiled directive (bound to an element) to all specs:
var elm, $scope;
beforeEach(module('myModule'));
beforeEach(inject(function($rootScope, $compile) {
$scope = $rootScope.$new();
elm = angular.element('<div t my-person="outsideModel"></div>');
$compile(elm)($scope);
}));
Then you just assert that:
it('should say hallo to the World', function() {
expect($scope.outsideModel).toBeUndefined(); // scope starts undefined
elm.click(); // but on click
expect($scope.outsideModel).toBe('clicked'); // it become clicked
});
Plnker here. You need jQuery to this test, to simulate click().

Resources