AngularJS - Unbind $window of an event from specific directive - angularjs

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;
});

Related

How to remove Cordova specific events outside Angular controller?

Imagine I have a controller which handles, for example, view changes:
function Controller($scope){
var viewModel = this;
viewModel.goBack= function(){
viewModel.visible = visibleLinks.pop(); //get last visible link
viewModel.swipeDirection = 'left';// for view change animation
}
}
But I want to handle it not only for example with HTML buttons inside <body>, but also with Back button on device. So I have to add Event Listener for deviceready event, and also explicit call $scope.$apply() in order to fact, that it is called outside of AngularJS context, like this:
document.addEventListener("deviceready", function(){
document.addEventListener("backbutton", function(){
viewModel.goBack();
$scope.$apply();
}, false);
}, false);
}
But I also want to follow (relatively :) ) new controllerAssyntax, cause this is recommended now e.g. by Todd Motto: Opinionated AngularJS styleguide for teams and it allows to remove $scope from controllers when things like $emit or $on are not used. But I can't do it, case I have to call $apply() cause my context is not Angular context when user clicks on device back button. I thought about creating a Service which can be wrapper facade for cordova and inject $scope to this service but as I read here: Injecting $scope into an angular service function() it is not possible. I saw this: Angular JS & Phonegap back button event and accepted solution also contains $apply() which makes $scope unremovable. Anybody knows a solution to remove Cordova specific events outside Angular controller, in order to remove $scope from controllers when not explicity needed? Thank you in advance.
I don't see a reason why to remove the $scope from the controller. It is fine to follow the best practice and to remove it if not needed, but as you said you still need it for $emit, $on, $watch.. and you can add it $apply() in the list for sure.
What I can suggest here as an alternative solution is to implement a helper function that will handle that. We can place it in a service and use $rootScope service which is injectable.
app.factory('utilService', function ($rootScope) {
return {
justApply: function () {
$rootScope.$apply();
},
createNgAware: function (fnCallback) {
return function () {
fnCallback.apply(this, arguments);
$rootScope.$apply();
};
}
};
});
// use it
app.controller('SampleCtrl', function(utilService) {
var backBtnHandler1 = function () {
viewModel.goBack();
utilService.justApply(); // instead of $scope.$apply();
}
// or
var backBtnHandler2 = utilService.createNgAware(function(){
viewModel.goBack();
});
document.addEventListener("backbutton", backBtnHandler2, false);
});
In my case I was simply forwarding Cordova events with the help of Angular $broadcast firing it on the $rootScope. Basically any application controller would then receive this custom event. Listeners are attached on the configuration phase - in the run block, before any controller gets initialized. Here is an example:
angular
.module('app', [])
.run(function ($rootScope, $document) {
$document.on('backbutton', function (e) {
// block original system back button behavior for the entire application
e.preventDefault();
e.stopPropagation();
// forward the event
$rootScope.$broadcast('SYSTEM_BACKBUTTON', e);
});
})
.controller('AppCtrl', function ($scope) {
$scope.$on('SYSTEM_BACKBUTTON', function () {
// do stuff
viewModel.goBack();
});
});
Obviously in the $scope.$on handler you do not have to call $scope.$apply().
Pros of this solution are:
you'll be able to modify an event or do something else for the entire application before the event will be broadcasted to all the controllers;
when you use $document.on() every time controller is instantiated, the event handler stays in the memory unless you manually unsibscribe from this event; using $scope.$on cares about it automatically;
if the way a system dispatches Cordova event changes, you'll have to change it in one place
Cons:
you'll have to be careful when inheriting controllers which already have an event handler attached on initialization phase, and if you want your own handler in a child.
Where to place the listeners and the forwarder is up to you and it highly depends on your application structure. If your app allows you could even keep all the logic for the backbutton event in the run block and get rid of it in controllers. Another way to organize it is to specify a single global callback attached to $rootScope for example, which can be overriden inside controllers, if they have different behavior for the back button, not to mess with events.
I am not sure about deviceready event though, it fires once in the very beginning. In my case I was first waiting for the deviceready event to fire and then was manually bootstrapping AngularJS application to provide a sequential load of the app and prevent any conflicts:
document.addEventListener('deviceready', function onDeviceReady() {
angular.element(document).ready(function () {
angular.bootstrap(document.body, ['app']);
});
}, false);
From my point of view the logic of the app and how you bootstrap it should be separated from each other. That's why I've moved listener for backbutton to a run block.

Deregistration of event handlers in Angular

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);
});
}
};
});

unbind an angular element

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.

How to only listen once on $document from inside directive

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).

Safe to pass current $scope from controller to angularjs service function

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.

Resources