Unit testing controller triggering custom directive - angularjs

Suppose you have an angular+jasmine app and your controller looks something like this:
app.controller('MyController', function($scope) {
$scope.myFunc = function() {
$scope.myArray = [];
// after some operations here
myArray.push(new_obj);
}
$scope.someOtherArray = [];
}
Additionally, I have a custom directive that is triggered by every element in myArray. Something like this:
app.directive('myDirective', function() {
return {
// directive goes here
// and some definitions
link: function(scope, element, attrs) {
scope.someOtherArray.push(other_obj)
}
}
}
and finally, in the HTML:
<div myDirective ng-repeat="obj in myArray"></div>
So, every time I add an element to myArray, another directive element is created and $scope.someOtherArray is modified. That works pretty well, but I found that myFunc() and the properties of the controller are difficult to test:
controller = $controller('MyController', { $scope: mockScope });
SpyOn(mockScope, 'myFunc').and.callThrough();
// expectGET and expectPOST here.
mockScope.myFunc();
However, after running that code, there were some objects (dependent on the custom directive) that were not being updated after calling mockScope.myFunc(); (such as someOtherArray). Apparently, I need to compile the custom directives manually:
var element = '<div myDirective ng-repeat="obj in myArray"></div>';
element = $compile(element)(mockScope);
Well, that seems a bit awkward. I was expecting that after running mockScope.myFunc() every side effect would take place automatically. Otherwise, I need to consider every directive involved in the process and compile each of them.
Is there a better way?

Related

Html element with AngularJS attribute generate after init AngularJS

I've AngularJS issue when there are some elements(button) generate dynamically or generate after init AngularJS. Source code below shows the button not able to trigger the ng-click attribute. Any ideas that can trigger the AngularJS attribute after init the AngularJS. Thanks.
OperationFormatter: function (value, row) {
var operations = [];
operations.push('<button class="btn-mini glyphicon glyphicon-edit" ng-
click="myFunc()" title="Shared_Edit"></button>');
return operations.join(' ');
// This function only can execute after init the AngularJS due to certain
conditions.
}
$(function () {
MyCtrl();
})
function MyCtrl($scope) {
var app = angular.module("appAffiliateInfo", []);
app.controller("ctrlBankInfo", function ($scope) {
$scope.myFunc = function () {
alert('ok');
};
});
};
I assume you're trying to dynamically insert the generated HTML code in your template.
It cannot be done this way, as AngularJS already parsed your template at "runtime" and created all its watchers.
You have to use the $compile service, like so:
// scope: the scope containing the function called by ng-click
// htmlCode: your html code (as a string)
var newElem = $compile(htmlCode)(scope);
// use the proper code here, depending on what you want to achieve with the HTML element
document.body.appendChild(newElem[0]);
When "compiled" by $compile, the HTML code is parsed in order to create the required watchers. You must make sure that you provide the correct scope to $compile, or the function intended to ng-click will never be called.

AngularJS directive doesn't update scope value even with apply

I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}

How to make a variable available in directive when it is set in scope on the controller level?

The controller is below the directive. I am trying to get the language code for the appropriate translation. This is what does it on the controller level.
PaymentPage.get($scope.pageTag)
.success(function(response) {
$scope.product = response;
console.log($scope.product);
// get the language of the product assuming its a valid language code
var lang_string = $scope.product.language;
//take the first two letters of the language and honoring the caveat of en-f for example
var lang = lang_string.substring(0,2);
// Translate the page guideline
$translate.use(lang);
loadCampaigns($scope.product.language);
initialiseBraintree(braintreeApiKey);
});
However, there is a popover directive just above the controller. The directive looks something like this. I thought I'd be able to retrieve the language code using $scope.product.language but I realize that is happening in the controller, so the directive can't access it.
.directive('popover', function($translate) {
// language scope is being set afterwards, must be set before to make use of $translate.use
//$translate.use('de');
var mouseOffset = 10;
return {
link: function (scope, element, attrs) {
// Returns a promise and must be handled with a .then
$translate('HOVER_MESSAGE').then(function (popover) {
console.log(popover);
var popoverElement = angular.element('<div class="myPopover hide">' + popover + '</div>');
element.after(popoverElement);
scope.popoverElement = popoverElement;
});
Also, the directive loads before the controller so other hacks like hiding language code in hidden spans also failed.
I can't try this out just now, but you should be able to use the model to bridge that scope and time gap. If you bind an attribute of your directive to product.language (from $scope.product.language in your controller) then it will re-render the directive when the controller provides the language code, and it will be available in the attrs passed to the directive link().
Something like, in your HTML:
<button id="buyNowButton" type="submit" popover language="{{product.language}}">
In your directive:
return {
link: function (scope, element, attrs) {
var lang = attrs.language;
...

Angular dynamically created directive not executing

Plnkr sample: [http://plnkr.co/edit/jlMQ66eBlzaNSd9ZqJ4m?p=preview][1]
This might not be the proper "Angular" way to accomplish this, but unfortunately I'm working with some 3rd party libraries that I have limited ability to change. I'm trying to dynamically create a angular directive and add it to the page. The process works, at least in the sense where the directive element gets added to the DOM, HOWEVER it is not actually executed - it is just a dumb DOM at this point.
The relevant code is below:
<div ng-app="myModule">
<div dr-test="Static Test Works"></div>
<div id="holder"></div>
<a href="#" onclick="addDirective('Dynamic test works')">Add Directive</a>
</div>
var myModule = angular.module('myModule', []);
myModule.directive('drTest', function () {
console.log("Directive factory was executed");
return {
restrict: 'A',
replace: true,
link: function postLink(scope, element, attrs) {
console.log("Directive was linked");
$(element).html(attrs.drTest);
}
}
});
function addDirective(text){
console.log("Dynamically adding directive");
angular.injector(['ng']).invoke(['$compile', '$rootScope',function(compile, rootScope){
var scope = rootScope.$new();
var result = compile("<div dr-test='"+text+"'></div>")(scope);
scope.$digest();
angular.element(document.getElementById("holder")).append(result);
}]);
}
</script>
While appending the directive to DOM you need to invoke with your module as well in the injector, because the directive drTest is available only under your module, so while creating the injector apart from adding ng add your module as well. And you don't really need to do a scope apply since the element is already compile with the scope. You can also remove the redundant $(element).
angular.injector(['ng', 'myModule']).invoke(['$compile', '$rootScope',function(compile, rootScope){
var scope = rootScope.$new();
var result = compile("<div dr-test='"+text+"'></div>")(scope);
angular.element(document.getElementById("holder")).append(result);
}]);
Demo

Restarted initialization angular when updating DOM

I have page:
<div>111</div><div id="123" ng-init='foo=555'>{{foo}}</div>
in browser:
111
555
Code js refresh id=123 and get new html. I get:
<div id="123" ng-init='foo="444new"'><span>..</span><b>NEW TEXT<b> {{foo}}</div>
in browser
111
...NEW TEXT {{foo}}
I want get in browser:
111
...NEW TEXT 444new
Is it possible to re-run the initialization angular in this situation?
DEMO: jsfiddle.net/UwLQR
Solution for me: http://jsbin.com/iSUBOqa/8/edit - this BAD PRACTICE!
UPD two months later: My God, what nonsense I wrote. :(
See my notes in the included code and the live demo here (click).
The two reasons that angular will not register data-binding or directives are that the element isn't compiled, or the change happens outside of Angular. Using the $compile service, the compile function in directives, and $scope.$apply are the solutions. See below for usage.
Sample markup:
<div my-directive></div>
<div my-directive2></div>
<button id="bad-button">Bad Button!</button>
Sample code:
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.foo = '123!';
$scope.bar = 'abc!';
//this is bad practice! just to demonstrate!
var badButton = document.getElementById('bad-button');
badButton.addEventListener('click', function() {
//in here, the context is outside of angular, so use $apply to tell Angular about changes!
$scope.$apply($scope.foo = "Foo is changed!");
});
});
app.directive('myDirective', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
//when using a link function, you must use $compile on the element
var newElem = angular.element('<div>{{foo}}</div>');
element.append(newElem);
$compile(newElem)(scope);
//or you can use:
//$compile(element.contents())(scope);
}
};
});
app.directive('myDirective2', function($compile) {
return {
restrict: 'A',
compile: function(element, attrs) {
//compile functions don't have access to scope, but they automatically compile the element
var newElem = angular.element('<div>{{bar}}</div>');
element.append(newElem);
}
};
});
Update based on your comment
It makes me cringe to write this, but this is what you would need to make that code work.
var elem = document.getElementById('123');
elem.innerHTML = "<div ng-init=\"foo='qwe123'\">{{foo}}</div>";
$scope.$apply($compile(elem)($scope));
Just as I said, you need to compile the element AND, since that is in an event listener, you need to use $apply as well, so that Angular will know about the compile you're doing.
That said, if you're doing anything like this at all, you REALLY need to learn more about angular. Anything like that should be done via directives and NEVER with any direct DOM manipulation.
Try next:
$scope.$apply(function() {
// your js updates here..
});
or
$compile('your html here')(scope);
Look $compile example at bottom of page.

Resources