Why can I not chain promises in a route 'resolve'? - angularjs

I am trying to chain the promise that fetches 'dtaDashboard' to use a value from the fetched dashboard, to execute a second call. For some reason the promise chain does not seem to work inside a 'resolve'. Why not?
state('uploadServiceTxData', {
url: '/dashboard/:id/upload',
templateUrl: 'views/dashboard-upload.html',
controller: 'DashboardUploadController',
resolve: {
dtaRefData: function(RefDataService) {
return RefDataService.getRefData();
},
dtaDashboard: function(DashboardService, $stateParams) {
return DashboardService.get({ id: $stateParams.id });
},
dtaUploads: function(TransactionSummaryUploadService, dtaDashboard, $q) {
return dtaDashboard.then(function(dashboard) { return TransactionSummaryUploadService.findByExample({ service: dashboard.service.name }) });
}
}
});
Some more information on the code above. The functions on the DashboardSerivce and TransactionSummaryUploadService, are implemented with $resource. So "get" and "findByExample" use $resource to make REST calls.

You are very close.
dtaDashboard has already been resolved when it's injected into datUploads. So you use the resolved result (meaning it's no longer a promise).
state('uploadServiceTxData', {
url: '/dashboard/:id/upload',
templateUrl: 'views/dashboard-upload.html',
controller: 'DashboardUploadController',
resolve: {
dtaRefData: function(RefDataService) {
return RefDataService.getRefData();
},
dtaDashboard: function(DashboardService, $stateParams) {
return DashboardService.get({ id: $stateParams.id });
},
dtaUploads: function(TransactionSummaryUploadService, dtaDashboard, $q) {
return TransactionSummaryUploadService.findByExample({ service: dtaDashboard.service.name });
}
}
});

The reason it did not work is that the services were implemented with $resource. The methods of $resource do not return promises, they return an object which contains a promise, and some other things. To get to the promise itself, I needed to use dtaDashboard.$promise.
So code like this works:
dtaDashboard: function(DashboardService, $stateParams) {
return DashboardService.get({ id: $stateParams.id });
},
dtaUploads: function(dtaDashboard) {
dtaDashboard.$promise.then(function(result) { ... /* result contains the data now. */ });
}
return DashboardService.get({ id: $stateParams.id });
Does not return the promise from the $resource service, it returns an object containing the promise (and eventually the data); that object is resolved, but not the promise it contains. To ensure that the route resolves to the data, this can be done instead:
dtaDashboard: function(DashboardService, $stateParams) {
return DashboardService.get({ id: $stateParams.id }).$promise;
}
Now dtaDashboard will be fully resolved to the data wherever it is injected.

Related

AngularJS service function not returning full promise after while loop has completed

I'm trying to paginate a map. I want to force the controller to wait for the service which performs the pagination to return all the available products before trying to do anything.
I've implemented a resolve function in my router which will call the service that performs the pagination (using a while loop) and fetches the results.
When running the code, inside the controller the number of "products" is always 1/2 or 2/3rds of the full amount depending on the browser being used. The controller initialises before the while loop has finished running/returning all the promises. I'm a bit confused as to how to force the controller to wait for all the results from the while loop be fetched. Do I need to make a change in my service function, my controller or the router (or all three?). Or, is it that my use of the $q library is incorrect?
Inside my router:
.state('products-map', {
url: "/products-map",
parent: 'base',
templateUrl: "../components/products/products.map.html",
data: { pageTitle: 'Products Map', pageSubTitle: 'map of all the products'},
controller: "ProductsMapController",
resolve: {
deps: ['$ocLazyLoad', function($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'myApp',
insertBefore: '#ng_load_plugins_before',
files: [
{ type: 'js', path: '//maps.google.com/maps/api/js?v=3&key=<my_api_key>&libraries=visualization,geometry'},
'../js/markerclusterer_packed.js',
]
});
}],
total_products: function(DashboardService) {
return DashboardService.getTotalProducts();
},
products: function(DashboardService, total_products){
return DashboardService.getPaginatedProducts(total_products);
},
}
})
Inside my service function which handles the pagination:
this.getPaginatedProducts = function(total_product_count) {
var total_fetched_results = 0;
var total_number_of_products = total_product_count;
console.log("total number of products in paginated products: ", total_number_of_products);
var results_per_page = 20000;
var page = 1;
var products = [];
var deferred = $q.defer();
while (total_number_of_products > total_fetched_results) {
$http({
url: $rootScope.apiUrl + 'products/location',
method: "GET",
params: { 'results_per_page': results_per_page, 'page': page}
}).then(function(response) {
console.log("response from service", response);
var response_data = response.data.data;
for (var i = 0; i < response_data.length; i++) {
products.push(response_data[i]);
}
});
page++;
total_fetched_results += results_per_page;
}
deferred.resolve(products);
return deferred.promise;
};
Inside my controller:
angular.module('SS2').controller('ProductsMapController', ['$rootScope', 'products', '$scope', '$http', 'NgMap', 'total_products',
function ($rootScope, products, $scope, $http, NgMap, total_products) {
$scope.$on('mapInitialized', function (event, map) {
console.log("ProductsMapController Initialised")
console.log("total_products inside ProductsMapController is: ", total_products);
$scope.map_products = products;
console.log("inside controller number of products is: ", products);
There's a few approaches you can take here and using Promises certainly make sense. What I'm seeing happening with your while loop is a logic flaw where your $http requests aren't all resolved before your function hits its exit condition.
You might want to look into using $q.all() and building out an array of $http calls to make sure everything is resolved BEFORE you return your results as I suspect, when you look at your network monitor, you'll see calls not resolved before the results are rendered.
https://docs.angularjs.org/api/ng/service/$q#all
an excellent example is https://chariotsolutions.com/blog/post/angularjs-corner-using-promises-q-handle-asynchronous-calls/

while unit testing ui-router config, ui-router's resolve function returns promise during expectation even after the service promise was resolved

Below is the sample route configuration that I have for myApp using ui-router
angular.module('myApp', ['ui.router']);
angular.module('myApp').config(stateConfig);
stateConfig.$inject = ['$stateProvider','$urlRouterProvider'];
function stateConfig($stateProvider, $urlRouterProvider) {
$stateProvider.state('view1', {
url: '/view1/:id?',
templateUrl: 'app/view1/view1.html',
resolve:{
init: ['$stateParams', 'view1Service', function($stateParams, view1Service){
if($stateParams.id !== ''){
return view1Service.getIdData($stateParams.id)
.then(function(response){
return { data: response.data, responseStatus: response.status };
}, function(response){
return { data:{}, responseStatus: response.status };
});
}
else{
return { data:{}, responseStatus: 200 };
}
}]
},
controller: 'View1Controller as controllerOne'
})
//some other routes with similar configuration
$urlRouterProvider.otherwise('/view1/');
}
Here is the spec for the above code that I have for now. Since resolve function for view1 state is dependent on view1Service I have mocked view1Service and also made it to return a promise(if promise was not returned from mocked service then infinite digest() loop was occuring).
describe('ui router config', function() {
var $rootScope, $state, $injector, myServiceMock, state = 'view2', deferred, mockedService;
beforeEach(function() {
angular.mock.module('myApp');
angular.mock.module('ui.router');
angular.mock.module(function($provide){
$provide.factory('view1Service', function($q){
function getIdData(id){
deferred = $q.defer();
return deferred.promise;
}
return {getIdData: getIdData}
});
});
inject(function(_$rootScope_, _$state_, _$injector_, $templateCache, _$stateParams_, view1Service) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
$stateParams = _$stateParams_;
$templateCache.put('app/view1/view1.html', '')
})
});
it('should respond to URL', function() {
expect($state.href(state, { id: 1 })).toEqual('#/view1/1');
});
it('should resolve data', function() {
$state.go(state, {id: '9999'});
deferred.resolve({
data: 'some data',
status: 666
});
$rootScope.$digest();
expect($state).toBe('checking');
expect($state.current.name).toBe(state+'q');
// Call invoke to inject dependencies and run function
expect($injector.invoke($state.current.resolve.init)).toBe('findAll+1');//this assertion fails with below error
});
});
I'm currently able to assert on the current state. I would like to test the resolve function's success and failure callback as well.
However I keep getting following error:
Expected Promise({ $$state: Object({ status: 0 }) }) to be 'findAll+1'.
Any idea why resolve block keeps returning Promise object as above. First of all it shouldn't be returning a promise since view1Service was resolved. And to my understanding even if resolve block invocation returns a promise doesn't expect statement wait till its resolved? I tried even using .then on invocation call, that didn't work either.
Any help is much appreciated.
You are transitioning to the state, which calls the resolve functions, and then you "resolve()" your deferred. All good here. But then you invoke the init function later, which returns a promise, which is an good. But you want to resolve the deferred after this, now that it's been setup.
Basically, I think you are calling "init" twice, once when you call "$state.go", and again explicitly afterwards.
You should be able to first do a $state.get('view1').resolve.init, to grab the init function that you want to test directly. Otherwise, calling "$state.go" will run it automatically.
Hope that helps!

Awaiting for services to return data or converting service into global dataset and passing it into angular controller?

I have simple problem of needing to wait on certain data to return from service calls before executing logic that depends on the data in question.
As confusing as it sounds I have this extract controller that I am working on at the moment which is exhibiting that problem.
// async services: $stateParams, GetTags, GetStrands, GetLessons, GetPlan, UpdatePlan, SavePlan
myApp.controller('EditNewCtrl', ['$scope', '$stateParams', 'GetTags', 'GetStrands', 'GetLessons', 'GetPlan', 'UpdatePlan', 'SavePlan', function ($scope, $stateParams, GetTags, GetStrands, GetLessons, GetPlan, UpdatePlan, SavePlan) {
// $stateParams correspondent to two different routes for this controller
// #/~/
// #/~/:planId
// #/~/:planId/:year/:level
$scope.planId = $stateParams.planId; // only defined when editing a plan
$scope.year = $stateParams.year; // may or may not be defined
$scope.level = $stateParams.level; // may or may not be defined
...
// calls to retrieve stuff from the server
GetTags.get({ groupId: 12, }, function (data) {
$scope.tags = data.tags; // go know when this will return
});
GetStrands.get({ groupId: 12, }, function (data) {
$scope.strands = data.strands; // god knows when this will return
});
GetLessons.get({ groupId: 12, }, function (data) {
$scope.lessons = data.lessons; // god know when this will return
});
// helpers
...
// init select controls
if ($scope.planId != undefined && $scope.planId > 0) {
GetPlan.get({ planId: $scope.planId, groupId: 12, }, function (data) {
var plan = data.plan; // god know when this will return
plan.Year = $scope.getYearById(plan.Year); // convert int to object
plan.Level = $scope.getLevelById(plan.Level); // convert in to object
$scope.plan = plan;
});
} else {
$scope.plan = { Name: '', Strand: {}, Year: {}, Level: {}, Tags: [], Lessons: [], };
}
if ($scope.year != undefined) {
$scope.plan.Year = $scope.getYearObj($scope.year);
}
if ($scope.level != undefined) {
$scope.plan.Level = $scope.getLevelObj($scope.level);
}
}]);
More often then not I run into a problem with $scope.plan.Year = $scope.getYearObj($scope.year); and $scope.plan.Level = $scope.getLevelObj($scope.level); when I enter edit mode. While I understand that service call happens asynchronously but what is the common ways of slowing down subsequent calls? Or perhaps its better to just encapsulate problem loginc in $scope.$watch?
I have another concern with $scope.tags and $scope.strands. Is it possible to have these datasets pre-fetched and managed (when I say managed I mean refreshed every so often in the background) on a more global level and have them passed in as references instead rather than retrieving these in every controller that I come up with. Please advise if there is Angular structure or mechanism for something like this.
In any case it is clear to me that I am doing something wrong here. Please advice what is the best course of action or where I should look.
Complementary notes
Just to complement suggested solution to my dilemma.
Because I am not using the $http services but instead I use AngularJs REST/factory services. Example of such service would look like so:
myApp.factory('GetTags', ['$resource', function ($resource) {
return $resource('/API/Service/GetTagList', {}, {
query: { method: 'GET', params: {}, }, isArray: true,
});
}]);
How to use this in a controller is already shown above but sometimes that's not enough. This is how one would use this service in a situation when you need access to then:
.state('state', {
url: '/url:Id',
templateUrl: '/template.html',
resolve: {
Tags: function (GetTags) {
//TODO: need to make a directive that returns groupId
return GetTags.get({ groupId: 12, }).$promise.then(
function (data) {
if (data.success) {
return data.tags;
} else {
return [];
}
});
},
},
controller: 'EditNewCtrl',
})
Here $promise is used to gain access to the raw $http promise object which allows us to use .then() to await for the call to resolve. Without $promise in other words just return GetTags.get({ groupId: 12, }) would return the promise object to the controller in question which no good.
To gain access to for example $stateParams.Id just pass it into the function call like so:
Tags: function (GetTags, $stateParams) {
return $stateParams.Id;
},
That's about it really. Just don't forget to pass in your resolved data objects/structures into your controller.
PS: Also important to note that definition for controller must come after definition for resolve otherwise it doesn't work.
PSS: I hope that advice that I have received and my example helps to complement the answers given.
As someone already mentioned resolve in the $stateProvider is the way to go.
However what you could also do is this :
'use strict';
angular.module('YourApp')
.service('YourService', function ($http) {
return {
doSomething: function (id, success, error) {
return $http.post(
'rest/bla/' + id, {}
).success(function (response) {
success(response);
}).error(function () {
error();
});
},
doSomethingElse: function (id, success, error) {
return $http.post(
'rest/bla/' + id, {}
).success(function (response) {
success(response);
}).error(function () {
error();
});
},
doSomethingDifferent: function (id, success, error) {
return $http.post(
'rest/bla/' + id, {}
).success(function (response) {
success(response);
}).error(function () {
error();
});
},
};
});
//then in your controller
angular.module('YourApp')
.controller('YourController', function(YourService){
// you add a success and error function for when the data is returned.
YourService.doSomething(id, function(data){
$scope.yourData = data;
YourService.doSomethingElse(id, function(data){
$scope.somethingElse = data;
YourService.doSomethingDifferent(id, function(data){
$scope.somethingDifferent = data;
// al three have been set so you can place initialization code here
}
}
}, function(){console.log('something went wrong'));
});
but what you really should do is this
$stateProvider.state('myState', {
url: 'the/url/you/want',
resolve:{
yourService: 'yourService' // you are dependency injecting it here,
yourFetch: function (yourService) {
return yourService.yourFetch.$promise;
},
yourSecondFetch: function(yourService) {
return yourService.yourSecondFetch.$promise;
},
yourTirthFetch: function(yourService) {
return yourService.yourTirthFetch.$promise;
},
controller: 'YourController'
})
// then your controller can just inject the yourFetch and they will be resolved before your controller loads so it will always be fetched prior
.controller('YourController', function($scope, yourFetch, yourSecondFetch, yourTirthFetch) {
$scope.yourFetch = yourFetch;
$scope.secondFetch = yourSecondFetch;
$scope.tirthFetch = yourTirthFetch;
});
I abstracted the idea of #Arno_Geismar into a reusable component/service, but as stated previously this is probably a bad idea of making asychronous code synchronous.
self.init = function (array, fx) {
if (array.length > 0) {
//The request for data.
if (array[0].data=== null) {
$http.get(array[0].link).success(function ($response) {
window.session.set(array[0].ref, $response);
array[0].data= $response;
check(array);
}).error(function () {
self.init(array, fx);
});
} else {
check(array);
}
} else {
exec(fx);
}
//Check whether the recursive function can stop.
function check(array) {
//Bypass when all the reference data has been set previously.
//All objects are set = exit recursive
if (array.every(function (e) {
return e.data!== null;
})) {
exec(fx);
} else {
self.init(array.slice(1, array.length), fx);
}
}
//Function to execute the fx, if available.
function exec(fx) {
if (fx !== null) {
fx();
}
}
};

defer.promise printing out object and not data

I have this code:
app.factory('loadDependencies', function ($q, $timeout) {
return {
load: function () {
console.log("start 1");
var defer = $q.defer();
$timeout(function () {
defer.resolve({ resolve: "got dependencies" });
}, 3000);
return defer.promise;
}
}
});
But the problem is that defer.promise doesn't wait until timeout ends, and it also just print out the object properteis and not the data, it prints out :
Object { then: qFactory/defer/deferred.promise.then(), catch: qFactory/defer/deferred.promise.catch(), finally: qFactory/defer/deferred.promise.finally() }
I'm new to angular and trying to understand what did i do wrong?
edit
Some more info about what i was trying to accomplish.
I have a resolve in route that supposed to dynamically load controllers and css files.
.when('/url', {
templateUrl: 'someview',
controller: 'somecontroller',
resolve: {
load: function (loadDependencies) {
loadDependencies.load(); // here i need to know get the result of what's inside $timeout of 'load'
}
}
})
That's because defer.promise does not return your data and also does not wait until your timeout is completed. So what do you do with it is assign some handlers and wait for it to complete/fail:
function success(data) {
//this is called after defer.resolve({ resolve: "got dependencies" }); gets executed
//here you can access your data
}
function error(error) {
//this is called if the promise gets rejected
}
loadDependencies.load().then(success, error);
Now when your promise completes, it automatically calls one of those handlers. There are more possibilities to apply callbacks, such as the .finally() method. Have a look at the documentation.
EDIT:
Just return the promise in your resolver:
.when('/url', {
templateUrl: 'someview',
controller: 'somecontroller',
resolve: {
load: function (loadDependencies) {
return loadDependencies.load(); // here i need to know get the result of what's inside $timeout of 'load'
}
}
})
Now the controller will be called after the promise is resolved and angular will pass the resolved value to it like so:
app.controller("CtrlName", function($scope, load){
//access your load value here
});

Angular ui-router get asynchronous data with resolve

I want to display a form with data corresponding to the edited item. I use ui-router for routing. I defined a state:
myapp.config(function($stateProvider) {
$stateProvider.
.state('layout.propertyedit', {
url: "/properties/:propertyId",
views : {
"contentView#": {
templateUrl : 'partials/content2.html',
controller: 'PropertyController'
}
}
});
In PropertyController, I want to set $scope.property with data coming from the following call (Google Cloud Endpoints):
gapi.client.realestate.get(propertyId).execute(function(resp) {
console.log(resp);
});
I don't know if I can use resolve because the data are returned asynchronously. I tried
resolve: {
propertyData: function() {
return gapi.client.realestate.get(propertyId).execute(function(resp) {
console.log(resp);
});
}
}
First issue, the propertyId is undefined. How do you get the propertyId from the url: "/properties/:propertyId"?
Basically I want to set $scope.property in PropertyController to the resp object returned by the async call.
EDIT:
myapp.controller('PropertyController', function($scope, , $stateParams, $q) {
$scope.property = {};
$scope.create = function(property) {
}
$scope.update = function(property) {
}
function loadData() {
var deferred = $q.defer();
gapi.client.realestate.get({'id': '11'}).execute(function(resp) {
deferred.resolve(resp);
});
$scope.property = deferred.promise;
}
});
You need to read the docs for resolve. Resolve functions are injectable, and you can use $stateParams to get the correct value from your routes, like so:
resolve: {
propertyData: function($stateParams, $q) {
// The gapi.client.realestate object should really be wrapped in an
// injectable service for testability...
var deferred = $q.defer();
gapi.client.realestate.get($stateParams.propertyId).execute(function(r) {
deferred.resolve(r);
});
return deferred.promise;
}
}
Finally, the values for resolve functions are injectable in your controller once resolved:
myapp.controller('PropertyController', function($scope, propertyData) {
$scope.property = propertyData;
});
I think your controller function needs $stateParams parameter from which you can get your propertyId. Then you can use $q parameter and create promise to set $scope.property with something like this:
var deferred = $q.defer();
gapi.client.realestate.get(propertyId).execute(function(resp) {
deferred.resolve(resp);
});
$scope.property=deferred.promise;
Here is description of using promises for handling async calls.
Try this easy way to use resolve in proper way
State code:
.state('yourstate', {
url: '/demo/action/:id',
templateUrl: './view/demo.html',
resolve:{
actionData: function(actionData, $q, $stateParams, $http){
return actionData.actionDataJson($stateParams.id);
}
},
controller: "DemoController",
controllerAs : "DemoCtrl"
})
In the above code I am sending parameter data which I am sending in the url,For examples if i send like this /demo/action/5
this number 5 will go to actionData service that service retrieve some json data based on id.Finally that data will store into actionData You can use that in your controller directly by using that name
Following code return some JSON data based on id which iam passing at state level
(function retriveDemoJsonData(){
angular.module('yourModuleName').factory('actionData', function ($q, $http) {
var data={};
data.actionDataJson = function(id){
//The original business logic will apply based on URL Param ID
var defObj = $q.defer();
$http.get('demodata.json')
.then(function(res){
defObj.resolve(res.data[0]);
});
return defObj.promise;
}
return data;
});
})();
How about this:
function PropertyController($scope, $stateParams) {
gapi.client.realestate.get($stateParams.propertyId).execute(function(resp) {
$scope.property = resp;
});
}

Resources