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(..
...
Related
I am using Angular resourse to get my data from an API, in this way:
var getAccountListPerUser = function () {
return $resource(uri, {}, {
get: {
headers: service.getDefaultHeaderRequest(),
method: 'GET',
transformResponse: function (data) {
var accountList = [];
try {
accountList = JSON.parse(data);
} catch (e) {
accountList = [];
}
return accountList;
},
isArray: true,
cache: true
}
}).get().$promise;
};
In my controller I have to use it and another two service functions defined in the same way.
var promiseResourcesAccountList = usrWebUserService.getAccountListPerUser();
promiseResourcesAccountList.then(function(result){
$scope.usersWithAccountsAndProfiles = result;
var filteredProfiles = [];
for (var account in result) {
...
}
$scope.filteredProfiles = filteredProfiles;
});
And:
var promiseResourcesEditUser = usrWebUserService.getResourcesUser(currentUser);
promiseResourcesEditUser.then(function (result) {
usrWebUserFactory.mapBasicPreferences($scope, result);
});
And then another very similar, this information loads data in three divs, but I want to show them only when all the three functions have completed correctly. I think I have to chain the result of the promises. How can I do that?
You can chain them like:
promiseResourcesAccountList.then(function(result){
///whatever processing
//return a promise
return promiseResourcesEditUser()
}).then(function(){
return anotherPromise();
}).then(function(){
//update scope here
});
alternatively, you could also use $q.all([promise1, promise2, promise3]).then(...);
#terpinmd is correct. Chaining promises is pretty simple. Say you have a service with a "getWidgets" that returns a promise, and you want to use the response from that service to call another service, "getWidgetOwners" that will return another promise :
Assumptions
getWidgets returns an array of widget objects.
getWidgetOwners accepts an array of ownerIds
How To:
service.getWidgets()
.then(function(widgets) {
return widgets.map(function(widget) { // extract ownerIds
return widget.ownerId;
});
})
.then(service.getWidgetOwners) // pass array of ownerId's to
.then(function(owners) { // the next service
console.log(owners);
});
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
}
]
}
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 trying to insert a documents into collections which are all related to each other: Posts, Comments, and Categories. Each document in Comments and Categories must have a PostId field.
I have created a method named insertSamplePost, which should return the id of the post after inserting a document into Posts. I have assigned this method call to a variable like so:
var postId = Meteor.call('insertSamplePost', samplePost, function(error, id) {
if (error) {
console.log(error);
} else {
return id;
}
});
However, when I try to use postId later to insert related comments and categories, it appears to be undefined! Does anyone know what is happening?
Here is my full code:
if (Meteor.isClient) {
Template.post.events({
'click .new-sample-post' : function (e) {
var samplePost = {
title: "This is a title",
description: "This is a description"
};
// Insert image stub
var postId = Meteor.call('insertSamplePost', samplePost, function(error, id) {
if (error) {
console.log(error);
} else {
return id;
}
});
// This returned undefined. :-()
console.log(postId);
var sampleComment = {
body: "This is a comment",
postId: postId
};
var sampleCategory = {
tag: "Sample Category",
postId: postId
};
Comments.insert(sampleComment);
Categories.insert(sampleCategory);
}
});
}
// Collections
Posts = new Meteor.Collection('posts');
Comments = new Meteor.Collection('comments');
Categories = new Meteor.Collection('categories');
// Methods
Meteor.methods({
insertSamplePost: function(postAttributes) {
var post = _.extend(postAttributes, {
userId: "John Doe",
submitted: new Date().getTime()
});
return Posts.insert(post);
}
});
When you do:
var myVar = Meteor.call("methodName", methodArg, function(error, result) {
return result;
}
Your myVar variable will actually be whatever Meteor.call() returns, not what your callback function returns. Instead, what you can do is:
var postId;
Meteor.call('insertSamplePost', samplePost, function(error, id) {
if (error) {
console.log(error);
} else {
postId = id;
}
});
However, as Akshat mentions, by the time the callback function actually runs and asynchronously sets the postId, your insert calls on the other collections will already have run.
This code would actually be a little simpler if you avoid the server method altogether - you can modify the document in your collection's allow callback:
Template.post.events({
'click .new-sample-post' : function (e) {
var samplePost = {
title: "This is a title",
description: "This is a description"
};
var postId = Posts.insert(samplePost);
var sampleComment = {
body: "This is a comment",
postId: postId
};
var sampleCategory = {
tag: "Sample Category",
postId: postId
};
Comments.insert(sampleComment);
Categories.insert(sampleCategory);
}
});
Now you can add the userId and submitted fields in your Posts.allow() callback:
Posts.allow({
insert: function(userId, doc) {
doc.userId = userId;
doc.submitted = new Date().getTime();
return true;
}
});
If you wanted, you can still do the two secondary inserts within the callback for your first insert, in order to make the operation more atomic (in other words, to make sure the secondary inserts don't happen if the first insert fails).
You can use Session to store the results since Session is reactive and client side javascript is asynchronous so you cant assign the result to a variable directly using return.
So the reason you get undefined is because the result of Meteor.call is given in the callback. The callback will yield a result much later and by the time it returns a result the rest of your code will already have run. This is why its a good idea to use Session because you can use it in a template helper.
However for inserting a post its better to just insert the comments and category in the callback itself since you're not displaying the result in the html.
Meteor.call('insertSamplePost', samplePost, function(error, postId) {
if (error) {
console.log(error);
} else {
var sampleComment = {
body: "This is a comment",
postId: postId
};
var sampleCategory = {
tag: "Sample Category",
postId: postId
};
Comments.insert(sampleComment);
Categories.insert(sampleCategory);
}
});
This way if the result is an error the comment and category wont be inserted.
I've got the following.
var lookupInit = function () {
http.get('api/employmenttype', null, false)
.done(function (response) {
console.log('loaded: employmenttype');
vm.lookups.allEmploymentTypes(response);
});
http.get('api/actionlist', null, false)
.done(function (response) {
console.log('loaded: actionlist');
vm.lookups.allActionListOptions(response);
});
http.get('api/company', null, false)
.done(function (response) {
console.log('loaded: company');
vm.lookups.allCompanies(response);
});
//... x 5 more
return true;
};
// somewhere else
if (lookupInit(id)) {
vm.userInfo.BusinessUnitID('0');
vm.userInfo.BuildingCode('0');
if (id === undefined) {
console.log('api/adimport: latest');
http.json('api/adimport', { by: "latest" }, false).done(viewInit);
}
else if (id !== undefined) {
console.log('api/adimport: transaction');
http.json('api/adimport', { by: "transaction", TransactionId: id }, false).done(viewInit);
}
} else {
console.log('User info init failed!');
}
The following "http.get('api/employmenttype', null, false)" means i set async to false.
I'm aware that this is probably inefficient. And i'd like to have all the calls load simultaneously.
The only problem is if i don't have them set to async false, the second part of my code might execute before the dropdowns are populated.
I've tried a couple of attempts with Jquery Deferreds, but they have resulted in what i can only describe as an abortion.
The only thing i'm looking to achieve is that the lookup calls finish before the adimport/second part of my code, in any order.... But having each call wait for the one before it to finish EG: async, seems like the only solution I'm capable of implementing decently ATM.
Would this be an appropriate place for deferred function, and could anyone point me into a direction where i could figure out how to implement it correctly, as I've never done this before?
You can use $.when to combine multiple promises to one that resolves when all of them have been fulfilled. If I got you correctly, you want
function lookupInit() {
return $.when(
http.get('api/employmenttype').done(function (response) {
console.log('loaded: employmenttype');
vm.lookups.allEmploymentTypes(response);
}),
http.get('api/actionlist').done(function (response) {
console.log('loaded: actionlist');
vm.lookups.allActionListOptions(response);
}),
http.get('api/company').done(function (response) {
console.log('loaded: company');
vm.lookups.allCompanies(response);
}),
// … some more
);
}
Then somewhere else
lookupInit(id).then(function(/* all responses if you needed them */) {
vm.userInfo.BusinessUnitID('0');
vm.userInfo.BuildingCode('0');
if (id === undefined) {
console.log('api/adimport: latest');
return http.json('api/adimport', {by:"latest"})
} else {
console.log('api/adimport: transaction');
return http.json('api/adimport', {by:"transaction", TransactionId:id});
}
}, function(err) {
console.log('User info init failed!');
}).done(viewInit);
In the Jquery API I've found this about resolving multiple deferreds:
$.when($.ajax("/page1.php"), $.ajax("/page2.php")).done(function(a1, a2){
/* a1 and a2 are arguments resolved for the
page1 and page2 ajax requests, respectively.
each argument is an array with the following
structure: [ data, statusText, jqXHR ] */
var data = a1[0] + a2[0]; /* a1[0] = "Whip", a2[0] = " It" */
if ( /Whip It/.test(data) ) {
alert("We got what we came for!");
}
});
Using this with your code:
var defer = $.when(
$.get('api/employmenttype'),
$.get('api/actionlist'),
$.get('api/company'),
// ... 5 more
);
defer.done(function (arg1, arg2, arg3 /*, ... 5 more*/) {
vm.lookups.allEmploymentTypes(arg1[0]);
vm.lookups.allEmploymentTypes(arg2[0]);
vm.lookups.allEmploymentTypes(arg3[0]);
// .. 5 more
vm.userInfo.BusinessUnitID('0');
vm.userInfo.BuildingCode('0');
if (id === undefined) {
console.log('api/adimport: latest');
http.json('api/adimport', { by: "latest" }, false).done(viewInit);
} else if (id !== undefined) {
console.log('api/adimport: transaction');
http.json('api/adimport', { by: "transaction", TransactionId: id }, false).done(viewInit);
}
});
You can use the defer of the $.when() inside an other $.when(), so if the json calls are not dependant on the first calls you can add them in a an onther defer.