NOTE: the fiddle uses an old version of Angular, and that it's not working any more because as of 1.2 the Angular template engine does not handle promises transparently.
I'm looking into chaining promises to populate my scope, and then having the scope automatically update the dom.
I'm running into problems with this though.. If I call "then" on an already resolved promise, it creates a new promise (that will call the success function asynchronously but almost immediately). I think the problem is that we've already left the digest cycle by the time the success function is called, so the dom never updates.
Here is the code:
<div ng-controller="MyCtrl">
Hello, {{name}}! <br/>
{{name2}}<br/>
<button ng-click="go()">Clickme</button><br/>
{{name3}}
</div>
var myApp = angular.module('myApp',[]);
function MyCtrl($scope, $q) {
var data = $q.defer();
setTimeout(function() {$scope.$apply(data.resolve("Some Data"))}, 2000);
var p = data.promise;
$scope.name = p.then(angular.uppercase);
$scope.name2 = p.then(function(x) { return "Hi "+x;});
$scope.go = function() {
$scope.name3 = p.then(function(x) {
// uncomment this to make it work:
//$scope.$apply();
return "Finally: "+x;
});
};
}
http://jsfiddle.net/QZM4d/
Is there some way to make this work without calling $apply every time I chain promises?
NOTE: the fiddle uses an old version of Angular, and that it's not working any more because as of 1.2 the Angular template engine does not handle promises transparently.
To quote #pkozlowski.opensource:
In AngularJS the results of promise resolution are propagated asynchronously, inside a $digest cycle. So, callbacks registered with then() will only be called upon entering a $digest cycle.
So, when the button is clicked, we are in a digest cycle. then() creates a new promise, but the results of that then() will not be propagated until the next digest cycle, which never comes (because there is no $timeout, or $http, or DOM event to trigger one). If you add another button with ng-click that does nothing, then click that, it will cause a digest cycle and you'll see the results:
<button ng-click="">Force digest by clicking me</button><br/>
Here's a fiddle that does that.
The fiddle also uses $timeout instead of setTimeout -- then $apply() isn't needed.
Hopefully it is clear when you need to use $apply. Sometimes you do need to call it manually.
Related
I am developing an angular-ionic-firebase application. I am struggling on an issue where I am unable to update a $scope in my html. My must resolve {{uevent}} and update the html with the result of {{uevent}}. My html code is below.
**<ion-view view-title="{{uevent}}">**
<ion-tabs class="tabs-stable tabs-icon-top">
<ion-tab title="Buddies" icon="ion-ios-people" ng- click="showBuddies(a)">
</ion-tab>
<ion-tab title="Summary" icon="ion-navicon" ng-click="billSummary()">
</ion-tab>
</ion-tabs>
my angular code is below. CurrUser is a factory. I've defined an empty $scope.uevent which I am expecting to update using a value returned from a factory.
var eventid = CurrUser.getEventid();
$scope.uevent = " ";
function updateEvent(desc) {
$scope.uevent = desc;
console.log($scope.uevent);// I am able to see the value returned from the factory. This function is executed below in the for loop.
};
// promise that returns a value from the factory - CurrUser.
CurrUser.getEventdesc(eventid).then(function (result) {
var description = result;
var desc;
for (var itemID in description) {
var tmp = description[itemID];
desc = tmp.Description; // This contains the value that must be updated to $scope.uevent
updateEvent(desc); // Calls the function defined above to update $scope.uevent.
};
});
I've been on this since last night without any clues. Any help is greatly appreciated and as always, thanks for your time.
UPDATE #1:(07 July)
When i added a timeout of 1 sec, the view was updated to the correct value.
function updateEvent(desc) {
$timeout(function(){
$scope.$apply(function () {
$scope.uevent = desc;
})
},1000);
};
Regards,
There are 2 execution context when You work with AngularJS - Angular Context and JavaScript Context.
When you are changing the data in AngularJS Context, the digest loop begins and all data updates, but when you change data in the JavaScript Context (e.g setTimeout, setInterval and so on), Angular doesn't know about the changes so it doesn't update the data.
In JavaScript Context you must change your data in scope.$apply() method to run the digest loop manually.
So your code will look like
var eventid = CurrUser.getEventid();
$scope.uevent = " ";
function updateEvent(desc) {
$scope.$apply(function(){
$scope.uevent = desc;
});
console.log($scope.uevent);// I am able to see the value returned from the factory. This function is executed below in the for loop.
};
// promise that returns a value from the factory - CurrUser.
CurrUser.getEventdesc(eventid).then(function (result) {
var description = result;
var desc;
for (var itemID in description) {
var tmp = description[itemID];
scope.$apply(function(){
desc = tmp.Description; // This contains the value that must be updated to $scope.uevent
})
updateEvent(desc); // Calls the function defined above to update $scope.uevent.
};
});
For more.If there is an Angular JS alternative, you must use it,because it runs te digest loop automatically($timeout instead of setTimeout(), $interval instead of setInterval())
Try this; $apply is used to trigger $digest .
function updateEvent(desc) {
$timeout(function(){
$scope.uevent = desc;
$scope.$apply();
},0)
};
Where is your controller defined in the html? Or is it in your app.js. Anyways, for a start can you do a $scope.$apply in your updateEvent function?
function updateEvent(desc) {
$scope.$apply(function(){
$scope.uevent = desc;
});
console.log($scope.uevent);
};
I dont know what happens in your getEventDesc() method but if there is a non-angular ajax request or something, then it might need the $apply to trigger the digest cycle and update watchers.
Detailed explanation below.
Reference: This awesome informative post -> https://github.com/angular/angular.js/wiki/When-to-use-$scope.$apply()
AngularJS provides wrappers for common native JS async behaviors:
Events => ng-click
Timeouts => $timeout
jQuery.ajax() => $http
This is just a traditional async function with a $scope.$apply() called at the end, to tell AngularJS that an asynchronous event just occurred.
$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.
Whenever possible, use AngularJS services instead of native. If you're creating an AngularJS service (such as for sockets) it should have a $scope.$apply() anywhere it fires a callback.
Im working on angularjs 1.4. Im trying to have some frontend-cache collection that updates the view when new data is inserted. I have checked other answers from here Angularjs watch service object but I believe Im not overwriting the array, meaning that the reference is the same.
The code is quite simple:
(function(){
var appCtrl = function($scope, $timeout, SessionSvc){
$scope.sessions = {};
$scope.sessions.list = SessionSvc._cache;
// Simulate putting data asynchronously
setTimeout(function(){
console.log('something more triggered');
SessionSvc._cache.push({domain: "something more"});
}, 2000);
// Watch when service has been updated
$scope.$watch(function(){
console.log('Watching...');
return SessionSvc._cache;
}, function(){
console.log('Modified');
}, true);
};
var SessionSvc = function(){
this._cache = [{domain: 'something'}];
};
angular.module('AppModule', [])
.service('SessionSvc', SessionSvc)
.controller('appCtrl', appCtrl);
})();
I thought that the dirty checking would have to catch the changes without using any watcher. Still I put the watcher to check if anything gets executed once the setTimeout function is triggered. I just dont see that the change is detected.
Here is the jsbin. Im really not understanding sth or doing a really rockie mistake.
You need to put $scope.$apply(); at the bottom of your timeout to trigger an update. Alternatively you can use the injectable $timeout service instead of setTimeout and $apply will automatically get called.
jsbin
I have an angular controller:
.controller('DashCtrl', function($scope, Auth) {
$scope.login = function() {
Auth.login().then(function(result) {
$scope.userInfo = result;
});
};
});
Which is using a service I created:
.service('Auth', function($window) {
var authContext = $window.Microsoft.ADAL.AuthenticationContext(...);
this.login = function() {
return authContext.acquireTokenAsync(...)
.then(function(authResult) {
return authResult.userInfo;
});
};
});
The Auth service is using a Cordova plugin which would be outside of the angular world. I guess I am not clear when you need to use a $scope.$apply to update your $scope and when you don't. My incorrect assumption was since I had wrapped the logic into an angular service then I wouldn't need it in this instance, but nothing gets updated unless I wrap the $scope.userInfo = statement in a $timeout or $scope.$apply.
Why is it necessary in this case?
From angular's wiki:
AngularJS provides wrappers for common native JS async behaviors:
...
jQuery.ajax() => $http
This is just a traditional async function with a $scope.$apply()
called at the end, to tell AngularJS that an asynchronous event just
occurred.
So i guess since your Auth service does not use angular's $http, $scope.$apply() isn't called by angular after executing the Async Auth function.
Whenever possible, use AngularJS services instead of native. If you're
creating an AngularJS service (such as for sockets) it should have a
$scope.$apply() anywhere it fires a callback.
EDIT:
In your case, you should trigger the digest cycle once the model is updated by wrapping (as you did):
Auth.login().then(function(result) {
$scope.$apply(function(){
$scope.userInfo = result;
});
});
Or
Auth.login().then(function(result) {
$scope.userInfo = result;
$scope.$apply();
});
Angular does not know that $scope.userInfo was modified, so the digest cycle needs to be executed via the use of $scope.$apply to apply the changes to $scope.
Yes, $timeout will also trigger the digest cycle. It is simply the Angular version of setTimeout that will execute $scope.$apply after the wrapped code has been run.
In your case, $scope.$apply() would suffice.
NB: $timeout also has exception handling and returns a promise.
I have a custom directive datepicker that uses scope.apply and works well. I cut out most of it to avoid cluttering the question, here is a simple version
appAdmin.directive("datepickerPss", ["$compile", "$parse", function ($compile, $parse) {
return {
$element.datepicker($scope.options).on("changeDate", function (ev) {
$scope.$apply(function () {
ngModel.$setViewValue(ev.date);
});
});
}
}]);
I have the custom datepicker in a modal, I simply want to initialize the value so in my controller I did the following at the top and had the "$digest already in progress" error
$scope.sDate = Date.now();
So reading up on this issue and the scope apply I changed it to the following in my controller
$timeout(function() {
$scope.sDate = Date.now();
});
However I still get the $digest in progress error. I'm not sure where to go from here. All the posts I have read have had their issues resolved by using $timeout.
Remove $scope.$apply and just use $timeout instead.
$element.datepicker($scope.options).on("changeDate", function (ev) {
$timeout(function () {
ngModel.$setViewValue(ev.date);
}, 0);
});
$scope.$apply starts a new $digest cycle on $rootScope, so calling it inside of your directive starts another $digest cycle while one is already occurring. By wrapping your call in $timeout, it'll wait until the previous $digest cycle finishes before applying your changes.
In addition, if you are trying to initialize a value AFTER you've already bound bound your change event in your directive, you could run into issues since the directive's digest cycle might still be in progress while your controller is being parsed and executed.
I'm trying to update the ui by pushing a new entry into an array but for some reason the ui is not updated until the next operation on the array.
function TestCtrl($scope){
$scope.projects = [{name: "project1"}];
$scope.test = function(){ return "batman"; };
$scope.addNew = function(){
$scope.projects.push({name: "project2"});
setTimeout(function(){
$scope.projects.push({name: "project3"});
}, 1000);
};
}
And here is an example http://jsbin.com/itasis/4/edit
I didn't tested yet but I expect the same issue in behavior from an ajax request.
Use $timeout instead of setTimeout. $timeout automatically calls $apply() for us, triggering an Angular digest cycle, which will update any views that need to be refreshed.
Regarding AJAX, I would encourage you to use Angular's $http service, which will also call $apply() for us. Otherwise, in your AJAX callback, after updating your Angular models/scope, manually call scope.$apply().