Angular: Promise dependant on other promises in routing - javascript

I'm a little confused when it comes to promises in Angular (or in general) and I just can't get this one to work the way I want it.
I can't share the code I'm working with but here's some mockup code:
var personModule = angular.module('personModule', [], ['$routeProvider', function($routeProvider){
$routeProvider
.when('/person/:personId/cars',{
templateUrl: 'partials/personCars.html',
controller: 'personCarsController',
resolve: {
'person': resolvePerson,
'cars': resolveCars
}
});
}]);
function resolvePerson($route, personFactory){
var personId = $route.current.params.personId;
return personFactory.getPerson(personId).then(function(response){
return angular.fromJson(response).data;
});
}
function resolveCars($route, personFactory, carFactory){
var personId = $route.current.params.personId;
return personFactory.getPersonCars(personId).then(function(response){
var cars = angular.fromJson(response).data;
angular.forEach(cars, function(car){
carFactory.getModel(car.modelId).then(function(response){
car.model = angular.fromJson(response).data;
carFactory.getMaker(car.model.makerId).then(function(response){
car.model.maker = angular.fromJson(response).data;
});
});
});
return cars;
});
}
The data format for cars will be something like this in JSON:
[
{
"year": "1992",
"color": "red",
"modelId": 1,
"model":
{
"name": "Corolla",
"makerId": 1,
"maker":
{
"name": "Toyota"
}
}
},
{
"year": "1998",
"color": "black",
"modelId": 1,
"model":
{
"name": "Escort",
"makerId": 2,
"maker":
{
"name": "Ford"
}
}
}
]
Now the code above works just fine. It's just that when I click on the link to open the page the data comes with a delay as the promises resolve. The routing waits for person and cars to resolve but not for the model and maker for each car, which is what I would like to happen. I've strolled through all Angulars documentation on promises and defers and I'm just at a loss right now.
EDIT:
function resolveCars($route, personFactory, carFactory){
var personId = $route.current.params.personId;
var promises = [];
return personFactory.getPersonCars(personId).then(function(response){
var cars = angular.fromJson(response).data;
angular.forEach(cars, function(car){
promises.push(carFactory.getModel(car.modelId).then(function(response){
console.log("Resolving model");
car.model = angular.fromJson(response).data;
promises.push(carFactory.getMaker(car.model.makerId).then(function(response){
console.log("Resolving maker");
car.model.maker = angular.fromJson(response).data;
}));
}));
});
return $q.all(promises).then(function(){
console.log("Resolved");
return cars;
});
});
}
Changed the code as suggested and now it waits for the carModel to resolve but not for the carMaker.
Console output for one car is this:
Resolving model
Resolved
Resolving maker
EDIT 2:
Solved it :) Changed the
promises.push(carFactory.getMaker(car.model.makerId).then(function(response){
console.log("Resolving maker");
car.model.maker = angular.fromJson(response).data;
}));
part to
return carFactory.getMaker(car.model.makerId).then(function(response){
console.log("Resolving maker");
car.model.maker = angular.fromJson(response).data;
});
Thanks for the help! I knew my original code wouldn't work like I wanted it to but couldn't for the life of me figure out how I should change it.

This happens, because the routing is resolved, as soon as the promise is resolved, that you define in your 'resolve' property of the route.
In your resolveCars Function you do resolve the cars and then inside you start your background "resolveModels" function. Nevertheless, 'cars' is resolved and you do return it. The background task won't stop the promise from resoling.
Your could return another promise, that will resolve only, when the models of all cars are resolved.
You could do something like this:
function resolveCars($route, personFactory, carFactory, $q) {
var personId = $route.current.params.personId;
return personFactory.getPersonCars(personId).then(function (response) {
var cars = angular.fromJson(response).data;
var promises = [];
var deferred = $q.defer();
angular.forEach(cars, function (car) {
var modelWithMakerPromise = carFactory.getModel(car.modelId).then(function (response) {
car.model = angular.fromJson(response).data;
return car.model;
}).then(function (model) {
return carFactory.getMaker(model.makerId).then(function (response) {
model.maker = angular.fromJson(response).data;
});
});
promises.push(modelWithMakerPromise);
};
$q.all(promises).then(function () {
defered.resolve(cars);
});
return defered.promise;
});
This is not tested, but should work and give you an idea.
Basically I create a new promise that i return. This promise will only resolve, when all those carModel Promises do resolve.
Have look at $q Api Doc
regards

Related

Angularjs $q is Not Returning Promise

I am trying to call service method in Angularjs. The service method is called alright, however IT DOES not return any value to the function that called it in the controller.
I will be glad if anyone can help me. This is my Code bellow.
logosStreams.factory("processPaypal", ['$http', '$timeout', '$q', '$state',function($http, $timeout, $q, $state){
var transCall = {};
var transcallPromise2 = $q.defer();
return {
onSuccesfulPayment: function(payment) {
console.log("payment success: " + JSON.stringify(payment, null, 4));
transcallPromise2.resolve(payment);
},
onAuthorizationCallback: function(authorization) {
console.log("authorization: " + JSON.stringify(authorization, null, 4));
//return authorization;
},
createPayment: function(data) {
// for simplicity use predefined amount
var paymentDetails = new PayPalPaymentDetails("0", "0", "0");
var payment = new PayPalPayment(data.amt, "USD", data.name, "Sale",
paymentDetails);
return payment;
},
buyInFutureBtn : function(e) {
// future payment
PayPalMobile.renderFuturePaymentUI(this.onAuthorizationCallback, this.onUserCanceled);
},
profileSharingBtn : function(e) {
// profile sharing
PayPalMobile.renderProfileSharingUI(["profile", "email", "phone","address", "futurepayments", "paypalattributes"],
this.onAuthorizationCallback, this.onUserCanceled);
},
buyNowBtn : function(data) {
// single payment
PayPalMobile.renderSinglePaymentUI(this.createPayment(data), this.onSuccesfulPayment, this.onUserCanceled);
return transcallPromise2.promise;
},
onPrepareRender: function() {
},
onUserCanceled: function(result) {
console.log(result);
transcallPromise2.reject(result);
}
}
}])
Inside the controller call the buyNowBtn methond
processPaypal.buyNowBtn($scope.MMParams).then(function(response){
console.log(response);
})
One of the possible reason that it does not return value is that your promise is not resolved. Your promise will only be resolved when you call onSuccessfulPayment().
Can you also put the code where your onSuccessfulPayment() function is executed.
You have returned the promise object on the buyNow function execution...try to return the promise as part of the returned object where you have returned all the function hookups.

Jasmine test a promise.then function

I try to test my app with Jasmine and got the following problem:
I will calculate something in the then function of my promise. That's the point where I need to test my code.
Here is the code of my controller:
TestCtrl.$inject = ["$scope", "TestService"];
/* ngInject */
function TestCtrl($scope, TestService) {
$scope.loadData = function () {
TestService.getData().then(function (response) {
$scope.data = response.data;
$scope.filtered = $scope.data.filter(function(item){
if(item.id > 1000){
return true;
}
return false;
})
});
}
}
And my Jasmine test code:
describe('TestService tests', function () {
var $q;
beforeEach(function () {
module('pilot.fw.user');
});
beforeEach(inject(function (_$q_) {
$q = _$q_;
}));
describe('UserController Tests', function () {
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
this.scope = $rootScope.$new();
this.$rootscope = $rootScope;
this.$httpBackend = _$httpBackend_;
this.scope = $rootScope.$new();
var TestServiceMock = {
getData: function () {
var deferred = $q.defer();
var result = [{
"id": 1720,
"user": 1132
},
{
"id": 720,
"user": 132
}, {
"id": 1721,
"user": 1132
}];
deferred.promise.data = result;
deferred.resolve(result);
return deferred.promise;
}
};
this.controller = $controller('TestCtrl', {
'$scope': this.scope,
'TestService': TestServiceMock
});
}));
it('test', function(){
this.scope.loadData();
expect(true).toBeTruthy();
})
});
});
The strange thing I don't understand is (tested with console logs):
My promise is created and returned
My loadData function is called and it will call the getData() function from the TestService
Everything inside the then function won't be executed although I return the promise as resolved
So how could I test the code inside the then function?
Thanks for help
the jasmine 'it' method takes a done parameter that you can call for async testing
it('Should be async', function(done) {
someAsyncFunction().then(function(result) {
expect(result).toBe(true);
done();
});
});
Feel free to go as deep as you want, just be sure to call done when EVERYTHING is finished. Jasmine's default timeout is 5 seconds per test, so if the async stuff isn't done by then jasmine will crash. You can change this setting in the configs or set it in the terminal.
This is straight from the jasmine docs, showing you how to handle the default timeout interval
describe("long asynchronous specs", function() {
var originalTimeout;
beforeEach(function() {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
});
it("takes a long time", function(done) {
setTimeout(function() {
done();
}, 9000);
});
afterEach(function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
});
I think that if it doesn't work in 10 seconds, you may have faulty methods. ESPECIALLY if you are talking to a local server / db. This stuff should only take this long if you are performing HEAVY computations, or are hitting an external api with a not-so-great internet connection. If everything is local (or stubbed / mocked!) then anything over 5-10 seconds is a definite red flag.
You better watch this https://codecraft.tv/courses/angular/unit-testing/asynchronous/
You have actually 3 ways:
1) use regular it:
it('test', (done) => {
const spy = spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
spy.calls.mostRecent().returnValue.then(res => {
...your expect here...
done();
})
} );
2) use async in beforeEach and it:
it('test', async(() => {
spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
fixture.whenStable().then(res => {
...your expect here...
})
} ));
3) use fakeAsync if you don't have Http or XHR calls:
it('test', fakeAsync(() => {
spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
tick();
...your expect here...
} ));
hope this solution helps. One approach I've found useful when testing is mocking dependencies. I've tried to comment out what I've done as much as possible.
var returnMock, $scope, TestServiceMock, controller;
beforeEach(module('app'));
beforeEach(inject(function($controller) {
returnMock = {
then: jasmine.createSpy(),
};
$scope = {};
// first assumption is You are testing TestService extensively,
// I don't care about what getData has to do to get results
// All I care about is it gets called when I call loadData
TestServiceMock = {
getData: jasmine.createSpy().and.returnValue(returnMock);
};
controller = $controller;
}));
it('should load data when loadData function is called and result set is
under 1000', function() {
controller('TestCtrl', {
$scope,
TestServiceMock
});
// another assumption is your data comes back in such a format
// perhaps in the actual code check whether data exists and proceed
// or do some other action
var returnedData = {
data: [
{
id: 1,
name: 'item 1',
},
]
}
// when I execute the function/method
$scope.loadData();
// I expect getData to be called
expect(TestServiceMock.getData).toHaveBeenCalled();
// I expect then to be called and the reason is I mocked it
expect(returnMock.then).toHaveBeenCalledWith(jasmine.any(Function));
returnMock.then.calls.mostRecent().args[0](returnedData);
// expect data on scope to be equal to my mocked data
expect($scope.data).toEqual(returnedData.data);
// don't expect any result because 1 < 1000
expect($scope.filtered).toEqual([]);
expect($scope.filtered.length).toEqual(0);
});
it('should load data when loadData function is called and result set is over 1000',
function() {
controller('TestCtrl', {
$scope,
TestServiceMock
});
var returnedData = {
data: [
{
id: 1,
name: 'item 1',
},
{
id: 1000,
name: 'item 1000',
},
{
id: 1001,
name: 'item 1000',
},
{
id: 1002,
name: 'item 1002',
}
]
}
$scope.loadData();
expect(TestServiceMock.getData).toHaveBeenCalled();
expect(returnMock.then).toHaveBeenCalledWith(jasmine.any(Function));
returnMock.then.calls.mostRecent().args[0](returnedData);
expect($scope.data).toEqual(returnedData.data);
// expect a result because some entries in the mocked data have id > 1000
expect($scope.filtered).toEqual([
{
id: 1001,
name: 'item 1000',
},
{
id: 1002,
name: 'item 1002',
}]);
expect($scope.filtered.length).toEqual(2);
});
Official Jasmine Docs explain most of the concepts extensively. Hope the solution helps!!!!
Let me tell ya what I do, for Angular 1.x and 2.x+ projects. Use the angular testing tools to get rid of callbacks/nests in your async tests. In angular 1.x, that means using a combination of $q and $rootScope.$apply(). In angular 2.x+, that means using something like fakeAsync.
From the Angular 1.x docs
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value) { resolvedValue = value; });
expect(resolvedValue).toBeUndefined();
// Simulate resolving of promise
deferred.resolve(123);
// Note that the 'then' function does not get called synchronously.
// This is because we want the promise API to always be async, whether or not
// it got called synchronously or asynchronously.
expect(resolvedValue).toBeUndefined();
// Propagate promise resolution to 'then' functions using $apply().
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));
The disadvantage is that your code is tied to angular, the advantages are that your code is flat and it's portable to 2.x+!
I was a fan of the mocha test runner that allowed me to return promises in my tests, you could try to get that going, but there are downsides to that as well like needing to modify your code specifically for a test.
In regards to your controller, you should 'return' values like so.
TestCtrl.$inject = ["$scope", "TestService"];
/* ngInject */
function TestCtrl($scope, TestService) {
$scope.loadData = function () {
// Return this call, since it will return a new promise
// This is what let's you do $scope.loadData.then()
return TestService.getData().then(function (response) {
// What you return in here will be the first argument
// of your then method, in the tests / any env
// Ex. return 'foo'
// will result in .then(result => result === 'foo') //=> true
// return one of these, i suggest the data, go SRP!
return $scope.data = response.data;
// I would do this stuff in a separate function, but you
// can return 'filtered' instead if you like.
//
// $scope.filtered = $scope.data.filter(function(item){
// if(item.id > 1000){
// return true;
// }
// return false;
// });
});
}
}
Remember that calling something AFTER 'then' doesn't mean anything, values must be called INSIDE 'then'. Not after it, or before it. But inside it. Like Tom Green and that poor moose in Freddy Got Fingered.

Two promises inside a hash in $q.all

I'm trying to make two API calls in parallel using $q.all, and return both of their responses as one to the controller, and when I break on the return lines for each promise inside the hash, they return the expected data, but it all seem to break when it reaches $q.all. This is all in a ui-router state, and I'm using resolve to supposedly provide the corresponding controller with the data from $q.all
It was originally written in Coffeescript, but here it is in Javascript:
resolve: {
content: [
'APIService', function($q, $timeout, APIService) {
var firstPromise, secondPromise, promises;
firstPromise = $q.defer();
secondPromise = $q.defer();
promises = {
firstPromise: APIService.get('/some/api/call').then(function(response) {
return response;
}),
secondPromise: APIService.get('/another/api/call').then(function(response) {
return response;
})
};
$.blockUI();
$timeout(function() {
firstPromise.resolve('firstPromise');
secondPromise.resolve('secondPromise');
}, 1000);
$q.all(promises).then(function(responses) {
$.unblockUI();
return responses;
});
return false;
}
]
}
Please help? I'm using Angular 1.3.15
content:[ should actually be :
resolve: {
content: function(){
// <create promise array>
return $q.all(promises);
}
}
It's hard to recreate your logic, but this example works well and hope will help (Angular 1.3)
angular.module('myApp',[])
.controller('MyCtrl', function ($scope, $q, $timeout) {
$scope.name = "Superhero"
var prom1 = $q.defer();
var prom2 = $q.defer();
var promises = {
prom1: prom1.promise,
prom2: prom2.promise
};
$timeout(function () {
prom1.resolve('prom1');
prom2.resolve('prom2');
}, 1000);
$q.all(promises).then(function (responces) {
$scope.prom1 = responces.prom1;
$scope.prom2 = responces.prom2;
});
});
http://jsfiddle.net/ugja9gth/1/
Example with real $http
http://jsfiddle.net/Evgeniy_D/ugja9gth/2/

Angular - Multiple $http.get Requests for Data

What is the best approach to take when making multiple calls to an API for data needed in the same view?
For example, you have multiple select boxes which need to contain data pulled in from outside the app, all in the same view.
Is there a more elegant solution than simply firing them all at once in your controller? Such as the following
app.controller('myCtrl', function($service) {
$service.getDataOne().then(function(response) {
$scope.dataOne = response;
}, function(error) {
console.log(error);
});
$service.getDataTwo().then(function(response) {
$scope.dataTwo = response;
}, function(error) {
console.log(error);
})
});
etc...with each service function performing a $http.get request.
While this works, I feel there is probably a more elegant solution.
Any thoughts as always is much appreciated.
You can use q.all(), as it accepts an array of promises that will only be resolved when all of the promises have been resolved.
$q.all([
$service.getDataOne(),
$service.getDataTwo()
]).then(function(data){
$scope.dataOne = data[0];
$scope.dataTwo = data[1];
});
If you look at the link, q.All() is at the very bottom of the page
I believe you are looking for the $q service.
http://jsfiddle.net/Zenuka/pHEf9/21/
https://docs.angularjs.org/api/ng/service/$q
function TodoCtrl($scope, $q, $timeout) {
function createPromise(name, timeout, willSucceed) {
$scope[name] = 'Running';
var deferred = $q.defer();
$timeout(function() {
if (willSucceed) {
$scope[name] = 'Completed';
deferred.resolve(name);
} else {
$scope[name] = 'Failed';
deferred.reject(name);
}
}, timeout * 1000);
return deferred.promise;
}
// Create 5 promises
var promises = [];
var names = [];
for (var i = 1; i <= 5; i++) {
var willSucceed = true;
if (i == 2) willSucceed = false;
promises.push(createPromise('Promise' + i, i, willSucceed));
}
// Wait for all promises
$scope.Status1 = 'Waiting';
$scope.Status2 = 'Waiting';
$q.all(promises).then(
function() {
$scope.Status1 = 'Done';
},
function() {
$scope.Status1 = 'Failed';
}
).finally(function() {
$scope.Status2 = 'Done waiting';
});
}
Credit: Code shamelessly stolen from unknown creator of fiddle.
If it for loading the looku pdata for all the dropdowns, I would make one call to get all the lookup data in one single payload. Something like this. Each property value is an array of items for each dropdown.
{
"eventMethods": [
{
"name": "In Person",
"id": 1
},
{
"name": "Phone",
"id": 2
}
],
"statuses": [
{
"name": "Cancelled",
"id": 42
},
{
"name": "Complete",
"id": 41
}
]
}

AngularJS: Working with callbacks and promises

I am unable to wrap my brain around the concept of asynchronous requests.
I have a controller for my view, which is creating an object instance from a provider:
va.controller('VaCtrl',function($scope,$shipment){
$scope.shipment = $shipment.Shipment();
});
The provider:
Shipment.provider('$shipment',function(){
this.$get = function($http){
function Shipment(){
}
Shipment.prototype.fetchShipment = function(){
var shipment = undefined;
$http.post('../sys/core/fetchShipment.php',{
// some data to POST
}).then(function(promise){
shipment = promise.data;
});
return shipment;
};
return {
Shipment: function(){
return new Shipment();
}
}
}
});
My goal is to get access to the data from Shipment.prototype.fetchShipment() inside my controller. My approach:
$scope.fetchShipment = function(){
var shipment = $scope.shipment.fetchShipment();
console.log(shipment); // undefined
};
However, this will return undefined.
I read about $q, and defers, promises and callbacks, and now i am like WTF; all i want to do is to push the retrieved data to my controller, what is the best possible way to do so?
You should modify your code as shown below to return the promise from fetchshipment directly, and then use then() inside your controller.
Shipment.prototype.fetchShipment = function(){
return $http.post('../sys/core/fetchShipment.php',{
// some data to POST
})
};
$scope.fetchShipment = function(){
var shipment = $scope.shipment.fetchShipment().then(function(data){;
console.log(data);
});
};
Explanation to Code :
Calling $http return a promise which is resolved when you get the data from the server. In the code above, I have returned $http.post from service function which returns a promise. So in the controller you are waiting for promise to be resolved, and when the promise is resolved, the result is logged to the console.
Read about more promise documentation on angular:
http://docs.angularjs.org/api/ng.$q
http://docs.angularjs.org/api/ng.$http
Just the give you an example how to get your example working with your own promise.
It's much more simple if you use $http builtin promise, so it's an $q-example:
angular.module('myApp', []).controller("myAppCtrl", function ($scope, $shipment) {
$shipment.Shipment().fetchShipment().then(function (shipment) {
$scope.shipment = shipment
});
}).provider('$shipment', function () {
this.$get = function ($http, $q) {
function Shipment() {
}
Shipment.prototype.fetchShipment = function () {
var defered = $q.defer();
demodata = {name: "jan", id:8282};
$http.post('/echo/json/', 'json=' + encodeURIComponent(angular.toJson(demodata)), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
}).then(function (response) {
//resolve promise
defered.resolve(response.data);
});
return defered.promise;
};
return {
Shipment: function () {
return new Shipment();
}
}
}
});
<div ng-controller="myAppCtrl">{{shipment}}</div>
JSFiddle (use JSFiddle echo-service as data provider):
http://jsfiddle.net/alfrescian/ayke2/
More about promises:
http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/
http://www.egghead.io/video/o84ryzNp36Q
AngularJS : Where to use promises?
stackoverflow.com/questions/15604196/… egghead.io/video/o84ryzNp36Q

Categories