Why axios callback changes are displayed in angularjs, without using $apply
I was trying axios library on angularjs and I was surprised when I saw that the changes to $scope in the axios callback were detected by angular. I thought I had to call $apply like, for example, when you use setTimeout.
// axios example
axios.get(url).then((response) => {
// Here I don't need $apply, why??
$scope.axiosResult = response.data;
});
// setTimeout example
setTimeout(() => {
// Here I need $apply for the timeoutResult to appear on the HTML
$scope.$apply(() => $scope.timeoutResult = {message: "timeout!"});
}, 2000)
Do you know why $apply is not needed in axios?
EDIT:
A comment by georgeawg helped me see that I was using $http on another place, so I guess the digest cycle triggered by $http is helping axios callback to be "digested" too.
How to use the axios library with AngularJS
Bring its ES6 promises into the AngularJS context using $q.when:
// axios example
̶a̶x̶i̶o̶s̶.̶g̶e̶t̶(̶u̶r̶l̶)̶.̶t̶h̶e̶n̶(̶(̶r̶e̶s̶p̶o̶n̶s̶e̶)̶ ̶=̶>̶ ̶{̶
$q.when(axios.get(url)).then((response) => {
$scope.axiosResult = response.data;
});
Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc.
Also use the $timeout service instead of setTimeout.
$timeout(() => {
$scope.timeoutResult = {message: "timeout!"});
}, 2000)
The $timeout service is integrated with the AngularJS framework and its digest cycle.
I am new to promise concepts and I'm facing a problem while I'm trying to submit a login form.
When the promise is resolved, I want to update the UI by changing the value of a scope variable.
Here is the controller method which is called
$ctrl.loginForm = function(isValid) {
// check to make sure the form is completely valid
if (isValid) {
LoginService.login($ctrl.user).then(function (value) {
$ctrl.loginError = false;
var cookie = value.token;
}, function (reason) {
$ctrl.loginError = true;
});
}
};
and here is my service method
service.login = function(credentials) {
return new Promise((resolve, reject) => {
$http.post(AppConst.Settings.apiUrl + 'api-token-auth/',credentials).success((data) => {
resolve(data);
}).error((err, status) => {
reject(err, status);
});
});
};
So once the promise is resolved the $ctrl.loginError in the controller changes its value but it does not update on the UI.
I've tried with $apply and it works, but I don't understand why I must use $apply! I thought that the digest cycle starts by default!
Am I doing something wrong?
$http methods return you a promise which is a $q service. So, problem is that you use native promise wrapper which doesn't tell angular's scope to run $apply method.
Change your service to following
service.login = function(credentials) {
return $http.post(AppConst.Settings.apiUrl + 'api-token-auth/',credentials);
};
I'm using angularJS and I do this:
xxx.then(function (response) {
$scope.x = response.x;
$scope.y = response.y;
}, function (error) {}
);
The response come from server not instantantly. Then when the response come, I want than the scope update my value, but it does that just when I click in some button other so.
In my html I receive the informations so:
<p>{{x}}</p>
<p>{{y}}</p>
Do you know what I'm doing wrong?
This could be an issue with the digest cycle, try doing $scope.$apply() like below :
xxx.then(function (response) {
$scope.x = response.x;
$scope.y = response.y;
$scope.$apply();
}, function (error) {});
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.
The results of your promise will not be propagated until the next digest cycle. As there is nothing else in your code that triggers the digest cycle, changes are not getting applied immediately. But, when you click on a button , it triggers the digest cycle, due to which the changes are getting applied
Check this for a clear explanation about this.
A .then method may fail to update the DOM if the promise comes from a source other than the $q service. Use $q.when to convert the external promise to a $q service promise:
//xxx.then(function (response) {
$q.when(xxx).then(function (data) {
$scope.x = data.x;
$scope.y = data.y;
}, function (error) {
throw error;
});
The $q service is integrated with the AngularJS framework and the $q service .then method will automatically invoke the framework digest cycle to update the DOM.
$q.when
Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. This is useful when you are dealing with an object that might or might not be a promise, or if the promise comes from a source that can't be trusted.
-- AngularJS $q Service API Reference - $q.when
I'm using Typescript 2.1(developer version) to transpile async/await to ES5.
I've noticed that after I change any property which is bound to view in my async function the view isn't updated with current value, so each time I have to call $scope.$apply() at the end of function.
Example async code:
async testAsync() {
await this.$timeout(2000);
this.text = "Changed";
//$scope.$apply(); <-- would like to omit this
}
And new text value isn't shown in view after this.
Is there any workaround so I don't have to manually call $scope.$apply() every time?
The answers here are correct in that AngularJS does not know about the method so you need to 'tell' Angular about any values that have been updated.
Personally I'd use $q for asynchronous behaviour instead of using await as its "The Angular way".
You can wrap non Angular methods with $q quite easily i.e. [Note this is how I wrap all Google Maps functions as they all follow this pattern of passing in a callback to be notified of completion]
function doAThing()
{
var defer = $q.defer();
// Note that this method takes a `parameter` and a callback function
someMethod(parameter, (someValue) => {
$q.resolve(someValue)
});
return defer.promise;
}
You can then use it like so
this.doAThing().then(someValue => {
this.memberValue = someValue;
});
However if you do wish to continue with await there is a better way than using $apply, in this case, and that it to use $digest. Like so
async testAsync() {
await this.$timeout(2000);
this.text = "Changed";
$scope.$digest(); <-- This is now much faster :)
}
$scope.$digest is better in this case because $scope.$apply will perform dirty checking (Angulars method for change detection) for all bound values on all scopes, this can be expensive performance wise - especially if you have many bindings. $scope.$digest will, however, only perform checking on bound values within the current $scope making it much more performant.
This can be conveniently done with angular-async-await extension:
class SomeController {
constructor($async) {
this.testAsync = $async(this.testAsync.bind(this));
}
async testAsync() { ... }
}
As it can be seen, all it does is wrapping promise-returning function with a wrapper that calls $rootScope.$apply() afterwards.
There is no reliable way to trigger digest automatically on async function, doing this would result in hacking both the framework and Promise implementation. There is no way to do this for native async function (TypeScript es2017 target), because it relies on internal promise implementation and not Promise global. More importantly, this way would be unacceptable because this is not a behaviour that is expected by default. A developer should have full control over it and assign this behaviour explicitly.
Given that testAsync is being called multiple times, and the only place where it is called is testsAsync, automatic digest in testAsync end would result in digest spam. While a proper way would be to trigger a digest once, after testsAsync.
In this case $async would be applied only to testsAsync and not to testAsync itself:
class SomeController {
constructor($async) {
this.testsAsync = $async(this.testsAsync.bind(this));
}
private async testAsync() { ... }
async testsAsync() {
await Promise.all([this.testAsync(1), this.testAsync(2), ...]);
...
}
}
I have examined the code of angular-async-await and It seems like they are using $rootScope.$apply() to digest the expression after the async promise is resolved.
This is not a good method. You can use AngularJS original $q and with a little trick, you can achieve the best performance.
First, create a function ( e.g., factory, method)
// inject $q ...
const resolver=(asyncFunc)=>{
const deferred = $q.defer();
asyncFunc()
.then(deferred.resolve)
.catch(deferred.reject);
return deferred.promise;
}
Now, you can use it in your for instance services.
getUserInfo=()=>{
return resolver(async()=>{
const userInfo=await fetch(...);
const userAddress= await fetch (...);
return {userInfo,userAddress};
});
};
This is as efficient as using AngularJS $q and with minimal code.
As #basarat said the native ES6 Promise doesn't know about the digest cycle.
What you could do is let Typescript use $q service promise instead of the native ES6 promise.
That way you won't need to invoke $scope.$apply()
angular.module('myApp')
.run(['$window', '$q', ($window, $q) => {
$window.Promise = $q;
}]);
I've set up a fiddle showcasing the desired behavior. It can be seen here: Promises with AngularJS.
Please note that it's using a bunch of Promises which resolve after 1000ms, an async function, and a Promise.race and it still only requires 4 digest cycles (open the console).
I'll reiterate what the desired behavior was:
to allow the usage of async functions just like in native JavaScript; this means no other 3rd party libraries, like $async
to automatically trigger the minimum number of digest cycles
How was this achieved?
In ES6 we've received an awesome featured called Proxy. This object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).
This means that we can wrap the Promise into a Proxy which, when the promise gets resolved or rejected, triggers a digest cycle, only if needed. Since we need a way to trigger the digest cycle, this change is added at AngularJS run time.
function($rootScope) {
function triggerDigestIfNeeded() {
// $applyAsync acts as a debounced funciton which is exactly what we need in this case
// in order to get the minimum number of digest cycles fired.
$rootScope.$applyAsync();
};
// This principle can be used with other native JS "features" when we want to integrate
// then with AngularJS; for example, fetch.
Promise = new Proxy(Promise, {
// We are interested only in the constructor function
construct(target, argumentsList) {
return (() => {
const promise = new target(...argumentsList);
// The first thing a promise does when it gets resolved or rejected,
// is to trigger a digest cycle if needed
promise.then((value) => {
triggerDigestIfNeeded();
return value;
}, (reason) => {
triggerDigestIfNeeded();
return reason;
});
return promise;
})();
}
});
}
Since async functions rely on Promises to work, the desired behavior was achieved with just a few lines of code. As an additional feature, one can use native Promises into AngularJS!
Later edit: It's not necessary to use Proxy as this behavior can be replicated with plain JS. Here it is:
Promise = ((Promise) => {
const NewPromise = function(fn) {
const promise = new Promise(fn);
promise.then((value) => {
triggerDigestIfNeeded();
return value;
}, (reason) => {
triggerDigestIfNeeded();
return reason;
});
return promise;
};
// Clone the prototype
NewPromise.prototype = Promise.prototype;
// Clone all writable instance properties
for (const propertyName of Object.getOwnPropertyNames(Promise)) {
const propertyDescription = Object.getOwnPropertyDescriptor(Promise, propertyName);
if (propertyDescription.writable) {
NewPromise[propertyName] = Promise[propertyName];
}
}
return NewPromise;
})(Promise) as any;
In case you're upgrading from AngularJS to Angular using ngUpgrade (see https://angular.io/guide/upgrade#upgrading-with-ngupgrade):
As Zone.js patches native Promises you can start rewriting all $q based AngularJS promises to native Promises, because Angular triggers a $digest automatically when the microtask queue is empty (e.g. when a Promise is resolved).
Even if you don't plan to upgrade to Angular, you can still do the same, by including Zone.js in your project and setting up a similar hook like ngUpgrade does.
Is there any workaround so I don't have to manually call $scope.$apply() every time?
This is because TypeScript uses the browser native Promise implementation and that is not what Angular 1.x knows about. To do its dirty checking all async functions that it does not control must trigger a digest cycle.
As #basarat said the native ES6 Promise doesn't know about the digest cycle. You should to promise
async testAsync() {
await this.$timeout(2000).toPromise()
.then(response => this.text = "Changed");
}
As it already has been described, angular does not know when the native Promise is finished. All async functions create a new Promise.
The possible solution can be this:
window.Promise = $q;
This way TypeScript/Babel will use angular promises instead.
Is it safe? Honestly I'm not sure - still testing this solution.
I would write a converter function, in some generic factory (didnt tested this code, but should be work)
function toNgPromise(promise)
{
var defer = $q.defer();
promise.then((data) => {
$q.resolve(data);
}).catch(response)=> {
$q.reject(response);
});
return defer.promise;
}
This is just to get you started, though I assume conversion in the end will not be as simple as this...
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.