async function in OnInit firing after template has loaded
Apologies if this has been asked. I couldn't seem to find something speaking to my specific scenario.
In my controller, I define an $onInit function that calls a function that that in turn calls an async function. The async function has a catch block that sets a boolean for an ng-if in the template (controls whether an error message should display):
controller:
$onInit() {
initFn();
}
function initFn() {
innerFn();
}
function innerFn() {
asyncFn
.then(() => {
// does some stuff
})
.catch(() => {
boolVal = true;
})
}
template:
<div ng-if="boolVal">some-error-message</div>
I test this by sending back a Promise.reject() for asyncFn but the error message doesn't show. boolVal, however, is true. What I think is happening is that the template compiles and builds before the async call is able to finish. How do I ensure that all API calls are completed in $onInit before the template has loaded? If that isn't the problem, what am I missing?
Thanks!
I test this by sending back a Promise.reject() for asyncFn
The ES6 promises returned by Promise.reject() are not integrated with the AngularJS framework. Instead, return AngularJS promises:
function asyncFn () {
if (/* error condition */) {
return $q.reject();
};
}
AngularJS modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and AngularJS execution context. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc.1
For more information, see
AngularJS $q Service API Reference - `$q.reject
what are common scenarios/services that I should be aware
Beware of ES6 promises -- use AngularJS promises
Beware of third-party promises -- use $q.when to convert to AngularJS promises
Beware of async/await -- they return ES6 promises
Beware of window.setTimeout - use the $timeout service
Beware of window.setInterval -- use the $interval service
Beware of jQuery.ajax -- use the $http service
Event from outside the AngularJS framework
Beware of element.addEventListener
Beware of jQuery.on
Beware of events from third-party APIs / libraries
You can also use $apply() to enter the AngularJS execution context from JavaScript.
Keep in mind that in most places (controllers, services) $apply has already been called for you by the directive which is handling the event. An explicit call to $apply is needed only when implementing custom event callbacks, or when working with third-party library callbacks.
For more information, see
AngularJS Developer Guide - Integration with the browser event loop
AngularJS $scope / $rootScope Class API Reference - $apply
Related
This is a very common question, but I have never found the answer that works properly. I have come across three answers, but none always works.
$apply: This will force an update, but will randomly through an error if it gets called while a digest is in progress.
"safe apply" where there is a check for a digest in progress before calling $apply. This doesn't always update the view for some reason I haven't been able to determine. In addition, there is a small chance that a digest will start between the check and $apply.
$timeout: according to the documentation, this should work reliably but doesn't seem to always update the view.
from $timeout documentation, the parameter invokeApply:
If set to false skips model dirty checking, otherwise will invoke fn within the $apply block. (default: true)
It never throughs an error, but sometimes doesn't update the view during a page load.
Here is a code sample where the problem occurs during page initialization:
EditService.getEvents(Gparams.curPersonID)
.then(function successCallback(response) {
if (response.status=='200') {
do some stuff
} else {
handle an error
}
var timer = $timeout(function() { })
.then(function successCallback(response) {
do something
});
$scope.$on("$destroy", function(event {
$timeout.cancel(timer)});
}); });
What is the correct answer? Please don't just say what but also discuss why.
Here is a code sample where the problem occurs during page initialization
A common cause of .then methods not updating the DOM is that the promise is not an AngularJS $q service promise. The solution is to convert the suspect promise to a $q service promise with the $q.when method.
//EditService.getEvents(Gparams.curPersonID)
//CONVERT to $q service promise
$q.when(EditService.getEvents(Gparams.curPersonID))
.then(function successCallback(response) {
if (response.status=='200') {
do some stuff
} else {
handle an error
}
The .then method of a $q service promise is integrated with the AngularJS framework and its digest cycle. Changes to the scope model will automatically update the DOM.
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
Method on $viewcontentloaded is firing asynchronously. To detail my problem, I have a variable in root scope i.e. my Main controller, which need to be initialized before my view controller loads. In module.run I am calling a sync function to initialize $rootScope.session. And In my view controller of a route, I am checking the status of session in afunction that is called like
$scope.$on('$viewContentLoaded', function() {
$scope.initialize();
});
But some times on page refreash, I am getting an undefined value for $rootScope.session, as It may have initialized later. So, Is there any way to make this synchronous like rootscope will be initialized before view loads. And for curiosity, how it will affect, if I call the $scope.initialize(); normally in my controller, in $viewContentLoaded or in $routeChangeSuccess.
Thanks in advance.
So, Is there any way to make this synchronous like rootscope will be initialized before view loads.
Use the $controller service to manually create the controller, as in a unit test.
$controllerProvider.register('FooCtrl', FooCtrl);
ctrl = $controller('FooCtrl', {$scope: scope});
Or $broadcast a custom event from the main controller down to the child:
function mainCtrl($rootScope)
{
$rootScope.$broadcast('abc');
}
function secondCtrl($scope)
{
$scope.$on('abc', function(event) { $scope.initialize(); });
}
Or use a try/catch block and a recursive call with a timer.
These are more or less the steps that you would take to implement lazy loading in AngularJS. In summary, you would first define your app module to keep instances of the relevant providers. Then you would define your lazy artifacts to register themselves using the providers rather than the module API. Then using a ‘resolve’ function that returns a promise in your route definition, you would load all lazy artifacts and resolve the promise once they have been loaded. This ensures that all lazy artifacts will be available before the relevant route is rendered. Also, don’t forget to resolve the promise inside $rootScope.$apply, if the resolution will be happening outside of AngularJS. Then you would create a ‘bootstrap’ script that first loads the app module before bootstrapping the app. Finally, you would link to the bootstrap script from your ‘index.html’ file.
References
AngularJS source: controllerSpec.js
Ifeanyi Isitor: Lazy Loading In AngularJS
AngularJS Lazy Loading with Require.js
Split Large AngularJS Controllers using the Mixin Pattern
At some point after a user action I would like to cause a digest to occur, so the UI reflects a change in the data-model backing it.
I have a service that performs some change in a callback (asynchronously).
I understand that $scope only makes sense in the context of a controller. Can I achieve the same effect by performing $apply() on the $rootScope?
I have seen code that checks for $$phase or similar related to avoiding digest errors, what checks should I perform in order to trigger a digest safely?
See this answer: Running $apply on $rootScope vs any other scope
You can call $rootScope.$apply() outside of a controller (i.e. in a service) in order to trigger a digest loop.
Alternatively, you could consider using $broadcast and $on to send a notification to other parts of your app when something needs refreshing. (See Understanding Angular’s $scope and $rootScope event system $emit, $broadcast and $on)
// in a service
$rootScope.$broadcast('myCustomEvent', {
someProp: 'foobar'
// other data
});
// in a controller or another service
$scope.$on('myCustomEvent', function (event, data) {
console.log(data);
// do something with this event
});
Say I have an service:
angular.module("app").factory("myService", function($rootScope){
return {
doSomething: function(){
console.log('doingSomething', $rootScope.$$phase);
$rootScope.someVar = true;
}
}
});
If I run it inside the controller like this
angular.module("app").controller("HomeController", function(myService){
myService.doSomething();
});
The console log gives:
doingSomething $apply
But if I run the service inside a unit test environment
it('should doSomething', inject(function(myService) {
myService.doSomething();
}));
The console log only get
doingSomething null
The typical answer that if you need to trigger the $digest cycle yourselves through $apply(). The angular js wiki mentioned about ng-click, $timeout and $http, but surely there are other places, such as running inside the controller. How can I determine that without trial and error?
It really depends on what you are testing. If you are testing something that requires a digest cycle (like $broadcast or $on events), then you will need to call scope.$apply() or simply scope.$digest() in the setup of the test.
However, most of the time, this is not required because services are usually designed as discrete pieces of functionality that do not require the scope. In the case of an HTTP call, the $httpBackend` service allows you to mock out the response.
For ng-click, you shouldn't be using this is a service. And for a controller you'll be binding to a function that you can test in isolation.
Hope this helps.
I have a resource with a custom update method :
angular.module('user.resources', ['ngResource']).
factory('User', function($resource) {
var User = $resource('/user/:id', {}, {
update: {
method: 'PUT'
}
});
User.prototype.update = function(cb) {
console.log('foo');
return User.update({
id: this._id
}, angular.extend({}, this, {
_id: undefined
}), cb);
};
I'm passing this resource to a custom directive via scope:
directive('avatarUpload', function($http) {
return {
restrict: 'E',
scope: {
model: '='
}, ...
and I'm calling the update method in the directive controller on a btn click:
$scope.model.update(function() {
console.log('bar');
});
The behavior which puzzle me atm is that clicking on the button the first time print 'foo' but not 'bar', clicking it a second time print 'bar', then 'foo'. Any more click always print 'bar' then 'foo'.
The PUT request is only fired from the second click and the ones after, never from the first.
Note: I've been using that resource update method fine in controllers, until trying to call it from a directive. I'm using angular 1.1.4
I do this resource passing because I want the directive to work on different type of resource.
It is hard to say for sure without seeing live code example but I presume that you are using AngularJS from the 1.1.x series (so called "unstable branch"). If so, the problem you are facing is linked to the new feature in AngularJS - HTTP request interceptors introduced in version 1.1.4 (this commit).
The newly introduced request interceptors are $q-based (promise-based) and in AngularJS world promises are only resolved as part of the $digest cycle. In other words you need to be in the "AngularJS world" ($digest cycle) for the promises to be resolved.
With the promise-based request interceptors there is a promise to be resolved before a $http call can be made. As as noted before this promise can only be resolved when you enter the $digest cycle. This won't happen if you are initiating the $http from outside of AngularJS (DOM event, setTimeout etc.).
In AngularJS $resource is based on $http so the above discussion apply to the $resource as well.
So, presuming that the above assumptions are correct and you are initiating the $resource call from outside of AngularJS (you are talking about a custom directive so I would bet on a DOM event) you should simply wrap the $resource call into scope.$apply.
Please note that wrapping the $resource call into $timeout (as suggested in another response), while will "fix" your issue (it will force a $digest loop and thus promises will get resolved) it is not the correct approach. The problem is that it will force a browser to leave the current JavaScript context and enter the repaint context for nothing. It will make your application slower and may result in UI flickering.