I have the following code with custom directive 'my-repeater':
<div ng-controller="AngularCtrl">
<div my-repeater='{{items}}'>Click here</div>
</div>
Here is my custom directive:
myApp.directive('myRepeater', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var myTemplate = "<div ng-click='updateRating({{item}});' ng-class='getRatingClass({{rating}});'>{{rating}}</div>";
var items = scope.items;
console.log('length: ' + items.length);
for (var i = 0; i < items.length; i++) {
var child = scope.$new(true);
console.log(items[i].ratings);
child.item = items[i];
child.rating = items[i].ratings;
var text = $compile(myTemplate)(child);
element.append(text);
}
}
};
});
ng-click and ng-class bindings are not happening properly inside my custom directive. Can anyone help me with what i am doing wrong here?
Here is the JS Fiddle.
http://jsfiddle.net/JSWorld/4Yrth/5/
Hi I've updated your sample to what I think you want to do.
http://jsfiddle.net/46Get/2/
First, in directives like ng-click='updateRating({{item}});' that
receive an expression you dont need to use '{{}}' because it is
already executed in the scope.
Second, when you need to add siblings to your directive, you need to do it in the compilation phase and not the linking phase or just use ng-repeat for that matter
I added .ng-scope { border: 1px solid red; margin: 2px} to #rseidi's answer/fiddle, and I discovered that a scope is being created by the $compile service for each item in the template -- i.e., each <div>. Since you have so many items to display, I assume that fewer scopes will be much better. After trying a bunch of different things, I discovered that Angular seems to create a new scope for each "top level" element. So a solution is to create an outer div -- i.e., ensure there is only one "top level" element:
var mainTpl = '<div>'; // add this line
...
for(...) { }
mainTpl += '</div>'; // add this line
Fiddle
Now, only the outer div creates a scope, since there is only one "top level" element now, instead of one per item.
Related
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
I have a simple angularjs directive that I use to show a tooltip.
<div tooltip-template="<div><h1>Yeah</h1><span>Awesome</span></div>">Click to show</div>
It works fine but now I'm trying to use it inside a timeline javascript component (visjs.org)
I can add items with html to this timeline like this
item...
item.content = "<div tooltip-template='<div><h1>Yeah</h1><span>Awesome</span></div>'>Click to show</div>";
$scope.timelineData.items.add(item);
The item is well displayed on the page BUT the code of the tooltip-template directive is never reached.
I suspect that because a third party component is rendering the item, the dom element is not read by angular.
I've tried to do a $scope.$apply(), $rootScope.$apply but the result is the same. The directive is never reached.
How can I tell angular to read my dom to parse these directives ?
Here is the directive code :
.directive("tooltipTemplate", function ($compile) {
var contentContainer;
return {
restrict: "A",
link: function (scope, element, attrs) {
var template = attrs.tooltipTemplate;
scope.hidden = true;
var tooltipElement = angular.element("<div ng-hide='hidden'>");
tooltipElement.append(template);
element.parent().append(tooltipElement);
element
.on('click', function () { scope.hidden = !scope.hidden; scope.$digest(); })
$compile(tooltipElement)(scope);
}
};
});
Edit
Added plunker : http://plnkr.co/edit/lNPday452GiZJBhMH4Kl?p=preview
I tried to do the same thing and came with a solution by manually creating scope and compile'ng the html of the directive with the scope using $compile method. Below a snippet
I did the below part inside a directive that created the timeline . Using the scope of that directive ,
var shiftScope = scope.$new(true);
shiftScope.name = 'Shift Name'
var shiftTemplate = $compile('<shift-details shift-name="name"></shift-details>')(shiftScope)[0];
I passed shiftTemplate as the content and it worked fine .
But trying to do this for >50 records created performance issues .
I have a templating directive that is almost working the way I want, simple version works :-
<div bw-template-replace>
<template>This is template text, replace [[#this]] please</template>
<this>text replaced</this>
</div>
expands to
<div bw-template-replace><span>This is template text, replace text replaced please</span></div>
However, if I embed other directives they dont fully work as expected.
See my plunk http://plnkr.co/edit/dLUU2CMtuN5WMZlEScQi?p=preview
At the end of the directive's link function I $compile the resulting text/node which works for {{scope interpolated text}} but does not work for embedded directives using the same scope.
The reason I need this is because I am using ng-translate for an existing ng-app and I want to use the existing English text as keys for translation lookups. An uncommon translation case is where we have HTML like the following (extract from the app), the [[#ageInput]] and [[#agePeriod]] 'arguments' may appear at different places in other languages, as I understand it ng-translate has no real support for this scenario currently.
<div class="row-fluid">
<div class="span12" translate bw-template-replace>
<template>
If current version of media has not been read for [[#ageInput]] [[#agePeriod]]
</template>
<ageInput><input type=number ng-model="policy.age" style="width:50px"/></ageInput>
<agePeriod><select style="width:100px" ng-model="policy.period" ng-options="p for p in periods" /></agePeriod>
</div>
</div>
Any help much appreciated.
I love having to work through these scenarios when you are a newbie at something as it really forces you to understand what is happening. I now have it working, basically my previous directive I found several ways of just replacing the html in the hope Angular would magically sort everything out. Now I have a better understanding of transclusion and in particular the transclusion function I made it work as desired. The cloned element passed into $transcludeFn already has scope attached and has been $compiled, so my function now parses the template text and generates individual textElement's and moves the argument elements around to suit the template.
My Current Solution
.directive('TemplateReplace', ['$compile', '$document', '$timeout',
function ($compile, $document, $timeout) {
return {
restrict: 'AC',
transclude: true,
link: function (scope, iElement, iAttrs, controller, transclude) {
transclude(scope, function (clone, $scope) {
$timeout(function () {
// Our template is the first real child element (nodeType 1)
var template = null;
for (var i = 0, ii = clone.length; i < ii; i++) {
if (clone[i].nodeType == 1) {
template = angular.element(clone[i]);
break;
}
}
// Remember the template's text, then transclude it and empty its contents
var html = angular.copy(template.text());
iElement.append(template); // Transcluding keeps external directives intact
template.empty(); // We can populate its inards from scratch
// Split the html into pieces seperated by [[#tagname]] parts
if (html) {
var htmlLen = html.length;
var textStart = 0;
while (textStart < htmlLen) {
var tagName = null,
tagEnd = htmlLen,
textEnd = htmlLen;
var tagStart = html.indexOf("[[#", textStart);
if (tagStart >= 0) {
tagEnd = html.indexOf("]]", tagStart);
if (tagEnd >= 0) {
tagName = html.substr(tagStart + 3, tagEnd - tagStart - 3);
tagEnd += 2;
textEnd = tagStart;
}
}
// Text parts have to be created, $compiled and appended
var text = html.substr(textStart, textEnd - textStart);
if (text.length) {
var textNode = $document[0].createTextNode(text);
template.append($compile(textNode)($scope));
}
// Tag parts are located in the clone then transclude appended to keep external directives intact (note each tagNode can only be referenced once)
if (tagName && tagName.length) {
var tagNode = clone.filter(tagName);
if (tagNode.length) {
template.append(tagNode);
}
}
textStart = tagEnd;
}
}
}, 0);
});
}
};
}
]);
If I have understood your question properly, I guess the issue is your directive is getting executed before ng-repeat prepares dom.So you need to do DOM manipulation in $timeout.
Check this plunker for working example.
Here is a nice explanation of similar problem: https://stackoverflow.com/a/24638881/3292746
Here is an example fiddle. Please open the console first.
It's a little long, so to quickly explain: You'll see two divs. One is "regular", the other is created via a directive. They both have click event handlers (added via ng-click).
Clicking the regular (non-directive) div does what I expect - the appropriate variables in the scope are set and you see that in the view (top two lines in the output show the id and top of the div that was clicked).
The problem is when clicking the directive div. These variables are not updated. I know I'm misunderstanding something about the scope of the directive, but what is confusing me is the log messages that are printed (open the console when you do this). The clicks are registered, and the controller's scope does set the variable values, as you can see in the console.
Yet the html does not update - try clicking one div, then the other, and go back and forth, and you'll see.
What am I missing here?
Code (please don't be put off by its length!):
<div ng-app="MyApp">
<div ng-controller="MyController">
Top of the div you just clicked = {{top}}.<br/>
Id of the div you just clicked = {{lastId}}.<br/>
<div id="notADirective" class="myClass" ng-click="update($event)">
Not a directive. Click me.
</div>
<my-div element-id="isADirective" cls="myClass">
I'm a directive. Click me.
</my-div>
</div>
angular.module('components', [])
.controller('MyController', function ($scope) {
$scope.lastId = '';
$scope.top = '';
$scope.update = function (event) {
var myDiv = getFirstMyClassAncestor(event.target);
var style = window.getComputedStyle(myDiv);
$scope.top = style.top;
$scope.lastId = myDiv.id;
console.log("You just clicked " + $scope.lastId + " and its top is " + $scope.top);
}
function getFirstMyClassAncestor(element) {
while (element.className.indexOf('myClass') < 0) {
element = element.parentNode;
}
return element;
}
}).directive('myDiv', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
controller: 'MyController',
scope: {
cls: '#',
elementId: '#'
},
template: '<div id="{{elementId}}" class="{{cls}}" ng-click="update($event)"><div ng-transclude></div></div>'
}
});
angular.module('MyApp', ['components'])
.myClass {
background-color: #DA0000;
position: absolute;
}
#isADirective {
top: 300px;
}
#notADirective {
top: 100px;
}
In case of your directive the assigned controller MyController gets the scope of this directive and not the scope of div as you probably expect. I added a log statement for the different scope ids:
http://jsfiddle.net/6zbKP/4/
If you want to update the outer or parent scope you have to use a binding like this:
scope: {
cls: '#',
elementId: '#',
callback: '='
},
Then bind your update function in the directive:
<my-div element-id="isADirective" cls="myClass" callback="update">
I'm a directive. Click me.
</my-div>
And call the callback in your directive:
template: '<div id="{{elementId}}" class="{{cls}}" ng-click="callback($event)"><div ng-transclude></div></div>'
See http://jsfiddle.net/6zbKP/5/
The angular expression bindings are outside of your directive's isolated scope. And you haven't imported lastId or top from your parent controller scope into your directives isolated scope, so there is no reason to expect that updating one variable in the inner directive scope will update the variable in the outer controller scope. There is no two way binding set up.
You can setup two way binding however using scope: {lastId: '=', top: '=' }.
Also you shouldn't be sharing controllers like you're doing. I suggest using anonymous controller functions.
I am testing idea of dynamically changing templates. I ended with sample code that oversimplifies
whole process just to show the problem:
<div ng-app="app" ng-controller="MyController">
<content-item ng-repeat="item in data"></content-item>
</div>
and JS:
var app = angular.module('app', []);
app.controller('MyController', function ($scope) {
$scope.data = ["heading1", "heading2"];
});
app.directive('contentItem', function ($compile) {
var templates = [
'<h1>{{item}}</h1>',
'<h4>{{item}}</h4>'];
var compiledTemplates = [];
for (var i = 0, ii = templates.length; i < ii; i++) {
compiledTemplates.push($compile(templates[i]));
}
return {
restrict: "E",
replace: true,
link: function (scope, element, attrs) {
var template = templates[0];
var link = $compile(template);
//var link = compiledTemplates[0];
element.append(link(scope));
}
};
});
Hardcoded is number of template (0) just to simplify the code and compiledTemplates array is not used (next sample will use it)
Effect the whole list of elements is rendered:
heading1
heading2
OK. But I want to pre-compile the templates so compile process doesnt happen each time link function is called.
So, If I use compiledTemplates I just need to comment first to lines in link function and uncomment fird line (comment in code above). In the end link function looks this time :
link: function (scope, element, attrs) {
var link = compiledTemplates[0];
element.append(link(scope));
}
Not much of a difference (at least I cannot see the difference), but effect is :
heading 2
(if we have 10 items in list only last one would be rendered)
Why? What am I missing?
Here is JSFiddle: http://jsfiddle.net/yoorek/DcaVM/
Maybe what I'm saying is wrong but ng-repeat is a special directive. It won't compile each item.
Read here in the documentation about the compiler
ngRepeat works by preventing the compilation process from descending
into the LI element so it can make a clone of the original and
handle inserting and removing DOM nodes itself.
Instead the ngRepeat directive compiles LI separately. The result of
the LI element compilation is a linking function which contains all
of the directives contained in the LI element, ready to be attached
to a specific clone of the LI element.
At runtime the ngRepeat watches the expression and as items are added
to the array it clones the LI element, creates a new scope for the
cloned LI element and calls the link function on the cloned LI.
I hope that will help!