is it necessary to include a destroy method in my directive - angularjs

I've written a pretty simple directive that adds/removes a css class on an element when the element is clicked.
app.directive('dropdown', function() {
var open = false, element,
callback = function(){
open = !open;
if (open) {
element.addClass('open');
} else {
element.removeClass('open');
}
};
return {
scope: {},
link: function(scope, elem){
element = elem;
elem.bind('click', callback);
scope.$on('$destroy', function(){
elem.unbind('click', callback);
elem.remove();
});
}
};
});
I think that the $destroy method is probably unnecessary. Since I've used the built in jqlite the listener will be destroyed along with the element right? Also is there any benefit to calling elem.remove(). I've seen it in some examples but not sure if I see the need.
Any thoughts appreciated
C

You don't have to remove the element manually for sure. You also don't need to unbind anything from scope because it will be handled by angularjs itsef.
For jquery dom listeners:
In case you are referencing JQuery then angular will use it instead of his internal jqLite implementation. It means that the native jquery remove method will be used for the element removal. And the jquery documentation for remove says:
Similar to .empty(), the .remove() method takes elements out of the
DOM. Use .remove() when you want to remove the element itself, as well
as everything inside it. In addition to the elements themselves, all
bound events and jQuery data associated with the elements are removed.
So i think that you don't need to unbind your listeners.
But I'm not 100% sure about this:)

In your case, you should be fine since the event is bound to the element that gets removed and thus the handler gets destroyed along with the element itself. Now, if your directive binds an event to a parent outside of its own DOM element, then that would need to be removed manually on the $destroy.
However, closure can cause any object to stay alive so that's something you do need to worry about. You could introduce a new function still referencing variable objects in the functions whose scope you are trying to destroy and that prevents GC from doing what you likely want it to. Again, that won't affect your current example, but it's something to always consider.

Related

Angularjs watch input binding change for Array when using ControllerAs

I have an AngularJs component having bindings to heroes, which is an array. How to watch this input for array changes? I tried $scope.watch("heroes", ...) and $onChanges, but didn't work so far.
bindings: {
heroes: '<',
}
Here is my plunker: https://plnkr.co/edit/J8xeqEQftGq3ULazk8mS?p=preview
The ControllerAs structure needs a special watch expression, since the attributes are not on the $scope.
//This one works and is the best one (> AngularJs 1.5)
$scope.$watch("$ctrl.heroes.length", function () {
console.log("ControllerAs syntax"); // Triggers once on init
});
//This one works as well
var ctrl = this;
$scope.$watch(() => {
return ctrl.heroes.length;
}, (value) => {
console.log("complex watch"); // Triggers once on init
});
See example here: https://plnkr.co/edit/J8xeqEQftGq3ULazk8mS?p=preview
The issue occurs because $scope.$watch by default doesn't deeply watch objects. Which means since you never destroy/recreate your array, the reference doesnt really change therefore $scope.$watch doesnt see any change. If you watched heroes.length, that primitive would change and your $scope.$watch would fire the corresponding listening function. By using $scope.$watch with the true option you are telling the angular engine to deeply watch all properties.
This is pretty intensive to do for large objects because $scope.$watch using angular.copy to track changes
If you were to use $scope.$watchCollection angular would create a shallow copy and would be less memory intensive. So I feel your 3 main options are
Watch heroes.length , add true or use $watchCollection
I feel that using heroes.length would be your best bet, so the code would look like
$scope.$watch('heroes.length',function(){});
The other two options are described below
$scope.$watch('heroes',function(){
//do somthing
},true)
or
$scope.$watchCollection
The benefit of using watchCollection is, that it requires less memory to deeply watch an object.
Shallow watches the properties of an object and fires whenever any of
the properties change (for arrays, this implies watching the array
items; for object maps, this implies watching the properties). If a
change is detected, the listener callback is fired.
The obj collection is observed via standard $watch operation and is
examined on every call to $digest() to see if any items have been
added, removed, or moved. The listener is called whenever anything
within the obj has changed. Examples include adding, removing, and
moving items belonging to an object or array.

Is there a way to get a scope of a DOM element when debug info is disabled?

I'm writing an directive which need to retrieve a scope of current DOM element. using the non public api angular.element().scope();
It works well until angular 1.3 introduces a new feature $compileProvider.debugInfoEnabled(false); which mainly aims to improve performance to avoid bind data in DOM element. But when debugInfoEnabled() is set to false, angular.element().scope() will return undefined. So I must find another way to get the scope of an DOM element or I have to redesign my code logic.
Is there a way to make this possible?
I just faced a similar problem in our application after compiling our app with $compileProvider.debugInfoEnabled(false);. I needed to later access some of our directive's isolate scope but couldn't use the isolateScope() method. To get around the problem, I created a helper function in a Utils service that looks like this:
this.setElementIsolateScope = function(element, scope) {
element[0].isolateScope = function() {
return scope;
};
};
Then inside any directive where I needed to be able to later access the isolate scope I called this function inside the link() function: Since element is a jqLite object, you need to set the isolateScope() function on element[0]. You should already have the jqLite wrapped element and scope already passed into your link function, which you then just pass to your service method.
Utils.setElementIsolateScope(element, scope);
To then access the isolate scope later, you would get a reference to your element and then do this (assuming child_element is the reference to your element/directive):
var child_iso_scope = _.isFunction(child_element.isolateScope) && child_element.isolateScope();
Depending on how you are getting the reference to your element, you may need to wrap it a jqLite wrapper like this:
child_element = angular.element(child_element);
And then just use the same way as above to get the isolate scope. Hope this helps!

AngularJs - Should I destroy a directive instance manually?

Consider an application that utilizes ngRepeat to show a list of directive instances.
When an element is removed from the list, should I manually destroy the directive instance or is it safe enough to call splice() on the array that holds the element?
The developer guide is not very helpful here.
If you are in doubt and want to check whether the directive is actually getting destroyed, you could put a watch on $destroy in your directive. For example:
// inside your link function
scope.$on('$destroy', function() {
console.log("destroyed");
});

How can I execute code after $digest is complete?

I am having a sortable list that gets populated by data in my Angular Controller. This list also contains an extra element containing some controls that can also be dragged
What I want do do, is make sure that the extra element stays in place after the $digest cycle is run, and I made a directive just for that case.
App.directive('ngIgnore', ['$timeout',function($timeout){
return {
link: function(scope, element){
scope.$watch(function(){
// Keep track of the original position of the element...
var el = element[0];
var siblings = Array.prototype.slice.call(el.parentNode.children);
var parent = el.parentNode;
var index = siblings.indexOf(el);
$timeout(function(){
// After the digest is complete, place it to it's previous position if it exists
// Otherwise angular places it to it's original position
var item;
if(index in parent.children)
item = parent.children[index];
if(!!item){
parent.insertBefore(el, item);
}
});
});
}
}
}]);
It worked, but not as I wanted it to... As you can see in this example shuffling the list does not move the ignored element, but the problem is that the $watch function gets executed infinitely since the $timeout triggers another $digest cycle... I tried changing the $timeout with setTimeout but no luck...
Is there any Angulary way to run code right after $digest is executed? Or is my entire logic wrong here?
Another (highly recommended) solution is to stop thinking in DOM manipulations.
Common problem with developers who start writing AngularJS code is tend to do DOM manipulations as the result of some event. Its common in jQuery:
$('#someid').click(function() { this.toggleClass('clicked') })
In AngularJS you should design your View to visualize your Model state (that should be in $scope). So
<div ng-click="clicked = !clicked" ng-class="{clicked: clicked}">I was clicked</div>
Same logic should be applied when designing components. In a HTML code you should put all visual logic - hide some elements using ng-show/ng-if, ng-switch. Add/remove classes using ng-class etc. So you define all possible model states.
Then by just changing model state you will get your view automatically updated reflecting current model state.
Same goes for repeated elements: you can repeat on some collection and then, depending on what element is present, you define how it would look. Keep in mind, that in ng-repeat each element will have own child (so mostly independed) Scope, so you can define some per-element manipulations.
See also this great answer: "Thinking in AngularJS" if I have a jQuery background?
You can try to use Scope.evalAsync Scope.evalAsync:
it will execute after the function that scheduled the evaluation
(preferably before DOM rendering).

AngularJS and ng-repeat elements do not exist in DOM yet - getting NULL exepction

So I am trying to use an external library function to create some manipulation in the DOM in one of my controllers that my ng-repeat is connected to. The problem is the following:
I am calling that external function in my controller that adds elements to the ng-repeat array , which in term adds the new elements to the DOM. However, when I am inside the controller, the element does not exist yet, even though I have added it to the array. How can I bound an external function to be called once the element has actually been appended to the DOM, rather than when it was actually added to the array that controls the ng-repeat?
I want to fire the event once the element has actually been created. Any suggestions?
Let me know if you would like to see a fiddle of this idea.
As mentioned in the comments, a check-last directive that checks for $last can be used to determine when the last iteration of ng-repeat is executing.
.directive('checkLast', function() {
return function (scope, element, attrs) {
if (scope.$last === true) {
element.ready(function() { // or maybe $timeout
... do something ...
});
}
}
});
See also https://stackoverflow.com/a/14656888/215945

Resources