Why $rootScope.$new does not let template into the directive? - angularjs

I'm building unit testing using Karma and Mocha. Testing my directives, and using html2js (It converts the htmls to cached strings in $templateCache).
Interestingly, when using $rootScope.$new() in my test, the template html will not get into the directive . Here's the code:
it('should show a thumb name', function() {
inject(function($compile, $rootScope,$controller) {
var scope = $rootScope;//.$new() ($new not working. Why?)
var linkFn = $compile('<thumb></thumb>');
var element = linkFn(scope);
scope.$digest(); // <== needed so that $templateCache will bring the html
// (that html2js put in it)
console.log(element.html());// correctly returns thumb's directive templateUrl content
}));
...
However, if I use scope = $rootScope.$new(), the element.html() will return an empty string
any ideas?
many thanks
Lior

According to the docs for $digest (http://docs.angularjs.org/api/ng.$rootScope.Scope), this will only process watchers etc for the current scope and its children.
This suggests to me that when you set scope = $rootScope and then $digest you will be processing watchers etc on the $rootScope, I think this is where promises will be resolved too, releasing your templates. When you do scope = $rootScope.$new() and call $digest on that, I expect anything that should happen from the $rootScope doesn't happen.
So, does this work if you change scope.$digest() to $rootScope.$digest() or scope.$apply()?

Related

Angular $observe with $apply

Im currently observing attribute values using $observe inside a directive, as I change the value of the attribute the callback is called, when I then change a $scope variable it isn't "updated" and I figured this is because I need to use $apply, when invoking $apply I get the error $rootScope:inprog which seems to be because of the $observe?
Simply, how do I update a $scope variable from inside an $observe?
My code (Coffee):
App.directive "reactiveButton", () ->
directive = {}
directive.restrict = "E"
directive.scope =
translateId: '#translateId'
loadingIndicator: '#loadingIndicator'
directive.templateUrl = App.Base.concat("directive/reactive-button.html")
directive.link = ($scope, $elements, $attrs) ->
$attrs.$observe 'translateId', (value) ->
$scope.$apply () ->
$scope.translateId = value
$attrs.$observe 'loadingIndicator', (value) ->
$scope.loadingIndicator = Boolean value
return directive
Your $observe block is redundant in this example. You've already got an isolate scope that's set up to assign translateId to the scope, so you can remove it.
What do you mean by "isn't updated"?
Inside reactive-button.html, you should be able to see the value of translateId by putting this somewhere: {{translateId}}
^ That should change to reflect your attribute.
directive.scope =
translateId: '#translateId'
loadingIndicator: '#loadingIndicator'
This part tells AngularJS to create an isolated scope. AngularJS will create watchers for the attributes of this directive for those properties. It will update the scope whenever those attributes change. You don't have to observe them.
App.directive "reactiveButton", () ->
directive = {}
directive.restrict = "E"
directive.scope =
translateId: '#translateId'
loadingIndicator: '#loadingIndicator'
directive.templateUrl = App.Base.concat("directive/reactive-button.html")
return directive
You're directive doesn't need a link function, because there isn't anything else for it to do (at this point). The template reactive-button.html will still work with the translateId and loadingIndicator bindings.
You don't need to $apply your scope in an $observe listener: it will already be triggered during a digect cycle.
Updating as you did should be enough, please reproduce and explain us the problem you have when doing so.

Jasmine unit test for Angular directive

I have the following angular directive, which adds a tooltip when I hover over a span.
angular.module('mainMod')
.directive('toolTip', [function() {
return {
restrict: 'A',
scope: {
theTooltip: '#toolTip'
},
link: function(scope, element, attrs){
element.tooltip({
delay: 0,
showURL: false,
bodyHandler: function() {
return jQuery('<div class="hover">').text(scope.theTooltip);
}
});
}
}
}])
;
<span ng-show="data.tooltip" class="icon" tool-tip="{{data.tooltip}}"></span>
I'm looking to write a unit test for this directive, atm I can't use jasmine-jquery.
I'm fairly new to writing unit tests, could anyone possibly help me out?
Give me some pointers or point me towards some helpful resources?
Any advice or suggestions would be greatly appreciated.
What I have atm isn't much...
describe('Unit testing tooltip', function() {
var $compile;
var $rootScope;
// Load the myApp module, which contains the directive
beforeEach(module('mainMod'));
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(function(_$compile_, _$rootScope_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it(' ', function() {
// Compile a piece of HTML containing the directive
FAILS HERE --> var element = $compile("<span class='icon' tool-tip='{{data.tooltip}}'></span>")($rootScope);
$rootScope.$digest();
});
});
It's failing with a message of
TypeError: undefined is not a function
I think it's being caused by the ($rootScope) at the end of the line I've specified above.
You have to wrap your DOM content with angular.element first before compiling it. I am not sure what the tooltip module you are using but I used the jQuery UI tooltip instead.
//create a new scope with $rootScope if you want
$scope = $rootScope.$new();
var element = angular.element("<span class='icon' tool-tip='This is the tooltip data'></span>");
//use the current scope has just been created above for the directive
$compile(element)($scope);
One more thing, because you are using isolate scope in your directive, to get the current scope from your directive, you need to call
element.isolateScope()
based on this reference : How to Unit Test Isolated Scope Directive in AngularJS
For a working fiddle, you can found it here : http://jsfiddle.net/themyth92/4w52wsms/1/
Any unit test is basically the same - mock the environment, construct the unit, check that the unit interacts with the environment in the expected manner. In this instance, you'd probably want to mock the tooltip creator
spyOn(jQuery.fn, 'tooltip');
then compile some template using the directive (which you're already doing), then simulate the hover event on the compiled element and then check that the tooltip creator was called in the expected manner
expect(jQuery.fn.tooltip).toHaveBeenCalledWith(jasmine.objectContaining({
// ... (expected properties)
}));
How you simulate the event depends on how the element.tooltip is supposed to work. If it really works the way you're using it in the question code, you don't need to simulate anything at all and just check the expected interaction right after template compilation.

How to update the scope of an appended html element?

I would like to interact with a scoped inside an appended html element on the page, can someone please show me how to update that scope?
var overlay = angular.element('<div id="flyout-overlay" class="page-overlay global" ng-show="testScope"></div>');
mainContent.append(overlay);
$timeout(function(){
$scope.testScope = true; // how?
},500);
use $compile service
$compile(overlay);
here is the documentation
don't forget to add $compile dependency in the controller
doc says,
apply to your case,
'<div id="flyout-overlay" class="page-overlay global" ng-show="testScope"></div>'
1 : compile - $compile collect all the directives, for ex, it will collect ng-show directive
2: link - combine the directive with a scope.. , for ex, it will bind the ng-show="testScope" directive with the scope.
#Kalhano gave the perfect answer but since you don't know about the $compile service, here is the code for you. Just a small change....
var overlayTmpl = angular.element('<div id="flyout-overlay" class="page-overlay global" ng-show="testScope"></div>');
var overlay = $compile(overlayTmpl)($scope);
mainContent.append(overlay);
$timeout(function(){
$scope.testScope = true; // how?
},500);
in nutshell, $compile service compiles your html and links it to the scope you provided,
for better understanding read the angular documentation.

ngRepeat in compiled by $compile does not work

I need to dynamically compile html and pass it from function as text.
So, I have this code (simplified version for debugging purpose):
angular.module('app', [])
.run(function($rootScope, $compile){
var data = ["1","2"];
var html =
'<div>' +
'<ul>' +
'<li ng-repeat="score in data">{{score}}</li>' +
'</ul>'+
'</div>';
var el = angular.element(html);
$rootScope.data = data;
var result = $compile(el)($rootScope);
console.log(result.html());
})
The result is only:
<ul><!-- ngRepeat: score in data --></ul>
So, it looks like ngRepeat does not "repeat" "li" element.
Why?
JsFiddle: http://jsfiddle.net/yoorek/K4Cmk/
(I know DOM manipulation should be in directive etc. and I know how to do it other way but I need to understand why this does not work)
If you look at the angular source code, ngRepeat will manipulate the DOM and "repeat" the elements within the $watchCollection handler:
ngRepeat Link Function
When you manually compile and link the element from the run block, the $watch handlers for your element have been set up but the $digest phase has not happened yet. It is the $digest phase where the scope examines all of the $watch expressions and executes their corresponding watch handlers.
If you want to inspect the element after the $digest (render) phase you can use $timeout:
$timeout(function() {
console.log(el.html());
alert(el.html());
});
Demo Fiddle
I was wondering why the answer above does not work with $scope.$evalAsync() instead of $timeout.
I believe these explain:
1. http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm
2. AngularJS : $evalAsync vs $timeout

Karma get scope of Angular directive

I am using karma to load an angular directive (with the html2js plugin):
beforeEach(module('partials/myDir.html'));
beforeEach(inject(function($injector, $compile, $rootScope){
$gCompile = $compile;
$gScope = $rootScope;
}));
it("test test", function() {
element = $gCompile('<my-dir></my-dir>')($gScope);
$gScope.$digest();
console.log($gScope);
});
This all works fine, what I now want to do is access the directives scope from the $rootScope object injected in the beforeEach.
It depends upon your directive definition object. I don't see one in your question so I will answer for all three options.
Defining Scope or Child Scope:
This would be set by either doing scope: true or using the default scope value, which is false.
element.scope()
Isolate Scope:
This would be used if an Isolate Scope is created by your directive.
element.IsolateScope()

Resources