Getting AngularJS multilink error after upgrade from v. 1.4 - angularjs

I have a big directive with sub-directives where I'm getting the error. Exactly, it happens on the step when calling transcludeFn() in the link function of main directive:
var link = function ($scope, elem, attr, parentCtrl, transcludeFn) {
// Run all nested directives in order to properly register columns in grid.
transcludeFn();
// Add compiled content to directive element.
elem.after($compile(template)($scope));
};
Can't figure out with that issue. What I've missed in the docs? Adding stuff with $onInit as described here doesn't help (I'm not sure I was doing that correctly).
I'm not familiar with AngularJS so any help will be a good point.

I really don't know what is the root of the issue but I've solved it by passing a dummy function into transcludeFn():
var link = function ($scope, elem, attr, parentCtrl, transcludeFn) {
// Run all nested directives in order to properly register columns in grid.
transcludeFn();
transcludeFn(function() {
// do nothing, only for fixing upgrade issue
});
// Add compiled content to directive element.
elem.after($compile(template)($scope));
};

Related

Inner nested directive with inherited scope used in outer directive with isolated scope

Note: I am modifying this question to improve rating.
I developed the following directive with isolated scope:
app.directive('outerIsolated', function () {
return {
restrict: 'A',
scope: {
theData:'=',
...
},
replace: true,
templateUrl: './photo-list-template.html',
link: function ($scope, elm, attrs) {
...
...
...
}
};
});
And also, I developed the following inner directive with inherited scope.
app.directive('innerInherited', ['$compile', '$timeout', '$parse', function ($compile, $timeout, $parse) {
return {
scope: false;
link: function (scope, el, attrs, ngModel) {
...
...
... }
};
}]);
The Problem:
If I use the directive outerIsolated as a parent directive for the inner directive innerInherited, then all references to inherited scope variables won't work.
The innerInherited directive works just fine on its own and has been used extensively. Now, I can't change it to isolated scope. This directive is actually called check-if-required and will loop across all child input fields to find out if anyone is required field, and make it required.
Just few days ago, I learned about directive with isolated scope which can be used to develop reusable components. I liked the idea, and I developed one call upload-photo-list which I referred to it here as outerIsolated.
Is there anyway I can solve this problem easily?
One direct way, is reverse the nesting of the directive. So, I tried to use directive with inherited scope in the outer level instead, but, the problem now is that the link function of the outer directive didn't see the elements of the inner directive after being replaced by the template. I even used this code to try to wait until the document is ready this this:
angular.element(document).ready(function (){...}
... but still, the outer directive cannot reach the HTML Elements generated by the inner directive.
Appreciate your help to find a solution.
Thank you.
Old Question:
Note: this part is obsolete. I kept it here for tracking purposes only.
I am building a simple example using ng-signature-pad and signature-pad plugins.
Click here to see the sample HTML file as per the download
I noticed that the following script tag works only if I place them before the </body> tag (same as the provided sample source code in the link above):
<script src="js/app.js"></script>
If I place the above script tag in the <head> tag it is not affected.
Could someone explain to me why?
I would need to look at the project you are referencing in more detail, but I would imagine that the library and the app.js that you are using are referencing elements on the page.
If you have the scripts in the HEAD tag, those elements are not there. By putting them at the bottom of the BODY element, you are ensuring that the elements are in fact in the browser.
The quickest way to solve my problem is as follows...
I had to use the directive with the inherited scope check-if-required to be on the outer level, and the directive with the isolated scope upload-photo-list in the inner level.
However, I had to make two modifications for the directive check-if-required:
Add $timeout in the link function to ensure the inner directive has finished rendering its HTML before looping through all the input elements as follows:
code:
var children;
$timeout(function() {
children = $(":input", el);
el.removeAttr('check-if-required');
angular.forEach(children, function(value, key) {
...
...
});
})
Must compile the element with respect to the scope of the element:
code:
angular.forEach(children, function(value, key) {
if(typeof (value.id) != 'undefined') {
if (scope.isFieldRequired(value.id)) {
angular.element(value).attr('required', true);
$compile(value)(angular.element(value).scope());
}
})
So far, this solution works well for me.
Any feedback to improve is welcomed.
Tarek

Transclude partial content

I am trying to create a "sticky table header" component for which I need to copy parts of the transcluded content of my directive.
Depending on how I transclude the content, it works only partially at best: with $compile, expressions are updated when the underlying data changes, but ng-repeat does not seem to work at all. It does not even render the first time, let alone update later. Simply appending the partial content I found does not seem to work at all: element.append($(transcludedEl).find('.wrapper'));
To illustrate my point, I have created a plunkr using three versions of the same code: http://plnkr.co/edit/xkAkzl8ID3m5Ras3Ww31
The first is super-simple direct ng-repeat, which only serves to show what should happen.
The second uses a directive that transcludes its full content, which works but is not what I need.
The third (reproduced below) uses a directive to try and include only part of its content, which is what I need, but which does not work.
The interesting bit is this:
app.directive('stickyPartial', ['$compile', '$timeout', function($compile, $timeout) {
return {
restrict: 'E',
transclude: true,
template: '<div></div>',
link: function(scope, element, attrs, controller, transclude) {
transclude(scope, function(transcludedEl) {
// this is what i want to achieve - not working
// element.append($(transcludedEl).find('.wrapper'));
// neither is this, though it does support expressions
$compile($(transcludedEl).find('.wrapper'))(scope, function(clone) {
element.append(clone);
});
});
}
};
}]);
So far, I have tried several combinations of $compile, .clone() and .html(), but to no avail. I can neither get a working partial DOM tree from the compiled template, nor a useful partial HTML source with ng-repeat intact that I can then compile manually.
As a last resort, I might try copying the DOM after angular is done (which seemed to work, previously) and then manually repeat this process every time the relevant model data changes. If there is another way, thought, I'd very much like to avoid this.
Using https://stackoverflow.com/a/24729270/2029017 I found a solution with compile that does what I want: http://plnkr.co/edit/V4yUbiAD9EAaaJXihziv

Why it's not safe to do DOM transformation in Pre-linking function

In $compile there is a Pre-linking-function and in the AngularJS API Documentation they are write that it's not safe to do DOM transformation in the Pre-linking-function. Does anyone knows why?
Reference to section of API Documentation
First, what is linking?
linking is the process that angular uses to bind the model to the DOM.
The link function is responsible for registering DOM listeners as well as updating the DOM. It is executed after the template has been cloned. This is where most of the directive logic will be put. ~AngularJS API Documentation
Below is a really great diagram to show how Angular works
So why you can't manipulate the DOM in pre-linking?
Because pre-linking occurs before the linking happens (again this is the part where angular compiles and binds the model to the DOM so it can manipulate it). So during pre-linking angular is not aware of the DOM and its elements to be able to manipulate it at all.
To Reference the Diagram:
pre-linking would be before all of the arrows/lines are drawn between the boxes
linking would be the phase during the arrows/lines being drawn
post-linking would be after all of the arrows/lines are drawn and the pieces are connected
The main practical difference between pre and post linking functions lies in the fact that pre-linking functions are executed from parent directives to children directives, and post-linking functions are executed in the opposite order.
This is the result of $compile service behaviour which is explained in the manual:
Pre-linking function
Executed before the child elements are linked. Not safe to do DOM
transformation since the compiler linking function will fail to locate
the correct elements for linking.
Post-linking function
Executed after the child elements are linked.
In general this behaviour affects any situation where pre-linking function expects something from one of its children but will not get it because pre/post-linking functions in children (which run after parent's pre-link) didn't take that into account and messed up each other's controllers or DOM somehow. But the manual implies the very specific situation.
In this plunker
app.directive('abba', function () {
return {
template: '<a><b></b></a>',
link: {
pre: function (scope, element, attrs, ctrl) {
console.log('hi from this world')
element.find('a').remove()
}
}
}
});
app.directive('a', function () {
return {
link: {
pre: function (scope, element, attrs, ctrl) {
console.log('hi from that world')
}
}
};
});
app.directive('b', function () {
return {
link: {
pre: function (scope, element, attrs, ctrl) {
console.log('no hi from that world')
}
}
};
});
this will result in
hi from this world
hi from that world
Error: linkNode is undefined
nodeLinkFn#https://code.angularjs.org/1.4.8/angular.js:8330:9
Obviously, a directive is a living dead here, its pre-link is still alive. $compile also expects to link its children but failed to find the parent.
This won't happen if post-links are used instead:
yes hi from that world
hi from that world
hi from this world
so a and b can die peacefully after they served their purposes.

Angular ngAnimate module will break my open layers map directive

I've made a directive that display an Open Layers map (below is not my production code, but a simplified version used to create a plunkr. Don't pay attention to the hardcoded DOM element ID).
EDIT : the hardcoded ID was indeed the issue, see comments below...
app.directive('tchOlMapCopy', function () {
return {
restrict: 'E',
replace: true,
template: '<div id="tchMap" class="full-height"></div>',
link: function postLink(scope, element, attrs) {
var map = new OpenLayers.Map("tchMap");
map.addLayer(new OpenLayers.Layer.OSM());
map.setCenter(new OpenLayers.LonLat(3, 47).transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913")), 5);
}
}
I had that issue that, when I change route in my app an go from one screen including that directive to another, the map won't display on the second screen. The link function in the directive isn't even called.
I've narrowed down the issue to be related to ngAnimate module. If I remove dependency to this module, the map will display correctly on second route change.
I've made a Plunkr to illustrate this issue. Comment or uncomment the ngAnimate module in app.js file to see the issue.
Does anybody have an idea why ngAnimate breaks my directive call ?
Having multiple instance of this directive which has an hardcoded id seems a very bad idea. Should be a unique identifier : http://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#id

Should you use the linking function over the compile function when you can in Angular?

I want to learn more about compile function and transclusion and found this Plnkr. This code contains a advanced tab directive. Based on this directive I started to create my own to see what happens and to see the values of the variables.
For my test I was playing with my 'button-bar'. When finished I had created this code:
testapp.directive('buttonBar', function($compile) {
return {
restrict: 'EA',
compile: function(cElement){
var title,
template;
template = "<div class='well'><div class='button-bar-title'></div></div>";
title = cElement.find(".title");
console.log("title:", title.html());
return function(scope, element, attributes){
var well = angular.element(template),
titleDiv = well.find(".button-bar-title");
titleDiv.append(title.html());
element.empty();
element.append(well);
$compile(titleDiv)(scope);
}
}
};
});
Nothing fancy. But when I looked at the code, I thought, why use a compile function? I think I can refactor this using a link function, so I did:
testapp.directive('buttonBar', function($compile) {
return {
restrict: 'EA',
link: function(scope, element, attributes){
var title,
template,
well,
titleDiv;
template = "<div class='well'><div class='button-bar-title'></div></div>";
title = element.find(".title");
console.log("title:", title.html());
well = angular.element(template),
titleDiv = well.find(".button-bar-title");
titleDiv.append(title.html());
element.empty();
element.append(well);
$compile(titleDiv)(scope);
}
};
});
And this gave me the exact same result.
At this point I am confused, why should the originally author of the plunker use a compile function instead of a link function? Is there a good reason for? Or are there guidelines on when to use a compile or a link function?
Edit:
For both directives I've used the same HTML (primary1Label is defined on the controller):
<button-bar>
<div class="title">De titel is {{primary1Label}}</div>
</button-bar>
In the example you provided compile is the correct method, as it executes before the template is cloned and therefore performs less manipulation on the DOM.
The most fundamental difference between the two is that compile runs once for all directives on the DOM, whereas link runs for each individual element.
As a result it's recommended that if you want to perform a template DOM manipulation you should do so within the compile function.
If you want to listen to scope variables individually and perform instance DOM manipulation (i.e. on this element only) then the link function is what you want.
There's another answer here What is the difference between compile and link function in angularjs that explains it more thoroughly.

Resources