Angular dynamic templating with compile VS template function? - angularjs

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

Related

ANGULAR JS dynamic data binding to template

Hello All Angular Friends
I am trying to find a way for dynamic data binding to the template.
Created a test page: http://jsbin.com/jiminey/edit?html,js,output.
Currently I have my HTML
<banner compSrc="banner1"></banner>
<banner compSrc="banner2"></banner>
And the Data
$scope.bannerData ={
"banner1": {
"heading": "Hero Test"
},
"banner2": {
"heading": "Page Title (h1)"
}
};
Template
template: '<div>BannerHeading - {{bannerData.banner2.heading}}</div>'
How can I make this template dynamic based on the compSrc attribute?
I am looking for something like below So I dont have to update the template.
template: '<div>BannerHeading - {{heading}}</div>'
Thank you.
You can use isolated scope for directives. Take in account the name normalization.
Here is fixed JSBin
Create a directive for your template, and use function as value for your compile property of DDO
Plz refer this question on SO: What are the benefits of a directive template function in Angularjs?
app.directive('myDirective', function(){
return {
// Compile function acts on template DOM
// This happens before it is bound to the scope, so that is why no scope
// is injected
compile: function(tElem, tAttrs){
// This will change the markup before it is passed to the link function
// and the "another-directive" directive will also be processed by Angular
tElem.append('<div another-directive></div>');
// Link function acts on instance, not on template and is passed the scope
// to generate a dynamic view
return function(scope, iElem, iAttrs){
// When trying to add the same markup here, Angular will no longer
// process the "another-directive" directive since the compilation is
// already done and we're merely linking with the scope here
iElem.append('<div another-directive></div>');
}
}
}
});

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');

Dynamic id inside AngularJS template

I'm wrapping a jQuery plugin inside a AngularJS directive. The way I would like to call the directive is for example:
<my-dialog data-trigger-id="myTriggerId">My dialog content...</my-dialog>
Inside my directive template it looks like this:
<button id="{{triggerId}}">Button text...</button>
I attach the event for the jQuery plugin (where you specify the trigger selector) inside the link function of my directive. My problem is that it works if I hardcode the id of the button inside the directive template like this:
<button id="myTriggerId">Button text...</button>
The generated html looks fine in the browser, which means that rendering an element with a dynamic id works. It's just that the jQuery plugin cannot find this element if I use the dynamic id but it works with the hardcoded version.
I also looked up AngularJS compile because it looks like at the point where the jQuery plugin wants to initialize the element doesn't exist yet.
Is there a gotcha I'm missing? Thanks!
Edit: I finally managed to simplify it down and create a jsfiddle example. If you run the example, you will see in the console that the element doesn't exist at the time I'm logging it but if you inspect the DOM, you will see that it's there and has the correct id.
However if you hardcode the id in the template (id=test instead of id={{elemId}}), the console log will show that one element could be found. I hope this helps to find a solution.
http://jsfiddle.net/a1nxyv8u/7/
The digest has not yet rendered in the DOM by the time you are calling you $("#test").length.
You need to add in a $timeout so that the digest will complete, then call your method
var app = angular.module('app', []);
app.directive('myDialog', ['$timeout', function ($timeout) {
return {
restrict: 'E',
template: '<button id="{{elemId}}" class="{{elemClass}}">Open dialog</button>',
link: function (scope, element, attrs) {
var selector = scope.elemSelector,
elemClass = (selector.indexOf('.') > -1) ? selector.substr(1) : '',
elemId = (selector.indexOf('#') > -1) ? selector.substr(1) : '';
scope.elemClass = elemClass;
scope.elemId = elemId;
$timeout(function() {
console.log('elem: ', $('#test').length);
});
// jQuery plugin init here but element doesn't seem to exist yet.
},
scope: {
elemSelector: '#'
}
}
}]);
Although it should be noted that you should try and alleviate any Id's at all and just use $(element) instead unless your jQuery absolutely needs the Id.

How do I assign an attribute to ng-controller in a directive's template in AngularJS?

I have a custom attribute directive (i.e., restrict: "A") and I want to pass two expressions (using {{...}}) into the directive as attributes. I want to pass these attributes into the directive's template, which I use to render two nested div tags -- the outer one containing ng-controller and the inner containing ng-include. The ng-controller will define the controller exclusively used for the template, and the ng-include will render the template's HTML.
An example showing the relevant snippets is below.
HTML:
<div ng-controller="appController">
<custom-directive ctrl="templateController" tmpl="template.html"></custom-directive>
</div>
JS:
function appController($scope) {
// Main application controller
}
function templateController($scope) {
// Controller (separate from main controller) for exclusive use with template
}
app.directive('customDirective', function() {
return {
restrict: 'A',
scope: {
ctrl: '#',
tmpl: '#'
},
// This will work, but not what I want
// Assigning controller explicitly
template: '<div ng-controller="templateController">\
<div ng-include="tmpl"></div>\
</div>'
// This is what I want, but won't work
// Assigning controller via isolate scope variable from attribute
/*template: '<div ng-controller="ctrl">\
<div ng-include="tmpl"></div>\
</div>'*/
};
});
It appears that explicitly assigning the controller works. However, I want to assign the controller via an isolate scope variable that I obtain from an attribute located inside my custom directive in the HTML.
I've fleshed out the above example a little more in the Plunker below, which names the relevant directive contentDisplay (instead of customDirective from above). Let me know in the comments if this example needs more commented clarification:
Plunker
Using an explicit controller assignment (uncommented template code), I achieve the desired functionality. However, when trying to assign the controller via an isolate scope variable (commented template code), it no longer works, throwing an error saying 'ctrl' is not a function, got string.
The reason why I want to vary the controller (instead of just throwing all the controllers into one "master controller" as I've done in the Plunker) is because I want to make my code more organized to maintain readability.
The following ideas may be relevant:
Placing the ng-controller tags inside the template instead of wrapping it around ng-include.
Using one-way binding ('&') to execute functions instead of text binding ('#').
Using a link function instead of / in addition to an isolate scope.
Using an element/class directive instead of attribute directive.
The priority level of ng-controller is lower than that of ng-include.
The order in which the directives are compiled / instantiated may not be correct.
While I'm looking for direct solutions to this issue, I'm also willing to accept workarounds that accomplish the same functionality and are relatively simple.
I don't think you can dynamically write a template key using scope, but you certainly do so within the link function. You can imitate that quite succinctly with a series of built-in Angular functions: $http, $controller, $compile, $templateCache.
Plunker
Relevant code:
link: function( scope, element, attrs )
{
$http.get( scope.tmpl, { cache: $templateCache } )
.then( function( response ) {
templateScope = scope.$new();
templateCtrl = $controller( scope.ctrl, { $scope: templateScope } );
element.html( response.data );
element.children().data('$ngControllerController', templateCtrl);
$compile( element.contents() )( templateScope );
});
}
Inspired strongly by this similar answer.

How to modify transcluded content before compile inside directive?

What I want to do, is to handle transclude by hand and modify the content before I insert into the DOM:
return {
restrict: 'E',
transclude: true,
template: '<HTML>',
replace: true,
link: function(scope, element, attrs, ngModelCtrl, $transclude) {
var caption = element.find('.caption');
$transclude(function(clone) {
console.log(clone);
clone.filter('li').addClass('ng-hide'); // this don't work
clone.addClass('ng-hide'); // same this one
clone.attr('ng-hide', 'true'); // same this one
$compile(clone)(scope.$new()).appendTo(caption);
caption.find('li').addClass('ng-hide'); // and this
});
}
}
In angular.js source I found this example:
var templateElement = angular.element('<p>{{total}}</p>'),
scope = ....;
var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) {
//attach the clone to DOM document at the right place
});
//now we have reference to the cloned DOM via `clonedElement`
but when I add clonedElement.appendTo(caption); inside link function it only add comment with ng-repeat inside.
I need this because I need to hide all elements in this case
<dropdown>
<li ng-repeat="item in items"><a>{{item.label}}</a></li>
</dropdown>
I need to modify the template before compile or DOM after ng-repeat is expanded. Before would be better because I will be able to add logic using ng-hide directive instead of ng-hide class.
I realise it's been a long time since this question was posted, but I hope you may find the following useful.
I've been quite long and heavily in this (transclusion) business, I tried a few ways to achieve what you #jcubic need and finally I came across a solution which is really robust and quite simple.
...
replace: false,
transclude: false,
compile: function( tElement, tAttributes ) {
// store your "transcluded" content of the directive in the variable
var htmlContent = tElement.html();
// then remove it
tElement.html('');
return function postLink(scope, elem, attrs) {
// then html var is available in your link!
var $html = $('<div />',{ html:htmlContent }); // for much easier manipulation (so you can use DOM functions - you can also manipulate directly on htmlContent string)
// so you can manipulate the content however you want
scope.myVariable = true;
$html.find('li').attr('ng-hide', 'myVariable'); // add native directive
$html.removeClass('inner-content').addClass('my-inner-content'); // add/remove class
$html.find('#myElement').attr('my-directive',''); // add custom directive etc. etc.
// after you finished you just need to compile your html and append your directive element - also however you want
// you also convert back $html to the string
elem.append( $compile( $html.html() )(scope) ); // append at the end of element
/* or:
elem.find('.my-insert-point').html( $compile( $html.html() )(scope) ); // append the directive in the specific point
elem.find('[my-transclude]').html( $compile( $html.html() )($parent.scope) ); // once the scope:true it will be the same as native transclusion ;-)
scope.variable = $html.html(); // or you can probably assign to variable and use in your template with bind-html-compile (https://github.com/incuna/angular-bind-html-compile) - may need $sce.trustAsHtml
*/
}
}
...
So as you can see you have full control on your "transcluded" content and you don't even need transclusion! :-)
ps. I tested it with Angular 1.4. Not sure if it works with replace:true (I wasn's bother to test it as it's minor nuisance if it doesn't). You can use pre and post link as normally you'd use within compile function and you need to inject $compile service into your directive.
jcubic. You do not have to use $compile for what you are trying to do.
You can filter the transcluded element 'clone' and add css classes to the filtered nodes , but after that you have to append the modified clone to the template (it is identified by the 'element' attribute of the link function).
element.append(clone)
I created this jsfiddle for you.
If you still have other questions , please create a jsfiddle of your case.It Will be better to make an answer Thx
If you're using angular > 1.3 and ngTransclude in template, so you need to update not the clone, but transcluded DOM, eg:
elm.find('ng-transclude')
http://jsfiddle.net/jhqkxgos/
but be sure to compile found elements if you update some you need to access from controller

Resources