The question is how do I clean destroy elements and scopes in AngularJS.
I've got a binary tree structure, which is implemented by recursively using the same directive.
I would like to change the binary tree structure and rebuild the tree with directives. This works fine, but it seems the old elements and scopes are not removed or destroyed properly.
Unfortunately I haven't found a good documentation on the topic of cleaning and destroying of elements. When should I actually use scope.$destroy(). (get element scope children in link does not work) Could you give me a link to a documentation?
The following code should give you an impression of my directive.
.directive('mydirective', function($compile) {
return {
priority: 1000,
restrict: 'E',
scope: {
node: '='
},
controller: 'mydirectiveController',
link: function(scope, element, attrs) {
function runLink() {
// Node
if ...
var pane1 = angular.element('<mydirective node="node.left" />');
element.children().remove(); // is not sufficient
element.append(pane1);
element.append(handler);
element.append(pane2);
$compile(element.contents())(scope);
// Leaf
else ...
element.children().remove(); // is not sufficient
$compile(widgets)(scope);
element.replaceWith(widgets);
}
runLink();
scope.$watch('', function(newValue, oldValue) {
if ...
runLink();
});
}
};
}
Couple of things... one is, because your scope: is an object in the options to the directive, you have isolate scope. So it's not the same scope as the parent scope.
Also, I believe there is something in the angular directive documentation about certain things not working when the directive can contain instances of itself. Read through all the links around directives and you should find it easily enough. I just ran into it today.
You generally only need to call $destroy() if you created a new scope as a part of setting up the directive. There are examples of this here on stack overflow.
Related
I have this code:
app.directive('foo', function($compile) {
return {
restrict: 'E',
scope: {},
template: '<span>{{bar}}</span>',
compile: function(element, attrs) {
element.attr('title', '{{bar}}');
return function(scope, element, attrs) {
scope.bar = 'hello';
$compile(element)(scope);
}
}
}
});
Plunkr:
http://plnkr.co/edit/nFTgvYqoiFAthmjoizWS?p=preview
If I remove the $compile bit in the link function then the title attribute remains with the expression text ({{bar}}) and not the value ('hello');
Anyone can explain why?
I thought (from what I read in the docs) that this is what the compile phase is for - manipulating the template and preparing it for the link with scope and data binding. Why do I need to manually call $compile again? Isn't the template already compiled?
Maybe the phase names should be changed from compile, preLink, and postLink to postCompile, preLink, and postLink. The postCompile phase is availble to manipulate DOM before linking to a scope, at this point the linking function has been created but no scopes have been created. DOM can be added that requires no compilation. If additional elements are added that include directives or require interpolation, those additional elements need to be compiled and linked in order for the directives and interpolation to work.
To manupulate the template before compile, furnish a function to the template property: template: function(tElement, tAttrs) {}. For more information, see AngularJS Comprehensive Directive API Reference -- Template.
can you share a reference to "DOM can be added that requires no compilation, etc." or explain how did you found out about this?
Some sources of information:
AngularJS Developers Guide -- HTML Compiler
AngularJS Developers Guide -- Creating a Directive that Manipulates the DOM
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
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
I've got a directive... like so:
.directive('formMenuBuilderMenu', function (formMenuService) {
return {
templateUrl: '../../views/templates/formmenubuilder-menu-template.html',
restrict: 'A',
scope:{
menu:'='
},
link: function postLink(scope, element, attrs) {
// does stuff
} ...
It gets built dynamically using $compile whenever a new menu node is created.
scope.menu = {//new data for menu view directive part}
var $nodeTemplate = '<div form-menu-builder-menu menu="menu"></div>';
var html = $compile($nodeTemplate)(scope);
$content.append(html);
I had the impression that because I've defined a scope section in the formMenuBuilderMenu directive that this directive would have isolate scope and not be affected by new instances created
BUT this doesn't work at all!
What happens is that every time a new directive is created using $compile the scope.menu gets updated using the new value for all previous directives created, not keeping its isolated scope. Indeed logging out the scope in each directive created shows it's the same scope instance every time.
How would I do this so the directive scope remains independent and each instance has it's own scope? Is it even possible? Please let me know if further explanation is needed. I'm sure I'm going about this the wrong way so a pointer in the right direction would be appreciated.
To be clear my main objective is basically to be creating dynamic template parts using directives each with their own subset of data.
Instead of:
var html = $compile($nodeTemplate)(scope);
Try this:
var html = $compile($nodeTemplate)(scope.$new());
From the directive Angular docs, I see the compile function has 3 parameters, one of which is transclude. The only explanation the docs provide is:
transclude - A transclude linking function: function(scope, cloneLinkingFn).
I'm trying to understand what exactly you would do in the clone linking function. I don't even know what parameters get passed into it. I found one example that has one parameter called clone that appears to be an HTML element. Are there other parameters available? Which HTML element is this exactly? I'm also looking at probably using transclude: 'element' in my directive. Do the answers to those questions change when using 'element' instead of true?
I'm understanding transclusion with the simple examples, but I can't to seem to find more complex examples, especially with transclude: 'element'. I'm hoping someone can provide a more thorough explanation about all this. Thanks.
EDIT: Completely and totally changing my answer and marking this as "Community Wiki" (meaning no points for me) as I was outright wrong when I answered this
As #Jonah pointed out below, here is a really good article on the compile option of directives and using the transclusion function
The basic idea is the compile function should return a linking function. You can use the transclusion function provided inside the linking function to take a clone of the transcluded DOM element, compile it, and insert it wherever it needs to be inserted.
Here is a better example I've pulled out of my butt on Plunker
The idea of the compile function is it gives you a chance to programmatically alter the DOM elements based on attributes passed BEFORE the linking function is created and called.
// a silly directive to repeat the items of a dictionary object.
app.directive('keyValueRepeat', function ($compile){
return {
transclude: true,
scope: {
data: '=',
showDebug: '#'
},
compile: function(elem, attrs, transclude) {
if(attrs.showDebug) {
elem.append('<div class="debug">DEBUG ENABLED {{showDebug}}</div>');
}
return function(scope, lElem, lAttrs) {
var items = [];
console.log(lElem);
scope.$watch('data', function(data) {
// remove old values from the tracking array
// (see below)
for(var i = items.length; i-- > 0;) {
items[i].element.remove();
items[i].scope.$destroy();
items.splice(i,1);
}
//add new ones
for(var key in data) {
var val = data[key],
childScope = scope.$new(),
childElement = angular.element('<div></div>');
// for each item in our repeater, we're going to create it's
// own scope and set the key and value properties on it.
childScope.key = key;
childScope.value = val;
// do the transclusion.
transclude(childScope, function(clone, innerScope) {
//clone is a copy of the transcluded DOM element content.
console.log(clone);
// Because we're still inside the compile function of the directive,
// we can alter the contents of each output item
// based on an attribute passed.
if(attrs.showDebug) {
clone.prepend('<span class="debug">{{key}}: {{value}}</span>');
}
//append the transcluded element.
childElement.append($compile(clone)(innerScope));
});
// add the objects made to a tracking array.
// so we can remove them later when we need to update.
items.push({
element: childElement,
scope: childScope
});
lElem.append(childElement);
}
});
};
}
};
});