Angular Directive - Compile Vs Link - Compile not rendering only once - javascript

I have the following code. I expected the compile to run only once, and link to run 5 times.
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js" ></script>
<style>.red{color:red;}.blue{background:blue;}</style>
</head>
<body ng-app="myApp" ng-controller="myCtrl">
<div hello dear> 1 </div>
<div hello dear> 2 </div>
<div hello dear> 3 </div>
<div hello dear> 4 </div>
<div hello dear> 5 </div>
<script>
//module declaration
var app = angular.module("myApp",[]);
//controller declaration
app.controller('myCtrl',function($scope){
//no code
});
//directives declaration
app.directive('hello',function(){
return{
restrict: 'A',
compile: function(tElement, tAttributes){
tElement.addClass("red");
console.log("compiled");
},
}
});
app.directive('dear',function(){
return{
restrict: 'A',
link: function($scope, element, attributes){
element.addClass("blue");
console.log("linked");
}
}
});
</script>
</body>
</html>
Expectation:
Compile to run once. Link to run 5 times.
Result:
Both compile and Link running 5 times.
Screen-shot:
Reference:
http://www.bennadel.com/blog/2794-when-do-you-need-to-compile-a-directive-in-angularjs.htm
Can someone tell me why compile running 5 times (OR, how to run it only once)?

The compile phase comes before the link phase for every directive, since you applied the hello directive on 5 elements it's expected behaviour to compile and then link 5 times. A more detailed explanation of the linking process of a directive with a subdirective can be found here.
Compile function
Each directive's compile function is only called once, when Angular bootstraps.
Officially, this is the place to perform (source) template manipulations that do not involve scope or data binding.
Primarily, this is done for optimisation purposes; consider the following markup:
<tr ng-repeat="raw in raws">
<my-raw></my-raw>
</tr>
The <my-raw> directive will render a particular set of DOM markup. So we can either:
Allow ng-repeat to duplicate the source template (<my-raw>), and then modify the markup of each instance template (outside the compile function).
Modify the source template to involve the desired markup (in the compile function), and then allow ng-repeat to duplicate it.
If there are 1000 items in the raws collection, the latter option may be faster than the former one.
Do:
Manipulate markup so it serves as a template to instances (clones).
Do not:
Attach event handlers.
Inspect child elements.
Set up observations on attributes.
Set up watches on the scope.

That's the intended behavior though.
Have a look at https://docs.angularjs.org/api/ng/service/$compile
The whole flow (compile, controller, pre/post-link) happens for each directive tag.
This would make your compile run only once:
<hello>
<div dear> 1 </div>
<div dear> 2 </div>
<div dear> 3 </div>
<div dear> 4 </div>
<div dear> 5 </div>
</hello>
Though in that case, there is no need to use the compile phase, just use the link
app.directive('hello',function(){
return{
restrict: 'AE',
link: function(scope, iElement){
iElement.children().addClass("red");
console.log("\"compiled\"");
},
}
});
Edit: if you need hello directive's code to run before dear's, put the code in prelink instead of link.
.directive('hello', function() {
return {
restrict: 'AE',
compile: function() {
return {
pre: function(scope, iElement) {
iElement.children().addClass("red");
console.log("\"compiled\"");
}
}
},
}
})

Related

AngularJS Directive Isolated scope inside nested ng-transclude

I've been looking all over the internet for something like this and I still can't find the answer.
I have a directive that is reused throughout my application, it allows the user to sort and search through lists of items. I have multiple kinds of items that can be passed in to the directive (html templates that I pass in) as well as multiple uses for those templates. I.e, sometimes I want a certain button on that template, but sometimes I want another. (This will make more sense in a minute).
Therefore I have created multiple directives with transclusion in order to achieve this. However, I'm having serious issues with scoping and I can't seem to figure out exactly how to pass the isolated scope to the child directive.
Below is my code:
Item List Directive
var app = angular.module('main');
app.directive('itemList', function(){
var linkFunction = function (scope, element, attributes, ctrl, transclude) {
//Things that modify the scope here. This scope is what I want to pass down to the child directives
//NOTE: I do not currently have a working transclude function which is why I didn't include it here because I have no idea what to do with it
scope.pagedItems = groupItemsToPages(items);
scope.currentPage = 0;
};
return {
restrict: 'E',
replace: 'true',
transclude: true,
templateUrl: 'partials/directives/item-list.html',
link: linkFunction,
scope: {
searchPlaceholder: "#",
items: "=",
control: "="
}
};
});
item-list.html
<div class="form-group">
<!-- I won't put all of the html here, just some to show you what i'm going for -->
<div class="search-field">
<input type="text" ng-model="query.value" placeholder="{{searchPlaceholder}}/>
</div>
<table class="table table-hover">
<tbody>
<tr ng-repeat="item in pagedItems[currentPage]">
<td ng-transclude></td>
</tr>
</tbody>
</table>
</div>
Here's the directive that simply returns the URL of whatever template is passed to it. This is so that I can add in an extra html through further nested transclusions.
item-template.js
var app = angular.module('main');
app.directive('itemTemplate', function() {
return {
restrict: 'AE',
replace: 'true',
transclude: true,
templateUrl: function(tElement, tAttrs){
return tAttrs.templateUrl;
}
};
});
Here's an example template (extremely simplified again, just to show you the layout)
profile-template.html
<div>
<p>item.name</p>
<p>item.description</p>
</div>
<div ng-transclude></div>
Here's an example of the HTML that calls this code
tab.html
<div class="tab">
<div class="available-items">
<item-list control="profileControl" search-placeholder="Search Profiles" items="profileControl.profiles">
<item-template template-url="partials/profile-template.html">
<button type="button" ng-click="launchProfile(item.id)">Launch Profile</button>
</item-template>
</item-list>
</div>
</div>
So now that you've seen the code. The issue I'm having is that my profile-template.html inclusion isn't getting the scope from the directive above it even though I've tried cloning the scope to it. All the examples I've seen require you to remove the template key from the directive and assume you're only returning the code you have in your transclusion. In my case, I have other html that I want to display in the template.
Any help would be appreciated
Instead of trying to pass the scope between your directives, you can make use of the $parent attribute and get access to higher scopes.
For instance, in your item-template controller you could gain access to the higher scope from item-list with a code like this:
if ($parent.searchPlaceholder === '') {
...
}

Include view + controller + style

I need to create a base view, and include "modules" in it.
What I call module is : a View (HTML page), a Controller (Javascript) and a Style (CSS).
At the moment, I need to includes all my javascripts at the end of the index.html and my css at the beggining... BUT, sometimes I won't load ALL my modules.
So if in my page, I need to display module 1 / 2 / 3, I only want to include their views / controllers / styles, and not Module 4's.
I tried with ngView and ngInclude, but I always get an error when I put the related javascript.
Here is an example of what I would like :
<div ng-include="'modules/module1.html'"></div>
The result would be ===>
<link href="modules/module1.css" />
<div ng-controller="module1Controller as module1Ctrl">
<h3 class="test">Module 1</h3>
<p>It is a test !</p>
</div>
<script src="modules/module1.js"></script>
I hope it makes sense...
Your directive -
app.directive('module1', function () {
var controller = ['$scope', function ($scope) {
//CONTROLLER FUNCTIONS HERE
}];
return {
restrict: 'E',
scope: {
data: '=data'
},
controller: controller,
templateUrl: 'module1.html'
};
});
module1.html:
<h3 class="test">Module 1</h3>
<p>It is a test !</p>
Then on your view
<module1 data="THIS COULD BE AN OBJECT" />
In regards to your css you should really be using something like LESS or SASS, creating separate files, then building one big global CSS file using some sort of task runner like Grunt, Gulp.
All your CSS pertaining to module1 can just start with module1
Example:
module1 h1{font-size:24px;}
module1 div.body{width:100px;}
Here is a link to the plunk... https://plnkr.co/edit/epD6ckxaXxbGHssGaJhu?p=preview

Custom directive with ng-repeat (outside and inside), replace and templateUrl

I've checked various ressources and all leads to a bug that was closed long ago. Here is a similar issue, but I think mine is slightly different and persists up to at least angular.js 1.5.
I'd like to validate, if this is a new angular bug or if I missed something, since I'm relatively new to angular.
My problem
Check this plunker for a short demo of the issue
I've got a directive <infos> rendered inside a ng-repeat block
<body ng-controller="MainCtrl" class="container">
<div ng-repeat="item in items">
<h2>{{item.title}}</h2>
<infos></infos>
</div>
</body>
The <infos> directive has a ng-repeat block on its first element
<div ng-repeat="(infoId, color) in item.infos">
<p style="background: {{color}}">{{color}}</p>
</div>
The directive is loaded with templateUrl and replace:true.
.directive('infos', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'infos.html'
};
})
The unexpected result:
When rendered, the directives of all iterations of the surrounding ng-repeat are rendered on the last ng-repeat of the outside loop and not as expected on each iteration of the surrounding ng-repeat. Check the plunker demo for a better understanding, what's going wrong.
Workarounds
I found several workarounds to the problem:
Wrapping the ng-repeat element in the directive by any other element
<div>
<div ng-repeat="(infoId, color) in item.infos">
<p style="background: {{color}}">{{color}}</p>
</div>
</div>
Leaving away the replace: true
Using template instead of templateUrl. Inlining the template code.
Using <script type="text/ng-template" id="infos.html">... to load the template
There might be another solution, using the $templateCache but I couldn't figure out how to preload the template, yet.

Accessing directive scope from a separate controller

Let's say i have a <lightbox> directive in AngularJS. The lighbox has two params: title and image_source, which in turn are referenced in its HTML with {{title}} and {{image_source}}.
Because I use Bootstrap's modal, the directive must say on the outmost scope of the page (if I insert it in the inner DOM elements it can have display problems).
I understand that if the directive is in a "parent" controller, there are many ways to interact:
<div ng-controller="ParentCtrl">
<lightbox src='mySrc' ... >
</div>
but I am forced to pull it out:
<div ng-controller="ParentCtrl">
</div>
<lightbox ...>
(Edit: to clarify: i am forced to put the modal OUT of the controller (which is a template called by a route) because it interacts badly if it is not. Another way would be to wrap all the page in a Main controller, but I'm not sure it's elegant.)
But how do I access the internal directive scope and change dynamically the two values?
(Edit: thought of using a service, not sure if it is the right way, though)
this is method to call rootscope to directive
angular.module("directiveAPP", [])
.controller("directiveController", function ($scope, $rootScope) {
$rootScope.src = "http://picbook.in/wp-content/uploads/2014/07/image_123.jpg";
$rootScope.title = "hai";
}).directive("lightBox", function () {
return {
restrict: 'E',
template: '<div><img src="{{imageSrc}}" title="{{imageTitle}}" width="100px" style="border:solid #ddd 1px;" /></div>',
scope: {
imageSrc: '=',
imageTitle: '='
}
};
});
this one use of directive
<div ng-app="directiveAPP">
<div ng-controller="directiveController">
<light-box image-src="src" image-title="title"></light-box>
</div>
<light-box image-src="src" image-title="title"></light-box>
</div>
FIDDLE LINK

Recompile block modified by external scripts

I have to use a framework that modifies my DOM. Here is some example HTML that gets used:
<div id="testID" ng-show="example === 'show'">Some Content</div>
wil be modified by the Framework to something like this:
<div id="wrapperOne">
<div id="wrapperTwo">
<div id="testID" ng-show="example === 'show'">Some Content</div>
</div>
</div>
This gets done by calling a JS function like this one:
framework.wrap($("#testID"));
As you see, the framework wraps the old HTML with some other elements.
My problem is, that Angular does not show the div, when $scope.example becomes 'show'. Without using the framework, it works fine, I think the problem is, that angular compiles the DOM before the framework changes it. The framework has to wait for an ajax request, so just including the angular scripts after it does not work.
I am searching for a solution to force angular to "recompile" the whole DOM, better some parts of it.
Is this the right proposal, or is there any other way to do this? Just omitting the framework isn't the solution as I have to use it.
Thank you very much,
-Lukas
Use a custom directive with ng-transclude
I've created a fiddle - http://jsfiddle.net/zghuoxhb/2/
directive:
.directive('myWrapper', function () {
return {
restrict: 'E',
templateUrl: "my-wrapper-template.html",
replace: true,
transclude: true,
scope: false
};
})
html:
<my-wrapper>
<div id="testID" ng-show="example === 'show'">Some Content</div>
</my-wrapper>
output:
BTW excellent question, this is a good use case to showcase the awesomeness of angular

Categories