You can see the problem in this plunk with Chrome or FireFox's console open:
http://plnkr.co/edit/jmDcxqQ48PmeHt3EPVMU
When you click "Push!" for the first time, the watch function doesn't execute. But if you push it again, it does execute. It's like it ignores the first push. However, you can clearly see the dataPoints array changes after the first push due to the timeout function that is executing in the background. What's weirder, is if you comment out the jQuery fadeIn wrapper, the code works as expected (the watch function is executed on the first button push).
What's going on here? Why is the $watch function not executing when jQuery().fadeIn is used?
jQuery('#mytest').fadeIn(200, function() {
$scope.mydata.dataPoints.push(1);
$scope.$$phase || $scope.$apply();
})
When the fadeIn callback runs, it runs "outside" Angular, so we need to call $scope.$apply() to run a digest cycle. However, if the object is already visible, it seems that jQuery runs the callback immediately (so we're still "inside" Angular), so we don't need to (and shouldn't) call $scope.$apply().
The easy way to handle both cases is to check to see if we are already in a digest cycle using private property $$phase. A cleaner implementation would be to not call fadeIn() if the div is already visible.
Related
I am having trouble understanding using jQuery with Angular's digest cycle.
I am using an ng-repeat= to load content. the content (using $compile) ends up being a div with with two child divs and several grandchild elements. after loading the arrary for the ng-repeat, I want to use jQuery's children.each() like so:
callBack = function(index) {
var objParent = $('#' + $scope.panes[index].windID); //entire window
var objChild = [];
//objChild[0] = title bar
//objChild[1] = contentpane
objParent.children().each(function(i){
objChild[i] = $(this);
});
//more code
}
The problem is that as my open window method got larger with more features, somehow Angular's digest started working differently. What I mean is that before I added my maximize/restore/minimize methods and buttons to match, when my controller got to the code above, the window was already digested through scope and showed in the dom (although hidden with {display: none} for jQuery's fade in) meaning that the above code worked.
However now that there is more going on to be digested, when it gets to that point the digest is not complete, and the child finder fails since that DOM has not been updated yet. I have to use $timeout, and I increasingly have to increase the time. I have to wait around 600ms right now, and I feel that is to slow from icon click to window showing.
Now, If I add a $scope.$apply() right before the $timeout(), it acts like it's just forcing the digest to hurry up. I can reduce the time back to 0ms, and everything works instantly. Yet I get a digest in progress error. I thought the $timeout waited for the end of the digest anyways?
I've tried various methods, even $$postDigest, but the only thing that seems to work is forcing an $apply resulting in a digest in progress error that the end user will never see nor even care about since it actually makes it all work. That would not me so much of a problem, but it bugs me because after opening and closing my window "panes" the digest in progress error can multiply in the console.
Here's a link to a little bit older version: AkadineWebOS
The app is more jQuery than Angular, I'm just using Angular for it's http module and to make it easy to push and splice my window array and let the ng-repeat magic handle the dom. jQuery does all the heavy lifting with dragging, resizing, fadein/out, centering windows on open, and most everything else.
So here is my question: Can someone please explain to me what I am missing about the way that Angular's digest works?
As Claise said, jQuery is not asyc like angular. I actually just had to change the $timeout() to a plain ol' setTimeout(), basically asycing my jQuery callback. Now it works great!
As far as i know, $timeout is a promise object in angular, which means the code will go on running without waiting for the timeout to end.
However, when i used it in my ionic code, for some reason it did and the whole loading of the page froze for 6 seconds. Can you please explain why?
$scope.$on("$ionicView.Enter", function( scopes, states ) {
$timeout(function(){
// some function i wrote
}, 6000);
});
Your assumption around the code continuing to run is wrong - otherwise what would the point of calling $timeout be? It's an Angular wrapper and recommended to use in place of window.setTimeout() but works exactly the same. The code above will execute after a 6000ms delay.
[ADDED] From the Angular API docs: "The return value of calling $timeout is a promise, which will be resolved when the delay has passed and the timeout function, if provided, is executed."
I'm very confused when a digest cycle is happening, is it called periodically based on a timer every 50ms (as it says here and implied here) or is it called after every event that enters the angular context (as it says here, here and here) ?
Example when it is matter:
In my model, I have a variable called myVar with the value of 3.
In my HTML, I have {{myvar}}.
An event such as a button click is fired and raises a handler in the controller, the code inside the handler is:
$scope.myVar = 4;
// some heavy actions takes place for 3 seconds...
$scope.myVar = 5;
Assuming the UI thread is not blocked, what will the user see after the button click? will he see only 5 or will he see 4 and after 3 seconds 5?
I think the description of the digest cycle at http://blog.bguiz.com/post/60397801810/digest-cycles-in-single-page-apps that it is
code that runs at an interval
is very misleading, and to be honest, when referring to Angular, I would even say wrong. To quote Pawel Kozlowski, Mastering Web Application Development with AngularJS
AngularJS does not use any kind of polling mechanism to periodically check for model changes
To prove there is no polling, if you have a template of
<p>{{state}}</p>
and controller code of
$scope.state = 'Initial';
// Deliberately *not* using $timeout here
$window.setTimeout(function() {
$scope.state = 'Changed';
},1000);
as in this plunker, then the string shown to the user will remain as Initial and never change to Changed.
If you're wondering why you often see calls to $apply, but not always, it is probably because the various directives that come with Angular, such as ngClick or ngChange will call $apply themselves, which will then trigger the cycle. Event listeners to native JS events directly will not do this, so they will have to deliberately call $apply to have any changes made reflected in templates.
The digest process is kicked-in when any of the following occur as part of angular context:
DOM Events (like ng-click etc.)
Ajax with callbacks ($http etc.)
Timers with callbacks ($timeout etc.)
calling $apply, $digest
etc.
It is important to note that the normal browser related DOM events (onclick etc.) and setTimeout would not trigger a digest process as they work out of "Angular Context".
I learnt the above from the following:
The above is a quick snapshot from a very in-depth tutorial available here: https://www.youtube.com/watch?v=SYuc1oSjhgY
Any AngularJS scope variable when handled from outside (including ajax) needs a $apply().
setTimeout is Javascript function So $apply is needed to update angularjs values.
$timeout is a angularjs function which returns promise and takes care of the current scope and runs in the same digest cycle.
So need not of $apply() function to update values.
I found this code snippet which is part of a angular directive someone wrote for bootstrap modal.
//Update the visible value when the dialog is closed
//through UI actions (Ok, cancel, etc.)
element.bind("hide.bs.modal", function () {
scope.modalVisible = false;
if (!scope.$$phase && !scope.$root.$$phase)
scope.$apply();
});
I understood that this part is for the latter half of two way binding we bind to hide.bs.modal event and update modal when UI changes.
I just wanted to know why is the person checking $$phase for scope and rootScope before calling apply ?
Can't we straightaway call apply ?
What is $$phase here?
I tried searching a lot, couldn't find any good explanation.
EDIT:
I found where I saw the example:
Simple Angular Directive for Bootstrap Modal
$$phase is a flag set while angular is in a $digest cycle.
Sometimes (in rare cases), you want to check $$phase on the scope before doing an $apply. An error occurs if you try to $apply during a $digest:
Error: $apply already in progress
Davin is totally correct that it's a flag that angular sets during the digest cycle.
But don't use it in your code.
I recently got a chance to ask Misko (angular author) about $$phase, and he said to never use it; it's an internal implementation of the digest cycle, and it's not future safe.
To make sure your code continues to work in the future, he suggested wrapping whatever you want to "safe apply" inside of a $timeout
$timeout(function() {
// anything you want can go here and will safely be run on the next digest.
})
This comes up a lot when you have callbacks or other things that might resolve during the digest cycle (but don't always)
Here's an example snippet from when I was dealing with one of google's libraries: (The rest of the service this was from has been left off.)
window.gapi.client.load('oauth2', 'v2', function() {
var request = window.gapi.client.oauth2.userinfo.get();
request.execute(function(response) {
// This happens outside of angular land, so wrap it in a timeout
// with an implied apply and blammo, we're in action.
$timeout(function() {
if(typeof(response['error']) !== 'undefined'){
// If the google api sent us an error, reject the promise.
deferred.reject(response);
}else{
// Resolve the promise with the whole response if ok.
deferred.resolve(response);
}
});
});
});
Note that the delay argument for $timeout is optional and will default to 0 if left unset ($timeout calls $browser.defer which defaults to 0 if delay isn't set)
A little non-intuitive, but that's the answer from the guys writing Angular, so it's good enough for me!
In that example, the element binding will get executed from a non-angular event. In most cases, it is safe to just call $apply() without checking the phase.
If you look at the rest of the code, however, there is a $scope function called showModal(). This function calls into the non-angular code which will likely cause a "hide.bs.modal" event to fire. If the event fires via this route, then the call stack is within a $digest.
So, this event does fall within the rare case of a function that will get called from both angular-managed code AND non-angular code. Checking the $$phase in this case is necessary because you don't know how the event originated. If the $$phase is set to something, then the digest loop will finish to completion and $apply() doesn't need to be called.
This pattern is often referred to as "safe apply".
my understanding is it is good to use when digesting or applying the scope. If it is truthy it means that there is currently a $digest or $apply phase in progress. If you are getting related errors you can do $scope.$$phase || $scope.digest(); which will only digest if $scope.$$pahse is falsy.
You can use $scope.$evalAsync() method rather than using the $scope.$apply() externally with the $$phase value check as rightly suggested by #betaorbust:
I recently got a chance to ask Misko (angular author) about $$phase, and he said to never use it; it's an internal implementation of the digest cycle, and it's not future safe.
The good thing about $scope.$evalAsync() is that it gives you what you really want, get the fresh data up with the digest cycle, faster than even the $timeout can:
Up until now, my approach to deferred-$digest-invocation was to replace the $scope.$apply() call with the $timeout() service (which implicitly calls $apply() after a delay). But, yesterday, I discovered the $scope.$evalAsync() method. Both of these accomplish the same thing - they defer expression-evaluation until a later point in time. But, the $scope.$evalAsync() is likely to execute in the same tick of the JavaScript event loop.
Usage is quite simple:
$scope.$evalAsync(
function() {
// Call some method here OR
$scope.excutemyMethod();
// assign some value;
$scope.someVariable = "asd"
}
);
The method inside the $evalAsync() is added to the Async queue so that it can be executed in the following digest cycle by internally triggering $apply which is what we really want.
Also it includes the $timeout call to handle rarest of rare scenarios watching for the async queue length and waiting the tasks in the Async Queue to be executed to put your function in the execution cycle as soon as possible. Refer Ben Nadel blog, which explains this fact.
I have a UI component which has a $watch callback on its width (the reason is not relevant for this post).
The problem is that in some cases:
The width is changed from a non angular context ->
There is no $digest cycle ->
My $watch callback is not called.
Eventhough my application is a full angular application there are still cases in which code is executed in non angular context. For example:
JQuery calles window.setTimeout - so even if my code called JQuery from within angular context the timeout callback is called in non angular context and my $watch callback will not be executed afterwards.
By the way, even angular themselves call window.setTimeout in their AnimatorService...
So my question is:
How can I make sure a $digest cycle is always performed after any code is executed? (even when the code is a 3rd party code...)
I thought about overriding the original window.setTimeout method but:
It feels a bit ugly and dangerous.
I'm afraid it won't cover all use cases.
Adding a plunker.
The plunker sample contains:
An element which can be hidden using JQuery fadeOut method.
A button which executes the fadeOut call for hiding the element.
A text showing the element display status (Shown!!! or Hidden!!!). This text is updated by $watching on the element display property.
A button which does nothing but to initiate some angular code so that a $digest cycle is called.
Flow:
Click the Fade Out button -> the element will be hidden but the status text will remain Shown!!!.
You can wait forever now - or:
Click the Do Nothing button -> suddenly the text will change.
Why?
When clicking the Fade Out button JQuery.fadeOut calls the window.setTimeout method. After that my $watch callback is called but the element is still not hidden.
The element is only hidden after the timeout callback is called - but then there is no $digest cycle (and i have no way that i know of to trigger one).
Only on the next time an angular code will run my $watch function will be called again and the status will be updated.
AngularJS provides a special $apply method on the scope object to allow you to execute code from outside the AngularJS framework.
The $apply function will execute your code in the correct context and apply a $digest cycle afterwards so you don't have to deal with that yourself.
To implement it in your code, you can:
// Get element
var $element = $('#yourElement');
// Get scope
var scope = angular.element($element).scope();
// Execute your code
scope.$apply(function(scope){
// Your logic here
// All watchers in the scope will be triggered
});
(The scenario above can change depending on your actual application).
You can read more about the $apply method of the scope object right here: http://docs.angularjs.org/api/ng.$rootScope.Scope
Hope that helps!
Looking at your plunker, you could add a callback on the call to animate to manually trigger an update to the Angular scope once the animation is complete:
Before:
$scope.fadeOut = function() {
animatedElement.fadeOut('slow');
};
var getDisplay = function() {
return animatedElement.css('display');
};
$scope.$watch(getDisplay, function(display) {
console.log('$watch called with display = `' + display + '`');
$scope.display = display === 'none' ? 'Hidden!!!' : 'Shown!!!';
});
After:
$scope.fadeOut = function() {
animatedElement.fadeOut('slow', function() { $scope.$digest(); });
};
This would cause your watch on getDisplay to be called when the animation is complete, by which time it will return the correct value.