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!
Related
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
I am trying to create a directive as custom video control. I loading an html file to templateUrl of this directive. The problem is when there are more than one controls, they have the same src file set to all of them and they are sharing state of video as well. When I pause from another control, it pauses video being played on 1st control. Here is directive template that I am using:
dApp.directive('myVideoControl', function(){
return {
scope: {
cameraUrl: '=vcCameraUrl'
},
restrict: 'E',
templateUrl: './../../js/directives/myVideoControl.html',
link: function (scope, element, attrs) {
scope.playVideo = function(){
var v = document.getElementsByTagName("video")[0];
v.play();
}
scope.pauseVideo = function(){
var v = document.getElementsByTagName("video")[0];
v.pause();
}
}
}
});
Will greatly appreaciate if anyone can point out if I am doing anything wrong here.
Thanks.
It looks like the problem you are having is that you are looking up the element by tag name. Basically, every element in your dom with the tag <video> is going to be effected by any use of your directive.
The idea with directives, is that they provide direct access to the element that the directive was assigned. In your case element inside the link function parameters. So you need to reference the individual associated elements like this:
var v = element[0];
v.play();
If you have assigned the directive on a parent element, and want all children, then use the find() jqLite function on the directive element:
var v = element.find('video')[0];
v.play();
var v = document.getElementsByTagName("video")[0];
You are selecting the first video tag in the entire page.
instead get the element inside your template,something like
element.find('video')[0]
Code is here.
I'm trying to create a directive that re-arranges it's child elements. I can't use a simple ng-transclude because I want to put some child elements in different places within the template. I've learned that I need to set terminal: true and control compilation myself, but how do you do that? As you can see in that code the ng-if and ng-model on the child elements have been compiled, but are not working properly.
One specific thing I may be doing wrong: the second argument to the $compile function. I don't know what it is, and the documentation, says nothing but "function available to directives".
Here's the directive in question:
.directive('controlGroup', function ($compile, $log) {
var template = "<div class='control-group'>" +
"<label class='control-label'></label>" +
"<div class='controls'></div>" +
"</div>";
return {
restrict: 'A',
terminal: true,
priority: 100,
compile: function (elt, attrs) {
// Re-arrange element, inserting parts into template from above.
var labelText = elt.find('label').text();
var inputsAndMessages = elt.children().filter('input, button, select, .text-error');
var newElt = $(template);
newElt.find('.control-label').text(labelText);
newElt.find('.controls').append(inputsAndMessages);
elt.html('').append(newElt);
// Now, how to finish compiling and linking it? What to pass for 2nd arg?
var link_ = $compile(elt, null, 99);
function link (scope, elt, attrs) {
}
return link;
}
};
})
This line var link_ = $compile(elt, null, 99); returned a template function. $compile docs :
Compiles a piece of HTML string or DOM into a template and produces a
template function, which can then be used to link scope and the
template together.
Now you just need to execute that template against a scope. Since there's no scope at compile time, we need to do it in your link function, like so:
function link (scope, elt, attrs) {
link_(scope);
}
That fixes it: Working plunker
The second parameter is a transclusion function that would give you access to the cloned element and scope if you were transcluding. Since you're not transcluding, null is fine to pass in.
I have created a directive that check if data was entered to an HTML element in the following way:
var myApp = angular.module('myApp', []);
myApp.directive("uiRequired", function () {
return function (scope, elem, attrs) {
elem.bind("blur", function () {
var $errorElm = $('#error_testReq');
$errorElm.empty();
if (angular.isDefined(attrs) && angular.isDefined(attrs.uiRequired) && attrs.uiRequired == "true" && elem.val() == "") {
$errorElm.append("<li>Field value is required.</li>");
$errorElm.toggleClass('nfx-hide', false);
$errorElm.toggleClass('nfx-block', true);
}
else
{
$errorElm.toggleClass('nfx-hide', true);
$errorElm.toggleClass('nfx-block', false);
}
});
};
});
A working example can be seen here
My question:
Is there a way of adding the directive (uiRequired) I have created dynamically to elements on screen on document ready.
I want to put the new directive on selected HTML elements according to pre-defined list I have. I can not know in advance on which field this directive has to be on.
So I have to put it while page is rendering.
I have tried putting it dynamically myself while page is loading, however AngularJS did interpret it.
I could not find an example on the internet that does that.
Can anyone help me?
You can dynamically add directives to a page during the compilation phase when Angular is walking the DOM and matching elements to directives. Each step of the compilation process may transform the DOM tree ahead of it, but you should never modify elements that Angular has already compiled. This point is important to remember because adding directives to elements that have already been walked will have no effect. Of course, there ways around this. For example, you could re-compile and re-link previously walked elements. However I strongly advise against this as it may lead to unintended side effects such as memory leaks, and slow performance.
To dynamically add uiRequired directives, you can create a parent directive - let's call it uiRequiredApplier.
app.directive('uiRequiredApplier', function($scope) {
return {
restrict: 'A',
compile: function(element, attr) {
// you can apply complex logic figure out which elements
// you want to add the uiRequired attribute to
$('input', element).attr('uiRequired','');
return function(scope, element, attr) {
}
}
}
});
You can then apply the attribute like this:
<div ui-required-applier>
<input type="text" ng-model="name" />
</div>
When the uiRequiredApplier is compiled, it will dynamically add uiRequired attributes to selected elements using jQuery that have not been compiled yet. And when Angular walks the DOM, eventually it will compile and link the uiRequired attributes, which will add the desired validation behavior.
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.