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.
Related
I have this code inside an angular directive, and I'm finding the $watch behavior a bit confusing. The updateSelect is called in an "ng-click":
scope.updateSelect = function (type) {
scope.selectionCtrl.activeList = scope.seedLists[type];
scope.selectionCtrl.activeListKey = type;
scope.selectionCtrl.activeSelection = scope.selection[type];
scope.selectionCtrl.staged = [];
scope.selectionCtrl.stageRemove = [];
if (type !== scope.activeTab) {
scope.activeTab = type;
}
console.log("update");
};
scope.$watch('selectionCtrl.activeList', function(newValue, oldValue) {
console.log("watch");
}, true);
When I click on the button (triggering updateSelect), and watch the console, I see "update" and then "watch". The first thing that happens inside the function is selectionCtrl.activeList is set, so I would expect to see "watch" and then "update".
Shouldn't watch trigger as soon as the array has changed?
The function has to finish first as javascript is single threaded.
Because the function was called via the ng-click directive, angular will run a digest cycle. Part of that digest cycle is to run through the watch list and resolve all the changes that may have occurred since the cycle last ran.
In the example you give, selectionCtrl.activeList is changed in updateSelect which subsequently results in the watch callback being called.
When does Angular execute watch callback?
It's related to $digest and $apply, and certainly it does not execute within your raw javascript code.
To make watch execute forcefully, you can run $scope.apply() manually, but may cause more problem and not necessary if it is within a angularjs function, i.e. $timeout, $interval, etc, because it will be called automatically after the function.
For more info., lookup;
How do I use $scope.$watch and $scope.$apply in AngularJS?
https://groups.google.com/forum/#!topic/angular/HnJZTEXRztk
https://docs.angularjs.org/api/ng/type/$rootScope.Scope :
The watchExpression is called on every call to $digest() and should return the value that will be watched. (Since $digest() reruns when it detects changes the watchExpression can execute multiple times per $digest() and should be idempotent.)
If you try i.e.:
scope.selectionCtrl.activeList = scope.seedLists[type];
scope.$digest();
you'll get Error: [$rootScope:inprog] $apply already in progress.
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 have a list of hidden items.
I need to show the list and then scroll to one of them with a single click.
I reproduced the code here: http://plnkr.co/edit/kp5dJZFYU3tZS6DiQUKz?p=preview
As I see in the console, scrollTop() is called before the items are visible, so I think that ng-show is not instant and this approach is wrong.
It works deferring scrollTop() with a timeout, but I don't want to do that.
Are there other solutions?
I don't see any other solution than deferring the invocation of scrollTop() when using ng-show. You have to wait until the changes in your model are reflected in the DOM, so the elements become visible. The reason why they do not appear instantly is the scope life cycle. ng-show internally uses a watch listener that is only fired when the $digest() function of the scope is called after the execution of the complete code in your click handler.
See http://docs.angularjs.org/api/ng.$rootScope.Scope for a more detailed explanation of the scope life cycle.
Usually it should not be a problem to use a timeout that executes after this event with no delay like this:
setTimeout(function() {
$(window).scrollTop(50);
}, 0);
Alternative solution without timeout:
However, if you want to avoid the timeout event (the execution of which may be preceded by other events in the event queue) and make sure that scrolling happens within the click event handler. You can do the following in your controller:
$scope.$watch('itemsVisible', function(newValue, oldValue) {
if (newValue === true && oldValue === false) {
$scope.$evalAsync(function() {
$(window).scrollTop(50);
});
}
});
The watch listener fires within the same invocation of $digest() as the watch listener registered by the ng-show directive. The function passed to $evalAsync() is executed by angular right after all watch listeners are processed, so the elements have been already made visible by the ng-show directive.
You can use the $anchorScroll.
Here is the documentation:
https://docs.angularjs.org/api/ng/service/$anchorScroll
Example:
$scope.showDiv = function()
{
$scope.showDivWithObjects = true;
$location.hash('div-id-here');
$anchorScroll();
}
I created a custom directive with an isolate scope that uses two-way data binding back to the parent scope. The scope/bindings all appear to be working correctly, but the template/view is not automatically updating the bound properties in the dom when they change. I can force the dom to update by reading the directive's model.
http://plnkr.co/edit/vPGm1oO0sSaHVvcrp2Ev
Note: In this plunkr example I use am using the isActive property on the wiglet 1's scope as the bound property. Note that I print the value to the console when the scope is created and also when it is updated 2 seconds later via a window.timeout... so you can see at this point that while the data has changed, the dom has not. To see the dom change click the 'print' button on either of the wiglets, which simply prints the value of isActive to console again. This causes the dom to update.
Is this a bug, or am I doing something wrong?
AngularJS only updates bound variables during a scope digest cycle; this normally happens automatically for Angular managed events (ng-click, etc.), but in asynchronous code, such as a setTimeout, you must manually call Scope#$apply() or Scope#$digest():
$window.setTimeout(function(){
$scope.$apply(function() {
$scope.wiglets[0].isActive = true;
console.log("Wiglet 1 isActive:", $scope.wiglets[0].isActive);
});
}, 2000);
This is so common with setTimeout that AngularJS has a built in service, called $timeout, that does this for you:
$timeout(function(){
$scope.wiglets[0].isActive = true;
console.log("Wiglet 1 isActive:", $scope.wiglets[0].isActive);
}, 2000);
Example: http://plnkr.co/edit/XDK06uhYMNcI6fs6SuHP?p=preview
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.