If I can't use $apply() on $scope() what can I use? - javascript

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

Related

When is it safe to use $scope.$apply()?

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

How to update $scope immediately when dealing with non angular code?

I have to deal with an non angular library and need to create a comunication between them.
<div id="MoipWidget" data-token="{{personaltoken}}" callback-method-success="successCB" callback-method-error="errorCB"></div>
Every time that the page is loaded, I have to get a token from my server.
$http.post('https://example.org', obj)
.success(function(data){
$scope.personaltoken = data.token;
//Here I call the non angular library and I get and error telling me that the token is undefined.
//If I run the code from inside a $timeout works as I need...
})
.error(function(data){
alert('error');
});
I've also tried to run inside $scope.$apply but I get an error telling that $digest already in progress
The non angularjs library that I have to call is simple is just two lines.
var settings = {}
LibraryCall(settings);
How can I update the model immediately?
I've tried to use $scope.$evalAsync as #Kjell suggested but did not work.
After reading more about $scope, I've found what I needed.
$scope.$applyAsync(function(){
var settings = {}
LibraryCall(settings);
});
$scope.$applyAsync will schedule the invocation of $apply to occur at a later time.
https://docs.angularjs.org/api/ng/type/$rootScope.Scope
I removed the error callback for brevity, don't do it in your code :)
I suppose the code you call is asynchronous, if it's not, you should not have any $scope updating problem (because all angular promises call $apply already)...
This should work:
$http.post('https://example.org', obj).success(function(data){
$scope.personaltoken = data.token;
otherLibrary.doSomething(data.token, function(error, result) {
$scope.changeSomething = 'toHey';
$scope.$apply();
});
});
This shoud also work:
$http.post('https://example.org', obj).success(function(data){
$scope.personaltoken = data.token;
otherLibrary.doSomething(data.token, function(error, result) {
$scope.$apply(function() {
$scope.changeSomething = 'toHey';
});
});
})
This shoud raise the $digest already in progress error, because $http does wrap the promise callback on a $apply call already.
$http.post('https://example.org', obj).success(function(data){
$scope.personaltoken = data.token;
$scope.$apply(function() {
otherLibrary.doSomething(data.token, function(error, result) {
$scope.changeSomething = 'toHey';
});
});
})
Try using either $scope.$evalAsync() or $scope.$applyAsync().
They are made for stuff like this. It will execute the code later in time. Not that different from $timeout, but potentially faster.
$scope.$evalAsync(function(){
var settings = {}
LibraryCall(settings);
})
Edit: Just to quote Ben Nadel on the difference between $timeout and $evalAsync, from this post:
So, in essence, $scope.$evalAsync() combines the best of both worlds:
When it can (which is most of the time), it will evaluate your
expression in the same tick; otherwise, it will evaluate your
expression in a later tick, which is exactly what $timeout() is doing.

why the need to use 'timeout' in angular

This is probably a total newb question...apologies, but I can't get my head around it.
In a lot of angular documentation/examples I see asynchronous functions wrapped in 'timeout' blocks. Many are wrapped in setTimeout() and require the explicit use of
if (!$scope.$$phase) {
$scope.$apply();
}
Given that angular provides $timeout, the above code just seems outdated or wrong and within angular the use of $timeout should always be preferred. However, I digress.
Here is a snippet of some example code taken from: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/
var myModule = angular.module('myModule', []);
// From this point on, we'll attach everything to 'myModule'
myModule.factory('HelloWorld', function($timeout) {
var getMessages = function(callback) {
$timeout(function() {
callback(['Hello', 'world!']);
}, 2000);
};
return {
getMessages: getMessages
};
});
I see this wrapping of code in timeout blocks everywhere particularly related to asynchronous calls. But can someone explain why this is needed? Why not just change the code above to:
var myModule = angular.module('myModule', []);
// From this point on, we'll attach everything to 'myModule'
myModule.factory('HelloWorld', function() {
var getMessages = function(callback) {
callback(['Hello', 'world!']);
};
return {
getMessages: getMessages
};
});
Why wouldn't the code snippet above work just fine?
The use of $timeout or $interval is to implicitly trigger a digest cycle. The process is as follows:
Execute each task in the callback function
Call $apply after each task is executed
$apply triggers a digest cycle
An alternative is to inject $rootScope and call $rootScope.$digest() if you are using services that don't trigger a $digest cycle.
Angular uses a dirty-checking digest mechanism to monitor and update values of the scope during
the processing of your application. The digest works by checking all the values that are being
watched against their previous value and running any watch handlers that have been defined for those
values that have changed.
This digest mechanism is triggered by calling $digest on a scope object. Normally you do not need
to trigger a digest manually, because every external action that can trigger changes in your
application, such as mouse events, timeouts or server responses, wrap the Angular application code
in a block of code that will run $digest when the code completes.
References
AngularJS source: intervalSpec.js
AngularJS source: timeoutSpec.js
$q deferred.resolve() works only after $timeout.flush()
AngularJS Documentation for inprog | Digest Phases
The $timeout in your example is probably used just to simulate an async function, like $http.get. As to why $timeout and not setTimeout: $timeout automatically tells angular to update the model, without the need to call $scope.$apply()
Also, consider the following example:
$scope.func = function(){
$scope.showSomeLoadingThing = true;
//Do some long-running stuff
$scope.showSomeLoadingThing = false;
}
No loading thingy will be shown, you would have to write it like this:
$scope.func = function(){
$scope.showSomeLoadingThing = true;
$timeout(function(){
//Do some long-running stuff
$scope.showSomeLoadingThing = false;
});
}

Angular's $rootScope.$digest

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.

Why should I make $scope.$apply()?

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

Categories