I have the following code inside of a directive and I want to make sure it gets cleaned up when the scope is destroyed. I've looked online as well as the code and I was wondering how do I unbind an element.
var window = angular.element($window);
window.bind("resize", function(e){
abc();
});
Solution:
var abc = function() {};
var window = angular.element($window);
window.bind('resize', abc);
scope.$on('$destroy', function(e) {
window.unbind('resize', abc);
});
Unbind the function from window when the directive's scope gets destroyed. Hence in your directive, you'll invoke a clean up function on the scope's destroy's event:
$scope.$on('$destroy', cleanUp);
Create a cleanUp function and call the jQuery's unbind function.
There is an example in this SO entry, but with the off function instead (which seems similar to unbind, but unavailable in jqLite). Also per this SO entry, you might have to name your function as you'll need to reference it again as a parameter in the unbind call.
Related
I created a scope and controller dynamically from my code (usually from a provider) as given below
var controllerFn = function ($scope) {
/* scope functions and variables */
$scope.$on('custom_ng_event', function (evt) {
console.log('Custom evt listened in dynamic scope');
});
$scope.$on('$destroy', function () {
console.log('Dynamically created scope destroyed');
});
}
var $scope = $rootScope.$new();
var ctrlInstance = $controller(controllerFn, {$scope: $scope});
I want to remove the scope and de-register the controller at a certain point. I thought that $scope.$destroy() would do the task, but I think I'm missing something as it is not giving the expected result. Like, any broadcast to the $rootScope is still being reflected in the dynamically created scope listener.
Please help me to understand what I have done wrong.
Additional Info:
I preference is to have my dynamically created scope to be a child scope of root scope (directly) because it is meant to be used for the whole application (similar to that of a modal).
Thanks in advance
Balu
I saw a piece of code in a controller recently that went something like:
.controller('foobar', ['$scope', '$rootScope', function($scope, $rootScope) {
var eventHandler = $rootScope.$on('some-event', function() {
...
});
// remove eventHandler
$scope.$on('$destroy', eventHandler);
}]);
Questions:
Is executing the eventHandler "deregistration" function on $scope's $destroy event necessary?
If yes, would executing the deregistration function on $scope's $destroy event have been necessary if 'some-event' was $on $scope instead of $rootScope?
How do I know when I need to execute a deregistration function? I understand detaching or unbinding events is common for cleanup in JavaScript, but what rules can I follow to know when to do this in Angular?
Any advice about understanding this snippet/"deregistration" would be much appreciated.
In the example above the destroy method is necessary. The listener is bound to the $rootscope which means that even after the controller gets $destroy-ed the listener is still attached to the dom through the $rootscope. Every time the controller is instantiated a new eventhandler will be created so without the destroy method you will have a memory leak.
However if you bind the listener to the controllers $scope it will get destroyed along with the controller as the $scope gets destroyed so the listener has no connection to the dom thus making it eligible for garbage collection
Event handlers are only deregistered on controller's $destroy event when it is on that controller's $scope.
The deregistering would be unnecessary if it's on $scope since that's handled for you by Angular.
Generally if it's not tied to instance of the individual element, controller, or service you are listening on then that is when you need to handle deregistering yourself.
A good example is a directive that registers event listeners on the $document:
var module = angular.module('test', []);
module.directive('onDocumentClick', function directiveFactory($document) {
return {
link: function (scope, element, attrs) {
var onDocumentClick = function () {
console.log('document clicked')
};
$document.on('click', onDocumentClick);
// we need to deregister onDocumentClick because the event listener is on the $document not the directive's element
element.on('$destroy', function () {
$document.off('click', onDocumentClick);
});
}
};
});
Inside one of my directives, I use angular.element($window).bind('scroll'). When the directive is destroyed, I then want to unbind it. Normally, I would just do:
$scope.$on('$destroy', function()
{
angular.element($window).unbind('scroll');
});
But what if another directive also has binded to the scroll event of the $window, and that event still needs to exist. If I use the unbind above, the other directive's binding is also eliminated.
What are my options?
Pass the same function reference to unbind/off as you pass to bind/on to unbind just that particular handler:
var fn = function () {};
angular.element($window).on('scroll', fn);
angular.element($window).off('scroll', fn);
For example:
var onScrollAction = function () {
// Do something
};
angular.element($window).on('scroll', onScrollAction);
scope.$on('$destroy', function () {
angular.element($window).off('scroll', onScrollAction);
});
Note that in jQuery the functions bind and unbind are deprecated. You can however still use them both with jQuery and jqLite as they just call on and off behind the scenes.
JQuery's on() supports namespacing events so they can be removed independent of each other. (https://api.jquery.com/on/)
Angular's jqLite however, does not. (https://docs.angularjs.org/api/ng/function/angular.element)
But if you include JQuery before Angular then JQuery will replace jqLite and you should be able to use namespaces. That's possibly not what you wanted to hear, but I'm not aware of any other way.
Just for more clearity on how it will goes in directive Within link function just use this make sure attach it to write element as in example it is attached to document
//attach event to document which is big thing
//make sure we remove it once we done with this
$document.on('keydown', handler);
//We need to remove this event as it should be only applicable when user is withing
//the scope of directive parent
scope.$on("$destroy", function () {
$document.off('keydown', handler);
});
//function gets executed when we hit the key specified from form
function handler (event) {
}
bind:
$window.onscroll = function() {
//your code
};
unbind:
$scope.$on('$destroy', function () {
$window.onscroll = undefined;
});
I have an angular directive that needs to listen for click events on $document.
Let's call it clickDirective.
The click listener is added inside its link function.
The problem is that if there are multiple clickDirectives in the document, each one adds a new click listener on $document resulting in a document click handler function firing once for each clickDirective on the page. I only want it to fire once.
However, the callback function should be scoped inside the link function so that it has access to scope, element and attrs for example.
I tried adding it to the compile function and while the click handler only fires once, its callback handler doesn't have access to the goodies inside the link function.
How can that be achieved?
In the comments you say that you want to listen on $document for a click and use it to close a popover bubble. So I think it probably is correct to listen to the event from within the directive.
You can use one instead of on to bind the event only for one event (it'll unbind itself after the first click). So something like this:
module.directive('bubble', function ($document) {
return {
link: function (scope, element, attrs) {
element.on('click', function () {
scope.$apply(function(){
// Show the bubble here
$document.one('click', function () {
scope.$apply(function(){
// Hide the bubble here
});
});
});
});
}
};
});
If you really did want to listen only once on the $document you would use a service, but then you wouldn't have access to scope, element and attrs (because for which element would you expect them to be?). You could create a service that listens once then tracks the bubbles to be closed which would look something like this:
module.factory('bubbleCloser', function ($document, $rootScope) {
var toClose = [];
$document.on('click', function () {
$rootScope.$apply(function () {
toClose.forEach(function (element) {
element.hide();
});
toClose = [];
});
});
return {
addBubbleElement: function (element) {
toClose.push(element);
}
};
});
Which meets your original requirements of only listening to click once but is more complicated and doesn't give any benefits really (but maybe is useful if there's more to this that you haven't shown).
Within an angular controller I am attaching to a websocket service. When the controllers scope is destroyed I obviously want to remove the subscription.
Is it safe to pass the current scope to my service subscription function so it can auto remove on scope destroy? If I dont then each controller who attaches to a socket listener has to also remember to clean up.
Basically is it safe to pass current $scope to a service function or is there a better way of doing this?
I had similar need in my project. Below is the object returned in a AngularJS factory (which initializes WebSocket). The onmessage method automatically unsubscribes a callback if you pass in its associated scope in the second argument.
io =
onmessage: (callback, scope) ->
listeners.push callback
if scope then scope.$on "$destroy", => #offmessage callback
offmessage: (callback) -> listeners.remove callback
The JavaScript equivalence is below.
var io = {
onmessage: function(callback, scope) {
var _this = this;
listeners.push(callback);
if (scope) {
scope.$on("$destroy", function() {
_this.offmessage(callback);
});
}
},
offmessage: function(callback) {
listeners.remove(callback);
}
};
I would not pass the scope. Instead, I would explicitly, in your controller, hook up the unsubscribe.
From http://odetocode.com/blogs/scott/archive/2013/07/16/angularjs-listening-for-destroy.aspx :
$scope.$on("$destroy", function() {
if (timer) {
$timeout.cancel(timer);
}
});
I think having this done explicitly is not as magical, and easier to follow the logic. I think the service would be doing too much if it were to also unsubscribe. What if a controller wants to unsubscribe early?
However, if you do have a very specific use case that's used everywhere, it would be fine to pass the scope in. The amount of time the service needs the scope is very small, basically when the controller first executes so that the service can listen to the $destroy event.