What is the correct way to do have elements update regularly, without explicitly doing that for each variable in an $interval callback?
This would be useful in the following situation: an element's content is bound to a function, like this:
<p ng-bind="timeLeft()"></p>
The timeleft() function returns a value depending on the current time. It needs to be refreshed every 20 ms.
Edit: usually ngBind updates just fine, but I guess because the outcome of timeLeft() only depends on time passing, in this case it doesn't. Without the code below, the view is not updated after loading the DOM, at least not without updating other values.
I've tried the following, which works:
$scope.updateView = function(){
if(!$scope.$$phase) { // To prevent this: "Error: [$rootScope:inprog] $digest already in progress"
$scope.apply();
}
}
$interval( function(){
$scope.updateView();
}, 50);
I noticed just using $interval may already suffice, in my setup this works too:
$scope.updateView = function(){
}
$interval( function(){
$scope.updateView();
}, 20);
Would it be reliable to use that last solution?
Yes, you should be reliable with second solution.
Angular does not run the digest cycle when event occurred from outside angular context, at that you need to tell AngularJs to run apply cycle to update all bindings. When you are using $interval angular will run digest cycle after an execution of callback function, there is no need of running digest cycle which you have done in your 1st solution.
You need to look at $interval service
$interval($scope.updateView,
20, //delay
10, //this option will run it 10 times, if not specified then infinite times
true, //do you want to run digest cycle after callback execution, defaulted to true
{} //any extra params
);
Related
I am trying to implement a function to check for user inactivity. My online search has suggested that I can use the $rootScope.$watch to monitor activity. However, I am a bit concerned about the $digest trigger... as I can't seem to be able to find exactly when or which events trigger this. Any pointers will be appreciated.
Angular's $digest alone isn't going to solve the requirement, in my opinion. What if the user moves their mouse, shouldn't that restart the idle timer?
What I recommend is making a directive that creates an event listener on the $window object for both keydown and mousemove. Then create a service that will allow other components to receive notification when the user's idle state changes.
Something like this:
angular
.module('myApp', [])
.service('Idle', function(){
var self = {
callbacks: {
onidle: []
},
on: function(event, callback){
event = 'on' + event.toLowerCase();
if(self.callbacks[event] && typeof callback === 'function'){
self.callbacks[event].push(callback);
}
return this;
},
trigger: function(event){
event = 'on' + event.toLowerCase();
if(self.callbacks[event])
{
lodash.each(self.callbacks[event], function(callback){
callback.call();
});
}
return this;
}
}
return {
on: self.on,
trigger: self.trigger
}
})
.directive('idleNotification',
function(
$window,
Idle
){
return {
link: function($scope){
var timer = setTimer();
angular
.element($window)
.on('keydown mousemove', function(){
clearTimeout(timer);
timer = setTimer();
});
function setTimer(){
return setTimeout(function(){
Idle.trigger('idle');
}, 30);
}
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Then use the Idle service like so:
Idle.on('idle', function(){
// do something here when the user becomes idle
});
Angular triggers $digest (generally) all by itself. Your $watch should "just work".
Oh - if you really need to initiate the $digest loop yourself, you should use $scope.apply() instead of calling digest directly.
What really happens behind the scenes though is all kinds of things actually trigger a $digest, including ng-click events, timeouts, watchers, long-polling, websockets, http calls, promises - anything that changes the internal application state that angular manages (via "dirty checking"). For more info, see here: https://www.sitepoint.com/understanding-angulars-apply-digest/ and https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest
To understand the Angular context, we need to look at exactly what
goes on inside of it. There are two major components of the $digest
loop:
When Angular thinks that the value could change, it will call $digest() on the value to check whether the value is “dirty.” Hence, when the Angular runtime is running, it will look for potential changes on the value.
$watch lists are resolved in the $digest loop through a process called dirty checking, so after the scope expression is evaluated and the $digest loop runs, the $scope’s watch expressions will run dirty checking.
During the $digest cycle, which executes on the $rootScope, all of the children scopes will perform dirty digest checking. All of the watching expressions are checked for any changes, and the scope calls the listener callback when they are changed.
For example
When we call the $http method, it won’t actually execute until the next $digest loop runs. Although most of the time we’ll be calling $http inside of an $apply block, we can also execute the method outside of the Angular digest loop.
To execute an $http function outside of the $digest loop, we need to wrap it in an $apply block.
That will force the digest loop to run, and our promises will resolve as we expect them to.
$scope.$apply(function() {
$http({
method: 'GET',
url: '/api/dataFile.json'
});
});
Hope this much understanding help!
In AngularJS, how can I ensure code is only executed after a change to a watched property has taken full effect in the UI.
For example, say that the visiblity of a loading indicator is bound to the the value of a isLoading property on a model. How can I ensure that subsequent functionality is only executed after the loading indicator has become visible?
e.g.
my-template.html
<my-loading-indicator ng-show="model.isLoading"></my-loading-indicator>
my-controller.js
// ...
MyController.prototype.doSomething = function() {
this.model.isLoading = true;
// how can I guarantee that the UI is now showing the loading indicator before proceeding?
}
You would need to use $timeout which will not be run until digest cycle completes.
// will start digest cycle since it is bound to `ng-show`
this.model.isLoading = true;
$timeout(function(){
// indicator should be visible now
}[,optional delay]);
Don't forget to inject $timeout wherever you are using it
You could use $scope.$apply(); for triggering a $digest.
"When a $digest cycle runs, the watchers are executed to see if the scope models have changed. If they have, then the corresponding listener functions are called."
Quick question about the $interval service in angular. Looking at the docs ($interval) they warn you to manually cancel intervals, but you have the option of providing a count parameter upon initialization. Once the timer has "ticked" past the allotted count does it cancel itself or simply stop calling the function and live on in the background?
TL;DR; After the count, the interval is cleared.
As the same documentation says, it is recommended that you cancel the $interval when the scope of your controller is detroyed. Something like:
var t = $interval(function(){
...
}, 1000);
$scope.$on('$destroy', function(){
$interval.cancel(t);
});
The delay parameter is time interval which the function is called. In the example above, the function is called every 1000 milliseconds. If you don't cancel the $interval, Angular will hold a reference to it, and may continue to execute your function, causing strange behaviors in your app.
Considering that the $interval provider is just a wrapper of the native setInterval(), with the adition of the $apply, looking at the Angular implementation (https://github.com/angular/angular.js/blob/master/src/ng/interval.js), we can find this snippet of code:
if (count > 0 && iteration >= count) {
deferred.resolve(iteration);
clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
}
So, the promise created by the provider is resolved, and the interval is cleared. The cancel method does this:
intervals[promise.$$intervalId].reject('canceled');
$window.clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
So, I think your assumption is right. After the count, the interval is already cleared.
There is this article I saw long time ago:
https://coderwall.com/p/ngisma
It describes a method that triggers $apply if we are not in an apply or digest phase.
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
Angular has the $scope.$evalAsync method (taken from 1.2.14):
$evalAsync: function(expr) {
// if we are outside of an $digest loop and this is the first time we are scheduling async
// task also schedule async auto-flush
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
$browser.defer(function() {
if ($rootScope.$$asyncQueue.length) {
$rootScope.$digest();
}
});
}
this.$$asyncQueue.push({scope: this, expression: expr});
}
Which calls digest if we are not in a phase and adds the current invocation to the asyncQueue.
There is also the $apply, $digest and $timeout methods.
It is confusing.
What is the difference between all ways mentioned to trigger a digest cycle (a dirty check and data binding)?
What is the use case for each method?
Is safeApply() still safe? :)
What alternative we have instead of that safeApply() (in case we call $apply in the middle of a digest cycle)?
As a high level introduction I would say that it is rarely required to actually initiate your own digest cycle, since angular handles most cases.
That being said let's dive into the question.
As a very high level, the $digest loop looks like this:
Do:
- - - If asyncQueue.length, flush asyncQueue.
- - - Trigger all $watch handlers.
- - - Check for "too many" $digest iterations.
While: ( Dirty data || asyncQueue.length )
So basically $evalAsync is adding the function to the asyncQueue and defering a digest if it needs to. However if it's already in a digest cycle it will flush the asyncQueue and it will just call the function.
You may notice that this is very similar to the safeApply. One difference is that instead of adding the function to the asyncQueue it just calls it, which can happen in the middle of a cycle for instance. The other difference is that it exposes and relies on a $$ variable, which are intended to be internal.
The most important difference however between $evalAsync and $apply from $digest (I will get to $timeout below) is that $evalAsync and $apply starts the digest at the $rootScope but you call the $digest on any scope. You will need to evaluate your individual case if you think you need this flexibility.
Using $timeout is very similar to $evalAsync again except that it will always defer it out of the current digest cycle, if there is one.
$timeout(function() {
console.log( "$timeout 1" );
});
$scope.$evalAsync(function( $scope ) {
console.log( "$evalAsync" );
});
If you already in a digest cycle will give you
$evalAsync
$timeout 1
Even though they are called in the opposite order, since the timeout one is delayed to the next digest cycle, which it instantiates.
EDIT For the questions in the comment.
The biggest difference between $apply and $evalAsync as far as I can tell is that $apply is actually triggering the $digest cycle. To you all this means is that you need to be sure you call $apply when you aren't in a digest cycle. Just for transparency, the following code is also valid:
$scope.$apply(function() {
$scope.$evalAsync(function() {
});
});
Which will call the apply function, add the no-op function to the $asyncQueue and begin the digest cycle. The docs say it is recommended to use $apply whenever the model changes, which could happen in your $evalAsync
The difference between fn(); $scope.apply() and $scope.apply(fn) is just that the $scope.apply(fn) does a try catch for the function and explicitly uses $exceptionHandler. Additionally, you could actually attempt to cause digest cycles in fn, which will change how the apply is handled.
I'd also like to point out at this time that it is even more complicated in 1.3 assuming it stays this way. There will be a function called $applyAsync used to defer apply calls (reference).
This post compiled some information from This blog and this so post plus some of my experience.
Hope this helped!
There's only one way to start a digest cycle: Calling $digest. It's rarely called directly.
$apply is essentially a safe wrapper for $digest. Safe means that it handles errors. So most of the time this is the method you want to call to propagate changes. Another important difference is that it always starts a digest cycle at the root scope, whereas $digest can be called on any scope and only affects that scope and its descendants.
Many services and directives trigger a digest cycle, usually by calling $apply. The only important thing to know is wether you have to call $apply yourself or it has already been called by the service / directive.
Is safeApply still safe?
Yes, because there's still only one way to start a digest cycle.
What alternative we have instead of that safeApply() (in case we call $apply in the middle of a digest cycle)?
Not calling $apply in the middle of a digest cycle. Seriously. Being in that situation is almost always a design issue in the first place. It's better to resolve that issue instead of covering it.
Anyway the method used by the solution, checking $$phase, is the only way of knowing if a digest cycle is running. As shown by you it's even used by Angular internally. It's enough to do if (!$rootScope.$$phase). But keep in mind that it's a hack that might break with a new version of Angular. BTW: Monkey patching the topmost scope, as proposed by the author, would make the solution useless for isolated scopes.
Angular's way of data binding might indeed be difficult to understand for someone new to Angular at first. But basically it all boils down to this: For every change to be recognized / processed someone has to call $apply (or $digestfor that matter). That's it. This is supposed be done by directives and services. It's actually quite hard to get a '$apply already in progress' error if you have done everything right.
I'm using jQuery to change the HTML of a tag, and the new HTML can be a very long string.
$("#divToChange").html(newHTML);
I then want to select elements created in the new HTML, but if I put the code immediately following the above line it seems to create a race condition with a long string where the changes that html() is making may not necessarily be finished rendering. In that case, trying to select the new elements won't always work.
What I want to know is, is there an event fired or some other way of being notified when changes to html() have finished rendering ? I came across the jQuery watch plugin, which works alright as workaround but it's not ideal. Is there a better way ?
As a commenter already mentioned, JavaScript is single threaded, so you can't get race conditions.
What may trip you up however, is the fact that the UI will not update itself based on JavaScript, until a thread is finished. This means that the entire method must finish, including all code after you call html(...), before the browser will render the content.
If your code after calling html(...) relies on the layout of the page being recalculated before continuing, you can do something like this:
$("#divToChange").html(newHTML);
setTimeout(function() {
// Insert code to be executed AFTER
// the page renders the markup
// added using html(...) here
}, 1);
Using setTimeout(...) with a time of 1 in JavaScript defers execution until after the current JavaScript code in the calling function finishes and the browser has updated the UI. This may solve your problem, though it is difficult to tell unless you can provide a reproducible example of the error you're getting.
use .ready jQuery function
$("#divToChange").html(newHTML).ready(function () {
// run when page is rendered
});
It's 7 years latter and I just ran into a scenario exactly like the one #mikel described, where I couldn't avoid a "timer based solution". So, I'm just sharing the solution I developed, in case anyone out there is still having issues with this.
I hate having setTimeouts and setIntervals in my code. So, I created a small plugin that you can put where you think it's best. I used setInterval, but you can change it to setTimeout or another solution you have in mind. The idea is simply to create a promise and keep checking for the element. We resolve the promise once it is ready.
// jquery.ensure.js
$.ensure = function (selector) {
var promise = $.Deferred();
var interval = setInterval(function () {
if ($(selector)[0]) {
clearInterval(interval);
promise.resolve();
}
}, 1);
return promise;
};
// my-app.js
function runWhenMyElementExists () {
// run the code that depends on #my-element
}
$.ensure('#my-element')
.then(runWhenMyElementExists);