I have a service function that I need to call each time a route is visited or refreshed. The function returns an Angular promise. The result of the promise needs to be loaded into the controller's scope each time the function is called.
I'm currently using a resolve argument on the state definition to call the function.
.state('app.theState', {
url: '/theRoute/:theRouteId',
views: {
'menuContent': {
templateUrl: 'templates/theRoute.html',
controller: 'TheRouteCtrl'
}
},
resolve: {theResolvedResult: function(theService){
return theService.theAsyncCall().then(function(result){
return result;
},function(errMsg){
return errMsg;
});}
})
The data is passed into the controller as an argument.
.controller( 'TheRouteCtrl', function($scope, $state, $log, theResolvedResult)
I am updating the controller $scope in a watch on the global that holds theResolvedResult. (Using the workaround described at the end of this post). I tried a watch on the argument itself, but it never gets triggered.
$scope.$state=$state;
$scope.$watch('$state.$current.locals.globals.theResolvedResult', function (result) {
$scope.aValue = result.aValue;
});
Unfortunately, I guess because the watch is on a global, the watch is triggered when any route runs, and all other routes throw an error for every route except the one where the resolve is defined.
ionic.bundle.js:26794 TypeError: Cannot read property 'aValue' of undefined
at route_ctrl.js:234
How can I fix these errors, or is there a better way to do this?
Maybe just guard against the case where result is not defined:
$scope.$watch('$state.$current.locals.globals.theResolvedResult', function (result) {
if (result) {
$scope.aValue = result.aValue;
}
});
Breaking change for Angular V1.6
Move initialization logic into $onInit function
/* REPLACE
$scope.$state=$state;
$scope.$watch('$state.$current.locals.globals.theResolvedResult', function (result) {
$scope.aValue = result.aValue;
});
*/
//WITH
this.$onInit = function() {
$scope.aValue = $state.$current.locals.globals.theResolvedResult.aValue;
};
Initialization logic that relies on bindings being present should be put in the controller's $onInit() method, which is guaranteed to always be called after the bindings have been assigned.
— AngularJS Migration Guide (V1.6 $compile breaking change bcd0d4)
Erroneous rejection handler
The resolve is converting a rejected promise to a fulfilled promise:
/* ERRONEOUS
resolve: {theResolvedResult: function(theService){
return theService.theAsyncCall().then(function(result){
return result;
},function(errMsg){
//ERRONEOUS converts rejection
return errMsg;
});}
*/
// BETTER
resolve: {
theResolvedResult: function(theService){
return theService.theAsyncCall().then(function(result){
console.log(result);
return result;
},function catchHandler(errMsg){
console.log(errMsg);
//return errMsg;
//IMPORTANT re-throw error
throw errMsg;
});
}
It is important to re-throw the error in the catch handler. Otherwise the $q service converts the rejected promise to a fulfilled promise. This is similar to the way a try...catch statement works in vanilla JavaScript.
The $watch method take a function as its first argument:
/* HACKY
$scope.$state=$state;
$scope.$watch('$state.$current.locals.globals.theResolvedResult', function (result) {
$scope.aValue = result.aValue;
});
*/
//BETTER
//$scope.$state=$state;
//$scope.$watch('$state.$current.locals.globals.theResolvedResult',
$scope.$watch(
function () {
return $state.$current.locals.globals.theResolvedResult;
},
function (result) {
if (result) {
$scope.aValue = result.aValue;
};
});
When the first argument of the $watch method is a string, it evaluates it as an Angular Expression using $scope as its context. Since the $state service is not a property of $scope a function should be used instead.
The best bet is to avoid the $watch hack in the first place. Making sure that the value is good should happen in the Service, or at the very least in the resolve.
Check out this Plunker for a demo of how resolve should work.
The general idea is that resolve has one job; to make sure that the data is ready when we want it. The docs has a few examples on how ui.router handles resolve resolutions. They're a little limited, but generally show that "good" results are expected. Error handling is ultimately left up to your own choice.
Enjoy!
Related
I have a $promise service in controller and i want to call these services while application start.
CustomerService.fetchReligion.list().$promise.then(function(response){
$scope.religionList = response;
$window.localStorage.setItem('religionList', JSON.stringify(response));
}, function(error) {
// error handler
});
CustomerService.fetchCaste.list().$promise.then(function(response){
$scope.casteList = response;
$window.localStorage.setItem('casteList', JSON.stringify(response));
}, function(error) {
// error handler
});
How do i call these services?
One way to do this is by using UI-Router. UI-Router has a "Resolve" functionality in which, whenever the view changes to a particular state, promises shall be resolved and made into a value before the corresponding controller is instantiated. This means that whenever the new state/view (and its controller) is loaded, it is ensured that the required objects/values have already been retrieved and are ready for use.
The following snippet is directly taken from UI-Router's documentation.
$stateProvider.state('myState', {
resolve:{
...
// Example using function with returned promise.
// This is the typical use case of resolve.
// You need to inject any services that you are
// using, e.g. $http in this example
promiseObj: function($http){
// $http returns a promise for the url data
return $http({method: 'GET', url: '/someUrl'});
},
// Another promise example. If you need to do some
// processing of the result, use .then, and your
// promise is chained in for free. This is another
// typical use case of resolve.
promiseObj2: function($http){
return $http({method: 'GET', url: '/someUrl'})
.then (function (data) {
return doSomeStuffFirst(data);
});
},
...
// The controller waits for every one of the above items to be
// completely resolved before instantiation. For example, the
// controller will not instantiate until promiseObj's promise has
// been resolved. Then those objects are injected into the controller
// and available for use.
controller: function($scope, simpleObj, promiseObj, promiseObj2, translations, translations2, greeting){
...
// You can be sure that promiseObj is ready to use!
$scope.items = promiseObj.data.items;
$scope.items = promiseObj2.items;
...
}
})
I want to create a service that returns a json
Or by request to to the server, or by checking if it exists already in: Window.content
But I don't want to get a promise from my Controller !
I want to get the json ready !
I have tried several times in several ways
I tried to use with then method to do the test in my Service
but I still get a promise
( Whether with $http only, and whether with $q )
I could not get the value without getting promise from my Controller
My Service :
app.service('getContent',['$http', function( $http ){
return function(url){ // Getting utl
if(window.content){ // if it's the first loading, then there is a content here
var temp = window.content;
window.content = undefined;
return temp;
}
return $http.get(url);
};
}]);
My Controller:
.state('pages', {
url: '/:page',
templateProvider:['$templateRequest',
function($templateRequest){
return $templateRequest(BASE_URL + 'assets/angularTemplates/pages.html');
}],
controller: function($scope, $stateParams, getContent){
// Here I want to to get a json ready :
$scope.contentPage = getContent(BASE_URL + $stateParams.page + '?angular=pageName');
}
});
If the data exists, just resolve it in a promise.
While this process is still asynchronous it won't require a network call and returns quickly.
app.service('getContent',['$http', '$q', function( $http, $q ){
return function(url){
// create a deferred
var deferred = $q.defer();
if(window.content){ // if it's the first loading, then there is a content here
var temp = window.content;
window.content = undefined;
deferred.resolve(temp); // resolve the data
return deferred.promise; // return a promise
}
// if not, make a network call
return $http.get(url);
};
}]);
Just to reiterate, this asynchronous, but it won't require a network call.
This is not possible. If the code responsible to calculate or retrieve the value relies on a promise, you will not be able to return the value extracted from the promise by your function.
Explanation: This can easily be seen from the control flow. A promise is evaluated asynchronously. It may take several seconds to retrieve json from a server, but the caller of your function should not wait so long because your whole runtime environment would block. This is why you use promises in the first place. Promises are just a nice way to organize callbacks. So when your promise returns, the event that caused the function call will have already terminated. In fact it must have, otherwise your promise could not be evaluated.
You're thinking about this wrong. A service always returns a promise, because there is no synchronous way of getting JSON from an API:
app.factory('myService', ['$http', function($http) {
return $http('http://my_api.com/json', function(resp) {
return resp.data;
});
}]);
You would then call this within your controller like so:
app.controller('myController', ['$scope', 'myService', function($scope, myService) {
myService.then(function(data) {
$scope.contentPage = data; // here is your JSON
}, function(error) {
// Handle errors
});
}]);
Your service is returning a promise as it's written at the moment. A promise is always a promise, because you don't really know when it will be finished. However with Angular's 2 way data binding this isn't an issue. See my edits bellow as well as the example on $HTTP in the docs
In your controller
controller: function($scope, $stateParams, getContent){
getContent(BASE_URL + $stateParams.page + '?angular=pageName')
.then(aSuccessFn, aFailedFn);
function aSuccessFn(response) {
// work with data object, if the need to be accessed in your template, set you scope in the aSuccessFn.
$scope.contentPage = response.data;
}
function aFailedFn(data) {
// foo bar error handling.
}
}
I'm trying to add a utility method to attach notify listeners to Angular's $q promises, which are not provided by default for some reason. The intention is to provide an .update method that is chainable, similarly to the existing API:
myService.getSomeValue()
.then(function() { /* ... */ })
.catch(function() { /* ... */ })
.update(function() {
// do something useful with a notification update
});
Guided by an answer in Get state of Angular deferred? , and seeing from the Angular documentation for $q as well as the source code that catch is simply defined as promise.then(null, callback), I've implemented this config block:
.config(['$provide', function($provide) {
$provide.decorator('$q', function ($delegate) {
var defer = $delegate.defer;
$delegate.defer = function() {
var deferred = defer();
deferred.promise.update = function(callback) {
return deferred.promise.then(null, null, callback);
};
return deferred;
};
return $delegate;
});
}]);
Which kind of works, but it seems like the above decorator doesn't get set up immediately which breaks the chaining interface. The first time a $q.defer() is defined (maybe per block?)
first.promise
.then(function() { /* ... */ })
.update(function() {
// do something useful with a notification update
});
throws a TypeError: first.promise.then(...).update is not a function.
Example here: http://plnkr.co/edit/5utIm0HXpIKsjsA4H9oS
I've only noticed this when I was writing a simple example, I've used this code without issue when the promises were returned from a service and other promises had already been used (if this maybe would have an impact?). Is there any way to get the plunker example to work reliably when chaining immediately?
Deferred and Promise are two different APIs in Angular, and this way only the promise belonging to defer is being decorated, while the promise returned from then is not.
Both of them use Promise() constructor which isn't exposed anywhere on $q. However, Promise.prototype can be modified with
Object.getPrototypeOf(deferred.promise).update = function(callback) { ... };
So to summarise I am using angular-ui router resolve function to retrieve state specific data. However it doesn't seem to full wait for the promise to resolve:
state('parent.ChildState', {
url: '/myUrl?param1¶m1',
templateUrl: 'views/list.view.html',
controller: 'MyController',
resolve: {
data: resolveData
}
}).
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
DataService.myDataObj = DataService.get(filters, function(result, headers) {
DataService.myDataObj = result;
});
return DataService.myDataObj;
// Note I have also tried returning directly the DataService.get call however this makes all the below console log statements as undefined (see below for the controller code to know what I mean). So I do the assignment first and then return that.
}
Now in the controller I had a function that executes on load like so:
function ExpensesController(DataService) {
$scope.viewData = DataService;
initData();
function initData() {
// this generally logs a ngResource and shows the full data obj on console
console.log($scope.viewData.myDataObj);
// this gets undefined on console
console.log($scope.viewData.myDataObj.someField1);
// this log fine, however why do I need to do promise
// resolve? should be resolved already right?
$scope.viewData.myDataObj.$promise.then(function() {
console.log($scope.viewData.myDataObj.someField1);
});
As your required data to resolve is async, you need to return a promise and add return statement inside your callback function.
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
return DataService.get(filters, function(result, headers) {
DataService.myDataObj = result;
return DataService.myDataObj
});
}
You can read ui-router resolve docs more about how resolver works and when they should return promise or pure values.
I don;t know if I have got your problem :), but here is what I feel is wrong
1) in the resolve return a promise, it should not be resolved
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
return DataService.get(filters);
}
2) In the controller you should inject the data that is declared in resolve not the DataService so your controller should be
function ExpensesController(data) {
$scope.viewData = data;
}
Is there a way to call a function every time after a response is returned from a server without explicitly calling it after in the callback?
The main purpose is that I do have a generic error handler service that I call in every request's callback and I want to specify it somewhere and it shall be called automatically.
I gave Gloopy a +1 on solution, however, that other post he references does DOM manipulation in the function defined in the config and the interceptor. Instead, I moved the logic for starting spinner into the top of the intercepter and I use a variable in the $rootScope to control the hide/show of the spinner. It seems to work pretty well and I believe is much more testable.
<img ng-show="polling" src="images/ajax-loader.gif">
angular.module('myApp.services', ['ngResource']).
.config(function ($httpProvider) {
$httpProvider.responseInterceptors.push('myHttpInterceptor');
var spinnerFunction = function (data, headersGetter) {
return data;
};
$httpProvider.defaults.transformRequest.push(spinnerFunction);
})
//register the interceptor as a service, intercepts ALL angular ajax http calls
.factory('myHttpInterceptor', function ($q, $window, $rootScope) {
return function (promise) {
$rootScope.polling = true;
return promise.then(function (response) {
$rootScope.polling = false;
return response;
}, function (response) {
$rootScope.polling = false;
$rootScope.network_error = true;
return $q.reject(response);
});
};
})
// other code left out
If you mean for requests using $http or a $resource you can add generic error handling to responses by adding code to the $httpProvider.responseInterceptors. See more in this post.
Although it is about starting/stopping spinners using this fiddle you can add your code in the 'stop spinner' section with // do something on error. Thanks to zdam from the groups!