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
}
]
}
Related
I'm loading forum data stored in several JSON to be displayed on a website, which then gets rendered using React. The data itself is seperated in two types of files: thread data and user data.
// threads/135.json
{
"title": "Thread Title",
"tid": 135,
"posts": [
{
"pid": 1234,
"timestamp": 1034546400,
"body": "First forum post",
"user": 5678
},
{
"pid": 1235,
"timestamp": 103454700,
"body": "Reply to first forum post",
"user": 9876
}
]
}
Once the thread data is loaded, user data is loaded based on the user ID.
// user/1234.json
{
"id": 1234,
"name": "John Doe",
"location": "USA"
}
The actual code is based on Google's Introduction to Promises, turned into a function it looks like this:
export default function loadData(id) {
var didLoadUser = [];
var dataUsers = {};
var dataThread = {};
getJSON('./threads/' + id + '.json').then(function(thread) {
dataThread = thread;
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
thread.posts.map(function(post) {
if (post.user > 0 && didLoadUser.indexOf(post.user) === -1) {
didLoadUser.push(post.user);
return getJSON('./users/' + post.user + '.json') ;
}
})
);
}).then(function(users) {
users.forEach(function(user) {
if (typeof user !== 'undefined') {
dataUsers[user.id] = user;
}
});
}).catch(function(err) {
// catch any error that happened so far
console.error(err.message);
}).then(function() {
// use the data
});
}
// unchanged from the mentioned Google article
function getJSON(url) {
return get(url).then(JSON.parse);
}
// unchanged from the mentioned Google article
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
The code itself worked fine before converting it into function that gets called in my components's componentWillMount function.
componentWillMount: function() {
this.setState({
data: loadData(this.props.params.thread)
})
}
I suspect the error lies within the function itself, since it looks like Promise.all gets resolved after loading the thread data and before loading any of the user data.
I only started using promises recently, so my knowledge is rather basic. What am I doing wrong? do I need to wrap the main function inside another promise?
this.setState({
data: loadData(this.props.params.thread)
})
You can't return from an asynchronous call like above as the data is simply not ready in time in a synchronous way. You return a promise, therefore the code should look like e.g.:
loadData(this.props.params.thread)
.then((data)=> {
this.setState({
data,
})
})
Note:
In order to get it work, you need to return the promise from the loadData, therefore:
...
return getJSON(..
...
I'm trying to unit test my app built on Angular with Jasmine via Karma. The app involves making a call to the GitHub API and pulling the names of all the repos of a user and filling an array with those names. I'm trying to test that the array is getting filled but I'm having some issues with $httpBackend.
The relevant parts of my controller are:
readmeSearch.controller('ReadMeSearchController', ['RepoSearch', function(RepoSearch) {
var self = this;
self.gitRepoNames = [];
self.doSearch = function() {
var namesPromise =
RepoSearch.query(self.username)
.then(function(repoResponse) {
addRepoNames(repoResponse);
}).catch(function(error) {
console.log('error: ' + error);
});
return namesPromise;
};
addRepoNames = function(response) {
self.repoSearchResult = response.data;
for(var i = 0; i < self.repoSearchResult.length; i++) {
var name = self.repoSearchResult[i]['name']
self.gitRepoNames.push(name);
};
};
}]);
My RepoSearch factory is:
readmeSearch.factory('RepoSearch', ['$http', function($http) {
return {
query: function(searchTerm) {
return $http({
url: 'https://api.github.com/users/' + searchTerm + '/repos',
method: 'GET',
params: {
'access_token': gitAccessToken
}
});
}
};
}]);
And the test in question is this:
describe('ReadMeSearchController', function() {
beforeEach(module('ReadMeter'));
var ctrl;
beforeEach(inject(function($controller) {
ctrl = $controller('ReadMeSearchController');
}));
describe('when searching for a user\'s repos', function() {
var httpBackend;
beforeEach(inject(function($httpBackend) {
httpBackend = $httpBackend
httpBackend
.expectGET('https://api.github.com/users/hello/repos?access_token=' + gitAccessToken)
.respond(
{ data: items }
);
}));
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
var items = [
{
"name": "test1"
},
{
"name": "test2"
}
];
it('adds search results to array of repo names', function() {
ctrl.username = 'hello';
ctrl.doSearch();
httpBackend.flush();
expect(ctrl.gitRepoNames).toEqual(["test1", "test2"]);
});
});
});
When I run the test I get the error
Expected [ ] to equal [ 'test1', 'test2' ].
So evidently this is because self.gitRepoNames is not being filled. When I console log ctrl.repoSearchResult just before the expectation in the test I get
Object{data: [Object{name: ...}, Object{name: ...}]}
Which is where the problem is I feel since self.repoSearchResult.length will be undefined when it is called in the for loop in the addRepoNames function.
So my question is why doesn't response.data return an array when it is called in the addRepoNames function in the test?
Any help would be greatly appreciated.
EDIT: I should mention that the app works fine when run on the server.
ctrl.doSearch is an asynchronous function. You should handle it in an async way. Try:
it('adds search results to array of repo names', function(done) {
ctrl.username = 'hello';
ctrl.doSearch().then(function() {
expect(ctrl.gitRepoNames).toEqual(["test1", "test2"]);
done();
});
httpBackend.flush();
});
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
I think I'm writing my promise incorrectly and I couldn't figure out why it is caching data. What happens is that let's say I'm logged in as scott. When application starts, it will connect to an endpoint to grab listing of device names and device mapping. It works fine at this moment.
When I logout and I don't refresh the browser and I log in as a different user, the device names that scott retrieved on the same browser tab, it is seen by the newly logged in user. However, I can see from my Chrome's network tab that the endpoint got called and it received the correct listing of device names.
So I thought of adding destroyDeviceListing function in my factory hoping I'll be able to clear the values. This function gets called during logout. However, it didn't help. Below is my factory
app.factory('DeviceFactory', ['$q','User', 'DeviceAPI', function($q, User, DeviceAPI) {
var deferredLoad = $q.defer();
var isLoaded = deferredLoad.promise;
var _deviceCollection = { deviceIds : undefined };
isLoaded.then(function(data) {
_deviceCollection.deviceIds = data;
return _deviceCollection;
});
return {
destroyDeviceListing : function() {
_deviceCollection.deviceIds = undefined;
deferredLoad.resolve(_deviceCollection.deviceIds);
},
getDeviceIdListing : function() {
return isLoaded;
},
getDeviceIdMapping : function(deviceIdsEndpoint) {
var deferred = $q.defer();
var userData = User.getUserData();
// REST endpoint call using Restangular library
RestAPI.setBaseUrl(deviceIdsEndpoint);
RestAPI.setDefaultRequestParams( { userresourceid : userData.resourceId, tokenresourceid : userData.tokenResourceId, token: userData.bearerToken });
RestAPI.one('devices').customGET('', { 'token' : userData.bearerToken })
.then(function(res) {
_deviceCollection.deviceIds = _.chain(res)
.filter(function(data) {
return data.devPrefix != 'iphone'
})
.map(function(item) {
return {
devPrefix : item.devPrefix,
name : item.attributes[item.devPrefix + '.dyn.prop.name'].toUpperCase(),
}
})
.value();
deferredLoad.resolve(_deviceCollection.deviceIds);
var deviceIdMapping = _.chain(_deviceCollection.deviceIds)
.groupBy('deviceId')
.value();
deferred.resolve(deviceIdMapping);
});
return deferred.promise;
}
}
}])
and below is an extract from my controller, shortened and cleaned version
.controller('DeviceController', ['DeviceFactory'], function(DeviceFactory) {
var deviceIdMappingLoader = DeviceFactory.getDeviceIdMapping('http://10.5.1.7/v1');
deviceIdMappingLoader.then(function(res) {
$scope.deviceIdMapping = res;
var deviceIdListingLoader = DeviceFactory.getDeviceIdListing();
deviceIdListingLoader.then(function(data) {
$scope.deviceIDCollection = data;
})
})
})
Well, you've only got a single var deferredLoad per your whole application. As a promise does represent only one single asynchronous result, the deferred can also be resolved only once. You would need to create a new deferred for each request - although you shouldn't need to create a deferred at all, you can just use the promise that you already have.
If you don't want any caching, you should not have global deferredLoad, isLoaded and _deviceCollection variables in your module. Just do
app.factory('DeviceFactory', ['$q','User', 'DeviceAPI', function($q, User, DeviceAPI) {
function getDevices(deviceIdsEndpoint) {
var userData = User.getUserData();
// REST endpoint call using Restangular library
RestAPI.setBaseUrl(deviceIdsEndpoint);
RestAPI.setDefaultRequestParams( { userresourceid : userData.resourceId, tokenresourceid : userData.tokenResourceId, token: userData.bearerToken });
return RestAPI.one('devices').customGET('', { 'token' : userData.bearerToken })
.then(function(res) {
return _.chain(res)
.filter(function(data) {
return data.devPrefix != 'iphone'
})
.map(function(item) {
return {
devPrefix : item.devPrefix,
name : item.attributes[item.devPrefix + '.dyn.prop.name'].toUpperCase(),
};
})
.value();
});
}
return {
destroyDeviceListing : function() {
// no caching - nothing there to be destroyed
},
getDeviceIdListing : function(deviceIdsEndpoint) {
return getDevices(deviceIdsEndpoint)
.then(function(data) {
return { deviceIds: data };
});
},
getDeviceIdMapping : function(deviceIdsEndpoint) {
return this.getDeviceIdListing(deviceIdsEndpoint)
.then(function(deviceIds) {
return _.chain(deviceIds)
.groupBy('deviceId')
.value();
});
}
};
}])
Now, to add caching you'd just create a global promise variable and store the promise there once the request is created:
var deviceCollectionPromise = null;
…
return {
destroyDeviceListing : function() {
// if nothing is cached:
if (!deviceCollectionPromise) return;
// the collection that is stored (or still fetched!)
deviceCollectionPromise.then(function(collection) {
// …is invalidated. Notice that mutating the result of a promise
// is a bad idea in general, but might be necessary here:
collection.deviceIds = undefined;
});
// empty the cache:
deviceCollectionPromise = null;
},
getDeviceIdListing : function(deviceIdsEndpoint) {
if (!deviceCollectionPromise)
deviceCollectionPromise = getDevices(deviceIdsEndpoint)
.then(function(data) {
return { deviceIds: data };
});
return deviceCollectionPromise;
},
…
};
I am having trouble with listsQuery not executing by the time everything gets sent to the browser. I know I need a Promise or something in there, but my attempts are so far unsuccessful. Help!
function processNavigation(navigation) {
var nav = [];
_.each(navigation, function(navItems) {
var navProperties = {
name: navItems.get("Name"),
longName: navItems.get("LongName"),
icon: navItems.get("Icon"),
url: navItems.get("Url"),
module: navItems.get("Module"),
runScript: navItems.get("RunScript"),
sortOrder: navItems.get("SortOrder")
};
switch (navItems.get("Module")) {
case "lists":
var listsQuery = new Parse.Query("ListItems"); // This should return back! But it's async? Needs promise?
listsQuery.ascending("SortOrder");
listsQuery.find().then(
function(results) {
var list = [];
_.each(results, function(listItems) {
var listProperties = {
name: listItems.get("Name"),
subName: listItems.get("Subname"),
sortOrder: listItems.get("SortOrder")
};
});
list.push(listProperties);
navProperties["source"] = list;
},
function() {
res.send('error');
}
);
break;
default:
navProperties["source"] = null;
break;
}
nav.push(navProperties);
});
res.send(nav);
}
This should give you something to go on, im not sure if it will work but it should show you the concept.
What you need to do is create an array of promises as it looks like your performing a query for each item in an array, and because the queries take some time your response is sent before the queries are complete. You then evaluate the array of promises and send your response back only when they are resolved.
I would suggest you split your logic into a few more functions as it's a little hard to follow.
Take a look at the parallel promises section
function processNavigation(navigation) {
var nav = [];
var promises = []
_.each(navigation, function(navItems) {
var navProperties = {
name: navItems.get("Name"),
longName: navItems.get("LongName"),
icon: navItems.get("Icon"),
url: navItems.get("Url"),
module: navItems.get("Module"),
runScript: navItems.get("RunScript"),
sortOrder: navItems.get("SortOrder")
};
switch (navItems.get("Module")) {
case "lists":
promises.push((function(navProperties){
var listsQuery = new Parse.Query("ListItems"); // This should return back! But it's async? Needs promise?
listsQuery.ascending("SortOrder");
listsQuery.find().then(
function(results) {
var list = [];
_.each(results, function(listItems) {
var listProperties = {
name: listItems.get("Name"),
subName: listItems.get("Subname"),
sortOrder: listItems.get("SortOrder")
};
});
list.push(listProperties);
navProperties["source"] = list;
promise.resolve();
},
function() {
promise.reject();
res.send('error');
}
);
})(navProperties))
break;
default:
navProperties["source"] = null;
break;
}
nav.push(navProperties);
});
Parse.Promise.when(promises).then(function(){
res.send(nav);
})
}