I have a service that make
$rootScope.$broadcast('myEvent', somedata)
from time to time. In controller I do
$scope.$on('myEvent', function (evt, somedata) { $scope.data = somedata })
The question is if I omit
if (!$scope.$$phase) { $scope.$apply(); }
in controller's event listener, then view won't change. Why is that? Is there any better way to do it?.
It is because $apply is a lazy worker and will do the job only there is enough stuff to refresh. You can't control when, unless you explicitly call $scope.$apply.
Yes, there is a better way to do this : call safeApply. Because calling explicitly many $apply can cause conflicts. There is no official safeApply implementation, so can choose you poison :
here : https://coderwall.com/p/ngisma
or there : AngularJS : Prevent error $digest already in progress when calling $scope.$apply()
Related
I am working with ionic 3. So I have this code on controller and it works
$scope.note = 'Lorem...';
$rootScope.$on('Active',function() {
$timeout(function() {
$scope.note = 'test';
},0);
});
But why this not work?
$scope.note = 'Lorem...';
$rootScope.$on('Active',function() {
$scope.note = 'test';
});
What is the best approach for this?
In this context it can be considered a bad practice. Spontaneous use of $timeout usually indicates that a developer doesn't know if the code runs inside or outside of digest cycle and tries to play it safe.
The explantion why this doesn't work stays outside of the scope of posted code, but the reason is that this code runs outside of a digest. This depends on where Active scope event is triggered, and this is what a developer should care about in the first place, since scope events don't necessarily happen inside of digest cycle.
If the event is known to happen outside of a digest, digest-dependent code should be wrapped with $apply:
$scope.$on('Active',function() {
$scope.$apply(function() {
$scope.note = 'test';
});
});
If the event is known to happen both inside and outside of a digest, code should be wrapped with $evalAsync:
$scope.$on('Active',function() {
$scope.$evalAsync(function() {
$scope.note = 'test';
});
});
$timeout(...) is supposed to be used only when it's behaviour is wanted, i.e. one tick delay or more and a digest.
As it was suggested by #georgeawg, it is also a bad practice to use $rootScope as global event bus in controllers (they have access to child scopes). This is basically an antipattern that may cause memory leaks. Considering that an event was $broadcasted, it will propagate to child scopes. As a rule of thumb, it should be $scope.$on(...), unless there are reasons why it should be done on $rootScope specifically.
This happens when the view does not get notified of a change in controller. It usually happens for ng-repeat arrays/objects or inside the forms that do not use a global object. The $timeout solution works or you can call $scope.$apply(). Please look at this document for more information:
https://www.sitepoint.com/understanding-angulars-apply-digest/
I guess the title is pretty much clear what I am asking. I have created this fiddle : http://jsfiddle.net/Sourabh_/HB7LU/13142/
In the fiddle I have tried to replicate an async scenario. This is just an example but in an AJAX call if I don't use $scope.$apply() the list does not get updated. I want to know if it is safe to use $scope.$apply() every time I make an AJAX call to update a list or is there some other mechanism I can make use of?
Code I have written to replicate the scenario(same as in fiddle):
HTML
<div ng-controller="MyCtrl">
<li ng-repeat="item in items">
{{item.name}}
</li>
<button ng-click="change()">Change</button>
</div>
JS
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.items = [{name : "abc"},{name : "xyz"},{name : "cde"}];
$scope.change = function(){
test(function(testItem){
$scope.items = testItem;
//$scope.$apply();
})
}
function test(callback){
var testItem = [
{name : "mno"},
{name : "pqr"},
{name : "ste"}
];
setTimeout(function(){callback(testItem)},2000);
}
}
Edit It was not clear the OP was trying to mock a backend call. Even so, using the $timeout service is a great way to avoid the need of calling $scope.$apply manually and is a more generally applicable solution than using a Promise (in cases where you're i.e. not calling $http it doesn't always make sense to force your changes into the next cycle by wrapping them with a Promise).
Update your code to use the $timeout service and it should work without having to call $apply.
$timeout is a wrapper around the native setTimeout with an important difference: $timeout will delay the execution at least until the next $digest cycle runs.
So passing in no delay will still delay the execution up until the next cycle. Passing in 2000 will delay the execution up to the next cycle after 2000ms.
Hence, this is an easy trick to make sure your changes are picked up by Angular without ever having to call $apply manually (which is considered unsafe in any case)
function MyCtrl($scope, $timeout) {
$scope.items = [{name : "abc"},{name : "xyz"},{name : "cde"}];
$scope.change = function(){
test(function(testItem){
$scope.items = testItem;
//$scope.$apply();
})
}
function test(callback){
var testItem = [
{name : "mno"},
{name : "pqr"},
{name : "ste"}
];
$timeout(function(){callback(testItem)},2000);
}
}
If you want to immidate an API-Rest-Call, use the returned promise in your Controller instead setting the scope inside the Rest-Call.
$http.get('uri')
.success(function(data) {
$scope.items = data
});
Avoid using $apply(). From the Angular GitHub Repo:
$scope.$apply() should occur as close to the async event binding as
possible.
Do NOT randomly sprinkle it throughout your code. If you are doing if
(!$scope.$$phase) $scope.$apply() it's because you are not high enough
in the call stack.
To your question:
If you find yourself in a situation where you need $apply(), rethink your structure.
Just for safety reason: Never use $apply()
You need to use $apply every time you use something that is not "angular way", like Anzeo told about $timeout.
For example if you use jQuery's http instead of angular's $http, you will have to add $scope.$apply.
The $apply, should be used when the code is not executed in a angular digest loop. In normal circumstances we will not need to use it, but we might have to use it if we have a code that is called from a jQuery event handler or from methods like setTimeout(). Even if you have a function that is called from another angular function like a watch or angular event handlers you need not use $apply() as those scripts are executed in the digest cycle.
One safe way is to check the $scope.$$phase param before calling $scope.$apply() like
if($scope.$$phase){
$scope.$apply();
}
In your case but you can use $timeout as suggested in the another answer
What is $$phase in AngularJS?
Why is using if(!$scope.$$phase) $scope.$apply() an anti-pattern?
All the above answers give some information but they did not answer few doubts I had or at the least I did not understand. So I'm giving my own.
It is very clearly stated in angular docs when to use $apply
the call backs of $http or $timeout or ng-click, ng-..... have $apply() wrapped in them. Therefore when people say you don't have to use $apply() when you do angular way of doing things, this is it. However, one of the answers mentions angular event handlers are also wrapped with $apply(). This is not true or by that the user means just ng-click kind of events (again ng-....). If the event is broadcast on rootScope (or any scope for that matter) outside of $http or $timeout or ng-click, for eg: from a custom service, then you need to use $apply() on your scope although $rootScope.$broadcast is also angular way of doing things. in most of the scenarios we will not need this because the state of the app changes when something happens. ie, clicks, selection change, etc... and these are in angular terms are already using $apply() when we use ng-click ng-change respectively. Handling server side events using signalr or socket.io and while writing custom directives where the necessity to change just the directive's scope are few examples where it is very essential to use $apply()
As #gruberb pointed out in the comments, if you tried to mock a REST call, you better use a promise than $apply.
For this you need to use $q service to create and return a promise. Then simply call it and work with you result by calling then() method on the returned promise.
function MyCtrl($scope, $q) {
$scope.items = [{name : "abc"},{name : "xyz"},{name : "cde"}];
$scope.change = function(){
test().then(function (items) {
$scope.items = items;
$scope.$apply();
});
};
function test() {
var defered = $q.defer();
var testItem = [
{name : "mno"},
{name : "pqr"},
{name : "ste"}
];
setTimeout(function() {
defered.resolve(testItem);
},2000);
return defered.promise;
}
}
A better way is to use $scope.$applyAsync(); instead of $scope.$apply();
The reason to avoid use of $scope.$apply() is given here:
Error: [$rootScope:inprog] digest in progress. Fix
I would like to notify multiple controllers of external changes from a service.
I know that i can do this by using a deferred object and calling notify on it and registering a callback on that.
e.g
// in service
$timeout(function() {
defered.notify('In progress')}
, 0)
//in controller
var promise = myService.promise
promise.then(function(success) {
console.log("success");
}, function(error) {
console.log("error");
}, function(update) {
console.log("got an update!");
}) ;
Is there a way to remove my notify callback when the controller is destroyed?
You can use $on, $broadcast and $emit to achieve a similar behavior.
Just $broadcast(name, args); an event and register a listener $on(name, listener);.
So when you got your data you just broadcast to everybody that you've got it.
$broadcast('gotTheData', actualDataObject);
Then in your controllers
$on('gotTheData', function(event, actualDataObject)
{
... update controller data ...
});
Note that in the example I've added only one actualDataObject, but you can pass multiple arguments from the $broadcast to $on. For more details refer to the documentation (see link above).
As per the comments to #Scott's answer you highlighted one important aspect: you need to unregister the $on when you destroy the controller through the "Returns a deregistration function for this listener." (see the docs) at $on returns. There is a question that targets specifically this issue: How can I unregister a broadcast event to rootscope in AngularJS?.
EDIT DUE TO FURTHER QUESTION DETAILS
You could cancel the $timeout when you destroy the controller:
Create the $timeout as so...
$scope.myTimeout = $timeout(function() {
defered.notify('In progress')}
, 0);
In the controller add this:
$scope.$on('$destroy', function () {
$timeout.cancel($scope.myTimeout);
});
In my experience this appears to clean everything up.
To clear just the notify callback itself, use the suggested $broadcast, $emit(see #Pio's answer) A guide to the difference between them is here.
Its important to know the difference!
In AngularJS, everywhere I read it says that you almost rarely ever call $apply() yourself, as it's a bad idea somehow (usually left unclear.)
Can someone explain to me:
How to avoid using $apply() but still updating variables in $scope
Why is it so terribly bad to call $apply()
And how to determine when it is OK to use $apply()
Example code:
app.controller('ExampleController', function($scope) {
$scope.variableForView = 'Initial value';
// for demonstration purposes
someEvent.on('eventFire', function(data) {
$scope.variableForView = data;
$scope.$apply(); // ???
});
});
Basically, $apply runs the $digest. If $digest is already running, then you can't initiate another $digest.
I have created the method $safeApply(), that I add to the prototype of my $rootScope inside the run() commend:
// $safeApply only calls $apply if $digest is not running
// Otherwise, will only call back fn()
Object.getPrototypeOf($rootScope).$safeApply = function(fn)
{
var phase = $rootScope.$$phase;
if (phase == '$apply' || phase == '$digest') {
if (_.isFunction(fn)) fn();
} else {
this.$apply(fn);
}
};
This basically makes sure you are not already in the $digest cycle before calling $apply. If you are (which means it is not necessary to call $apply, I just run the the fn. I never use $apply() and always use $safeApply().
Use the binding methods angular provides. Then it will do the $apply for you. The reason they say manually $applying is bad is because you should be using the angular methods for binding and if you are not using them and having to $apply manually, then your doing something wrong.
app.controller('ExampleController', function($scope) {
$scope.variableForView = 'Initial value';
// for demonstration purposes
$scope.$on('someEventFire', function(data) {
$scope.variableForView = data;
});
});
Also see https://github.com/angular/angular.js/wiki/When-to-use-%24scope.%24apply%28%29
Reading this excellent book, Mastering Web Development in AngularJS, I ran across this code:
var Restaurant = function ($q, $rootScope) {
var currentOrder;
this.takeOrder = function (orderedItems) {
currentOrder = {
deferred:$q.defer(),
items:orderedItems
};
return currentOrder.deferred.promise;
};
this.deliverOrder = function() {
currentOrder.deferred.resolve(currentOrder.items);
$rootScope.$digest();
};
this.problemWithOrder = function(reason) {
currentOrder.deferred.reject(reason);
$rootScope.$digest();
};
My understanding is that the $rootScope.$digest(); calls are made in order to alert Angular that the Promise's state has been updated.
Is my understanding correct? Also, is it necessary to make the above $rootScope.$digest(); calls?
$scope.$digest() is what processes all of the $watch events that are on the current and children scope. It essentially manually tells the scope to check if a scope variable has changed. You don't generally want to use this when you are inside of a controller or a directive, because the $scope.$apply() function calls the $digest anyway and it is called when you mutate a scope variable.
Checkout this link for an example.
You don't need a $rootScope.$digest here because resolving/rejecting the promise will fire a $rootScope.$digest internally, as $interval,$timeout,and $http (after request finished) do that for you. And this $digest can throw errors of $digest already in progress.