angular.js $destroy event - should I manually unbind? - javascript

I'm trying to figure out if angular base automatically unbinds watchers and scope events bound with $scope.$on(...) or $scope.$watch(...) when scope is destroyed?
Suppose I have following code:
$scope.$on('someEvents', handleSomeEvent);
$scope.$watch('someProperty', handleSomePropertyChange);
Do I need to manually unbind these watchers and events when $destroy event is triggered on scope?

According to Angular documentation on $scope:
'$destroy()' must be called on a scope when it is desired for the scope and its child scopes to be permanently detached from the parent and thus stop participating in model change detection and listener notification by invoking.
Also
Removal also implies that the current scope is eligible for garbage collection.
So it seems when $destroy() is called all the watchers and listeners get removed and the object which represented the scope becomes eligible for garbage collection.
If we look at the destroy() source code we'll see a line :
forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
Which is supposed to remove all the listeners.
As mentioned by #glepretre it applies to the watchers and listeners in the controller. The same doc page listed above says that:
Note that, in AngularJS, there is also a $destroy jQuery event, which can be used to clean up DOM bindings before an element is removed from the DOM.
So if you have specific listeners in the directives you should listen to the $destroy event and do the necessary cleanup yourself

If the scope is in a controller, angular unbind for you. Else you can unbind your event by calling the returned function :
var myevent = $scope.$on('someEvents', handleSomeEvent);
myevent() ; // unbind the event
http://docs.angularjs.org/api/ng/function/angular.bind

As previously answered, Angular indeed takes care of cleaning things for you, whenever possible. So if you do $scope.$on('someEvents', handleSomeEvent);, once the scope is destroyed (eg when you go to another page/view in your app), the event is automatically removed.
One important thing to note though, is that $rootScope is of course never destroyed, unless you quit your app. So if you do $rootScope.$on('someEvents', handleSomeEvent);, you may have to remove the event yourself, depending on where you listen to the event:
if in a controller or directive, then you'll have to remove it manually, else each time you'll instantiate the controller, a new event will be attached, and so handleSomeEvent will be called many times
if in a service, then you do not need to remove it manually, as services are always singleton (note that in Angular service, factory, ... all end up being the same thing)

Related

Attach a regular JavaScript variable to Angular 2 model (input)?

My code scenario:
I have Angular 2 app which has, out of multiple fields, one input field (for lookup), that opens a decade old URL in a popup window (using window.open()) to get some lookup data. And it calls back a function from the window object of the parent page.
The function is defined in index.html page of the Angular 2 application like this:
<script>
function handler(res) {
var value = res;
}
</script>
The need:
The variable value now has to be tied/bound to the input's model named inputModel. Is there way by which this can be achieved?
Please Note: I am aware that this is not a good practice of having <script> tags, everything has to be component based. But its how I have received the task :-P
Thank you!
You can store the value on the window object like this: window['value'] = res;
Now, you can access the value of res inside your angular components like this window['value']
But, the problem now is that this will only work if the handler has already been invoked by the time the angular component reads the value from the window object. Your angular component will not be notified of any subsequent handler calls which update the value property in the window object.
So, you need a mechanism for the global handler function to notify your angular component that the value property on your window object has been updated.
Here is one way of communicating with your angular component from outside angular:
Inside your angular component's template, create a hidden input
Attach a (click) handler to this.
In your global handler function, use document.getElementById('hidden input's id').click() to simulate a click.
Now, the (click) handler you created in step 2, which is inside angular's context will get invoked.
Now, every time the global handler function is invoked, the (click) handler within angular's context will be invoked, essentially telling angular to get the updated value from the window object.
Here is a working Plunker
In this plunker, I've attached the global handler function to the html's onclick handler which is outside angular's context to simulate your requirement. This click event will be communicated to the angular component using the approach I mentioned above.

How to clean up left over $watches

We interop our angularJS web components with a jqxGrid. When the user edits in a cell, we create a custom typeahead editor (written in angular). When the editor is destroyed, I noticed that my $watches array doesn't return back to the previous value.
I am creating a new isolateScope for my directive, which I then compile and then append to the DOM element that JQX passes to me when the editor is needed:
var scope = $rootScope.$new(true);
var customEditor = $compile(directive)(scope);
What do I have to do in order to clean up these $watches?
Its likely that the new scope you are creating via
var scope = $rootScope.$new(true);
Is not being destroyed by the jqxGrid once the jqxGrid is done with the editor.
To clean up the watches, you simply need to ensure that a call is made to
scope.$destroy();
The tricky part is figuring out when to execute the destroy call; I believe the jqxGrid should raise events such as beforeEdit and afterEdit which you can subscribe to; the place where the $destroy() call should be made is within an event handler for the afterEdit event.
Here is the way to clean up watchers effectively.
Should angular $watch be removed when scope destroyed?
Hope this helps.

Debouncing root scope digest invocations

If I invoke $scope.$apply() ten times in immediate succession I presume ten root scope digests will occur.
Can we say that if the call to $scope.$apply() was debounced so that the trailing call was always completed that the final state of the application would be the same as if the debounce was not in effect?
Can we say anything about the duration of successive root scope digests, given that a previous digest has just completed?
Edit:
I would like to clarify the purpose of my question.
Say I have a controller MyController, with an instance instantiated for each instance of a directive MyDirective.
These controller instances listen for an event my-event and trigger a root-scope digest each time my-event is raised.
Ten instances of MyDirective are rendered to the UI via an ng-repeat.
Another component raises an event my-event. Each of the ten MyController instances then trigger a root-scope digest.
If we put the sanity of this state of affairs to one side, my question is this: if I debounce the root-scope digest attempts made by MyController, and ensure that the trailing attempt always gets invoked, is the correctness of the program maintained?
var service = require('my-service');
function MyController($scope) {
this._$scope = $scope;
myService.on('my-event', this.triggerRootScopeDigest.bind(this));
}
MyController.prototype.triggerRootScopeDigest = function() {
this._$scope.apply();
}
The edited question still points to $applyAsync or $evalAsync as your solution.
Here's an example fiddle comparing both $apply() and $applyAsync():
http://jsfiddle.net/635pvkkt/
You'll notice that 10 items are added via ngRepeat, each watching an doApply event, and an doApplyAsync event, to trigger the respective functions.
When you click the button that broadcasts the doApply, it triggers 10 $digest calls, each doing the directives work (in this case, as simple console.log).
The doApplyAsync broadcast, however, causes all 10 directives to do their work in a single $digest.
Of course, a debounced callback would also work. You could pass each directive a reference to a debounced function that is attached to a parent Controller's scope. If that debounce function works correctly and has a long enough debounce-time, it will only apply once. In some situations that's preferred, but the original question feels simple enough (assuming triggering a $digest is the main goal of the event) that substituting $apply for $applyAsync (or $evalAsync, depending on the semantics) seems more appropriate.
EDIT: Though the results are the exact same, this fiddle is more accurate as it triggers real DOM events on the elements directly: http://jsfiddle.net/uh9wxxho/

Turn Backbone events off when Collection is disposed of

I have my Collection subscribing to an event I trigger on the Backbone object itself:
const Items = BaseCollection.extend({
model: ItemModel,
initialize() {
BaseCollection.prototype.initialize.apply(this, arguments);
Backbone.on('api:data:foobar', (data) = > {
});
}
});
However, when I create new instances of this Collection, Backbone adds another listener. When this event is triggered, the callback is fired many many times. Is there a way to either:
Only set this once per Collection lifecycle?
Where is the best place to unsubscribe/turn off this event listener?
The reason this is happening is because your binding the event handler directly to the Backbone object, every time you create a new instance (which calls the initialize method of your collection).
What you want to do instead is invert the relationship and have your collection listen to the event and unsubscribe when you are disposing of your collection. Since version 0.9.9 Backbone provides a built it way to do this using the listenTo.
For example
initialize() {
BaseCollection.prototype.initialize.apply(this, arguments);
this.listenTo(Backbone, 'api:data:foobar', (data) = > {
});
To unsubscribe/turn off this event listener you can use the the stopListening method (they should also be automatically removed if you remove your collection). With views when you call remove it will automatically call stopListening.
If you really want to use bind the event handler directly to the Backbone object and bind it just once you could do this in this by moving the event binding outside of your initialize method, but you will probably be better off using listenTo.

event.preventDefault() not working for routeChangeStart in angularjs app

Hope any angularjs gurus can help me with this.Here is my angularjs code
$scope.$on('$routeChangeStart', function(event, next, current) {
if ($scope.myForm.$dirty) {
if(!confirm("Unsaved, do u want to continue?")) {
event.preventDefault();
}
}
});
It alerts in browser back button click when data is dirty, but on clicking cancel or ok it still completes the route change.Seems like event.preventDefault() is not working.
Can any one point out what may be wrong
I had lots of trouble finding this one, but instead of the "$routeChangeStart" event, you can listen to the "$locationChangeStart" event, for which you can prevent default:
$scope.$on("$locationChangeStart", function(event, next, current) {
if (!confirm("You have unsaved changes, continue navigating to " + next + " ?")) {
event.preventDefault();
}
});
You could also always prevent default, store "next", and display a clean JS modal and decide asynchronously.
$locationChangeStart is currently undocumented but referenced here : https://github.com/angular/angular.js/issues/2109
Fixed exactly after Angular 1.3.7
https://code.angularjs.org/1.3.7/docs/api/ngRoute/service/$route
$routeChangeStart
Broadcasted before a route change. At this point the route services starts resolving all of the dependencies needed for the route change to occur. Typically this involves fetching the view template as well as any dependencies defined in resolve route property. Once all of the dependencies are resolved $routeChangeSuccess is fired.
The route change (and the $location change that triggered it) can be prevented by calling preventDefault method of the event. See $rootScope.Scope for more details about event object.
According to the AngularJS docs (see at $broadcast) you simply cannot cancel an event of type broadcast ($routeChangeStart is of that type):
The event life cycle starts at the scope on which $broadcast was
called. All listeners listening for name event on this scope get
notified. Afterwards, the event propagates to all direct and indirect
scopes of the current scope and calls all registered listeners along
the way. The event cannot be canceled.
This problem was fixed in the newest versions ( >= 1.3.8 ).
Since the arguments supplied to $routeChangeStart are more detailed (and often more useful), if possible, try to update your angular version ...
The problem might persist if you are using a $stateProvider.
In this case use:
$scope.$on('$stateChangeStart', function( event){
.....
event.preventDefault();
});

Categories

Resources