Is calling $scope.$digest within $scope.$on fundamentally incorrect? - javascript

I have inherited some AngularJS code, and I know very little about it (the code and Angular both). The code that I inherited contains several places where $scope.$digest is called within a $scope.$on method within a controller. For example:
$scope.$on("ScriptEditorEnabled", function (e, enabled) {
$scope.scriptEditorDisabled = !enabled;
$scope.$digest();
});
In my context, this is causing the popular $digest already in progress error (AngularJS : Prevent error $digest already in progress when calling $scope.$apply()). Isn't an $on method part of the $scope object, and thus, any changes to its data would be picked up automatically? If so, doesn't that mean that calling $digest within one (either directly or indirectly), just plain incorrect?
I've been simply removing these calls, seemingly without any loss in functionality, and the errors go away. Is there any dangers I should be aware of when doing this?

$scope.$on gets called in response to $scope.$broadcast or $scope.$emit. The $scope.$on should always assume that it runs inside a digest loop. Neither $apply nor $digest calls should be needed inside the $on handler.
Angular is pretty clear about it in the When to use $scope.$apply(). If you dispatch events from non-angular environments, you should wrap the $emit or $broadcast call in $apply instead of calling it in the event handler. Example:
$rootScope.$apply($rootScope.$broadcast('recievedMsg', msg));
It's dispatcher responsibility to enter the digest loop if it knows there isn't one started already.

Isn't an $on method part of the $scope object, and thus, any changes
to its data would be picked up automatically?
No. $scope.$on callback is triggered by $scope.$emit or $scope.$broadcast. If you inspect source code of both of them, you'll see that they don't by itself trigger $digest loop that checks for changes. This means that callback on $scope.$on can be executed from within $digest loop as well is outside of it. A good approach is to have a convention which defines whether the $scope.$emit or $scope.$broadcast$ should be executed inside $digest loop or the callback within $scope.$on should be executed inside $scope.$apply. Usually, it's the code that triggers event ensures the digest loop by using $scope.$apply().
A solution could be to use $scope.$evalAsync instead of $scope.$apply to avoid digest already in progress when calling $scope.$apply() error.

Related

Trigger $digest from external code

I have an application that exposes an methods (from various angular services) to code that is loaded and eval'd at runtime. What is the best way to guarantee $digest triggers when these methods are called from outside angular, taking into account that they might also be called from within angular? Should I expose a separate interface wrapped in $scope.$apply instead of exposing the service methods directly?
Quick solution would be to call $evalAsync. From documentation:
" if this function is called outside of a $digest cycle, a new $digest cycle will be scheduled. " https://docs.angularjs.org/api/ng/type/$rootScope.Scope
My advice to expose something outside Angular is to expose element (by providing ID or selector) where you have scope or (better) controller attached and then use 'angular.element(someDomEkenent).scope().someFunction()'
In this case you would avoid possibility to capture unessesary objects that can lead to memory leaks that is very hard to debug
call $timeout() function with no time.
for example
function someEventOutsideAngualer(value) {
$timeout(function(){
$scope.bindedValue = value;
})
}
The idea is that when $timeout callback is executed, in our case it will be picked up with the next $digest. Please do not use $scope.$apply() and $digest triggers. They cause more problems than they solve, if not properly understood

ng-if executes method continuously

I have the following code:
data-ng-if="(fooCtrl.displayControl(1))
displayControl method return a true or false.
What I found is thatngif is constantly listening for this even after it has been evaluated once. How I found this, was by pointing a break point on the method. After a few F8(in debugger), it will continue as normal.
Why is ngif is executing the method continuously?
Angular runs each watcher function on each digest cycle, so this is normal that your function gets checked multiple times. If you don't want this behaviour you can consider one-time binding (Angular 1.3.x).
data-ng-if=":: fooCtrl.displayControl(1)"
All your built-in directives will be triggered in every $digest cycle, see more about angular digest cycle here and here
so be aware of using directives like ng-show or ng-if and don't make them listen on a function that does a lot of processing or function that have some loops and that kind of stuff, keep them listening on already determined booleans or simple functions

When should I call $digest instead of $apply?

I have seen both
$rootScope.$apply();
and
$rootScope.$digest();
being used inside a factory. However Angular docs say:
Usually, you don't call $digest() directly in controllers or in
directives. Instead, you should call $apply() (typically from within a
directive), which will force a $digest().
If you want to be notified whenever $digest() is called, you can
register a watchExpression function with $watch() with no listener.
In unit tests, you may need to call $digest() to simulate the scope
life cycle.
One of the advantages of not calling $digest directly is to avoid running an infinite loop.
Are there any cases in which one should use $digest over $apply (apart from testing, as mentioned in the quote)?
When your application becomes sufficiently large enough that digests become prohibitively expensive, you can call digest on scopes that you're sure don't cause side-effects on other parts of your application.
For instance you may have introduced a jQuery calendar plugin, that you would ordinarily call $apply on to observe its bindings to a scope. This calendar might be for a feedback form so it is, so to speak, independent to the rest of your application, which may be a large real-time data feed that has thousands of watchers.
A scope.apply on your calendar would go through your whole application and do dirty checks on all your thousands of watchers for your real-time feed when it is unnecessary. In this situation, you should call $scope.$digest() because you know only the calendar model has changed.
I think the documentation is unhelpful here. As far as i know:
$digest will process all watchers of the current scope and its children.
$apply() is more or less a wrapper around $rootScope.$digest and thus will run a full digest on all scopes.
Unlike the doc says you can use both methods to pull an update from outside Angular into the digest cycle. As already said, $apply may invoke a full digest cycle, meaning $rootScope.$digest(). Calling $digest instead will most likely prevent that from happening and is better for performance.
If you want to be absolutely certain that Angular recognizes the change, use $apply. If you know what you are doing and only need to update the current scope (or its children) use $digest.

How do I create a globally-available callback API that DOESN'T use $scope?

I want my main module to offer an api which allows any directive, controller, factory, etc. to register a function to be called when a user resizes a div.
Right now I'm successfully doing this by using jQuery's resizable callback to update x and y variables on scope then calling $apply(). The problem, as you can imagine, is that when a user is dragging a div, it calls the resize callback very rapidly. Rapidly calling $apply() isn't ideal for performance reasons.
I thought about using a service/factory, but there's an indeterminate number of resizable divs/directives and I'm not sure a singleton makes sense here.
Is there a way I can create an "informal" API that doesn't use $scope, doesn't trigger digest cycles, and doesn't use additional libraries? Perhaps there's a way using controllers and require: ^myCtrl? Do I need to store all the registered callback functions in an array?
What you're doing sounds pretty hacky, so it's probably not a big leap for you to publish angularjs events that get listened to on their scope. So to 'register' a function you listen on the scope for some custom event you create, execute whatever you want in the callback. Get your jquery to broadcast the event on $rootScope.
Have a read about angularjs events
Working with $scope.$emit and $scope.$on

AngularJS - Clear all $watch on $destroy

I have a few $watch on my scope. I know that I a $watch will return its deregister function, and I can just call it to deregister it, but is there a way to just deregister all watchers on a scope in one simple command?
I want to basically deregister all of my watchers on $destroy, and I don't want to save ALL of these watchers, and call them all one by one.
There is a $$watcher attribute on the scope. Can I somehow use that?
Basically when $destroy is broadcasted from the scope, it also means that the scope has invoked the $destroy() method. This means that everything related to that scope is also being cleaned up, including the watchers, this part of the source code shows how the $destroy() method empties the watchers of that scope.

Categories

Resources