ngRepeat in compiled by $compile does not work - angularjs

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

Related

Angular dynamic templating with compile VS template function?

I already know what is the purpose of each item in : compile vs link(pre/post) vs controller
So let's say I have this simple code :
HTML
<body ng-controller="mainController">
{{ message }}
<div otc-dynamic=""></div>
</body>
Controller
app.controller("mainController", function($scope) {
$scope.label = "Please click";
$scope.doSomething = function() {
$scope.message = "Clicked!";
};
});
Directive
app.directive("otcDynamic", function($compile) {
var template = "<button ng-click='doSomething()'>{{label}}</button>";
return {
compile: function(tElement, tAttributes) {
angular.element(tElement).append(template);
for (var i = 0; i < 3; i++) {
angular.element(tElement).append("<br>Repeat " + i + " of {{name}}");
}
return function postLink(scope, element, attrs) {
scope.name = "John";
}
}
}
});
So as we can see , I modify the template (at the compile function - which is where it should be actually)
Result ( plnker):
But
I didn't know that template:... can also take a function.
So I could use the template function instead (plunker) :
app.directive("otcDynamic", function() {
var template1 = "<button ng-click='doSomething()'>{{label}}</button>";
return {
template: function(element, attr) {
element.append(template1);
for (var i = 0; i < 3; i++)
element.append("<br>Repeat " + i + " of {{name}}");
},
link: function(scope, element) {
scope.name = "John";
}
}
});
Question
If so - When should I use the template function vs compile function ?
Let me try to explain what I understood so far.
Directives is a mechanism to work with DOM in Angular. It gives you leverage of playing with DOM element and it's attribute. So it also gives you callbacks to make your work easy.
template , compile and link are those examples. Since your question is specific with compile and template I would like to add about link as well.
A) Template
Like it state, it is a bunch of HTML tags or files to represent it on DOM directly as the face of your directive.
Template can be a file with specific path or inline HTML in code. Like you stated above. template can be wrap in function but the sole use of template is the final set of HTML which will be placed on DOM. Since you have the access to element and its attributes, you can perform as many DOM operation here as well.
B) Compile
Compile is a mechanism in directive which compiles the template HTML or DOM to do certain operation on it and return final set of HTML as template. Like given in Angular DOC
Compiles an HTML string or DOM into a template and produces a template function, which can then be used to link scope and the template together.
Which clearly says that, this is something on top of template. Now like I said above you can achieve similar operations in template as well but when we have methods for its sole purpose, you should use them for the sake of best practice.
You can read more here https://docs.angularjs.org/api/ng/service/$compile
C) Link
Link is used to register listeners like $watch, $apply etc to link your template with Angular scope so that it will get binded with module. When you place any directive inside controller, the flow of scope goes through the link that means the scope is directly accessible in link. Scope is sole of angular app and thus it gives you advantage of working with actual model. Link is also useful in dom manipulations and can be used to work with any DOM element using jQlite
So collecting all above in one
1. Template is the primary source of DOM or HTML to directive. it can be a file or inline HTML.
2. Compile is the wrapper to compile HTML into final template. It is used to gather all the HTML element and attribute to create template for directive.
3. Link is the listener wrapper for various scope and watchers. It binds scope of current controller with html of template and also do manipulation around it.
Hope this helps a bit to understand. Thanks

Angular: add ng-click inside directive

In my directive i using appendTo and ng-click inside it:
$("<div>" + "<a href='' ng-click='addIt()' + user + "</a></div>").appendTo '.user-class'
And ng-click doesn't work. Should i use $compile? How to use it in that case?
this code is a part of directive that make dropdown menu from json. There is no option to move this to directive template, so need to find solution how to make 'ng-click' works here.
Like Matthew, I am most curious as to which part(s) cannot be moved out into an external template / internal template function. It doesn't make much sense to me to have to re $compile a directive only to add a ng-click handler.
The senseful way would be to add the ng-click directive to a template. Either an external one, or a function returning a string value in the directive definition object.
Running $compile after the link step has finished is not a good idea performance wise, seeing as how it recompiles the entire DOM element your directive is attached to. Avoid doing so for as long as you possibly can.
.directive('myDir', function () {
return {
template: function (/*element, attrs*/) {
return '<div>{{::user}}</div>';
},
link: function (scope, el, attrs) {
scope.user = 'John';
}
};
});
This would result in the following DOM structure:
<div>
John
</div>
jsbin
Yes you will need $compile , inject $compile in directive and you can do it like this :
var link = $compile("<div>" + "<a href='' ng-click='addIt()'" + user + "</a></div>")(scope);
link.appendto('.user-class');

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.

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

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()?

Resources