I have an Angularjs project that uses Restangular to access the database. I have three layers of data (say mydata, mysubdata, mysubsubdata) and there is a one-to-many relationship between each layer. My problem is that, for my display, I need to concatenate the mysubsubdata to the mysubdata. When I try to get data back from the database, I am hitting a complaint in the compiler that says I can't have a function in a loop. Here is what I am trying to do:
DataService.one(mydata.id).getList('mysubdata')
.then(function(data) {
var dataList = data;
for (returnedData in dataList) {
DataService.one(mydata.id).one('mysubdata',returnedData.id).getList('mysubsubdata')
.then(returnedSubData) {
dataList = angular.extend(dataList, returnedSubData);
});
}
});
All the examples I've found have loops inside the .then function or are trying to get a bunch of promises back first. I don't think those apply. I'm still pretty new to Angular, so I may be flailing a bit. Not sure about the extend either, but that's likely a separate question.
Edit: I suspect this should be done with a $q.all but haven't grasped the method yet.
Adding a then() method inside your loop won't work because of the simple reason that loop does not wait for the promises to be resolved. You can achieve this using recursive method.
var myCustomData = null,
dataList = null,
dataListCounter = 0;
DataService.one(mydata.id).getList('mysubdata')
.then(function (data){
dataList = data;
myCustomData = mydata;
$scope.getSubSubData();
});
$scope.getSubSubData = function () {
if (dataList.length >= dataListCounter)
return;
DataService.one(myCustomData.id).one('mysubdata',dataList[dataListCounter].id).getList('mysubsubdata')
.then(function (returnedSubData) {
dataList = angular.extend(dataList, returnedSubData);
dataListCounter++;
$scope.getSubSubData();
});
};
Please let me know if this helps!
Minor corrections to #Anadi Sharma's response.
$scope.getSubSubData = function () {
if (dataList.length == dataListCounter)
return;
DataService.one(myCustomData.id).one('mysubdata',dataList[dataListCounter].id).getList('mysubsubdata')
.then(function (returnedSubData) {
dataList[dataListCounter].mysubsubdata = returnedSubData;
dataListCounter++;
$scope.getSubSubData();
});
};
Note that I then use a filter when I display the data to concatenate the subsubdata values.
Related
I have a function that calls another function which then does two things:
It does an http get to grab a list of IDs
It then loops through that list of IDs, makes another http get for each ID, and adds the result to the var 'dataList'.
I need to have it return the fully populated dataList as the result, then I can take that list and do something with it. I know I need to use promises for this but I'm having trouble getting the behavior I'm looking for. My latest attempt is below. This does return the expected list, but then it seems to get stuck in a loop - I think it's doing a return for every iteration of the map loop. Any suggestions would be appreciated.
getDataList(searchString, matchCase, rows, start).then(function(result) {
// Do something with result
});
getDataList: function(searchString, matchCase, rows, start) {
let body = {};
let dataList = [];
var url = getUrl();
var defer = $q.defer();
return $http.get(url).success(function(response) {
let dataIdList = [];
body = response.response.docs;
body.map(function getDataId(wfr) {
if (wfr.referenceType === 'data') {
dataIdList.push(wfr.referenceId);
}
});
dataList = dataIdList.map(function getData(dataId) {
dataSvc.getDataDetails(dataId).then(function(response) {
dataList.push(response);
});
});
defer.resolve(dataList);
}).error(function(result) {
defer.reject();
});
}
The .success and .error methods have been removed from the AngularJS framework.1 Avoid using the deferred anti-pattern.2 Use $q.all to resolve multiple AngularJS promises:
getDataList: function(searchString, matchCase, rows, start) {
var url = getUrl();
return $http.get(url).then(function(response) {
let body = response.data.response.docs;
let dataIdList =
body.filter(wfr => wfr.referenceType === 'data').map(_ => _.referenceId);
let dataListPromiseArr = dataIdList.map(dataId => {
return dataSvc.getDataDetails(dataId).then(function(response) {
return response.data;
});
});
return $q.all(dataListPromiseArr);
}).catch(function(response) {
console.log(response);
throw response;
});
}
For more information, see
AngularJS $q Service API Reference - $q.all
I think Promise.all() is what you are looking for.
You can push all the getDataDetails(dataId) requests as promises to an array, then do Promise.all(yourPromiseArray) and return what you want only after Promise.all has successfully finished.
I'd highly recommend looking into async/await during your research, as it could be quite helpful. Plenty of articles on the internet on how you can implement it, and many many StackOverflow questions to look through if you feel like you're stuck with it.
Hope this helps, good luck! :)
Lately I've been stuck with a problem that I don't know how to solve. I asked this question and after some efforts we've found that Firebase works differently with promises than normal requests, and I couldn't use them properly.
As explained in the question, I'm filling an array with some informations from Firebase, and I need to call another method when I'm sure the array is filled, in other words when I'm sure the call to Firebase has finished.
This is my code as I'm using it now:
var user_pref = firebase.database().ref('/users/'+ self.api.email_id+'/preferenze/');
var ref = firebase.database().ref('/tag/')
var userTags = [];
var self1 = self;
user_pref.once('value', function(preferenze) {
preferenze.forEach(function(t) {
ref.once('value', function(tags) {
tags.forEach(function(t1) {
if (t.key == t1.key) {
console.log("Found " + t1.key)
userTags.push(t1.key)
}
return false;
});
})
return false;
});
}).then(a =>{
await this.sleep(1000) //----> WORKAROUND
console.log("Out")
this.myTags = userTags
this.findPoiByTag(this.myTags) //method I have to call when finished
})
I'm using this orrible workaround with sleep to be sure the code outside is executed after the one inside. Without that, it prints "Out" before and then all the "Found" in the loop. I've tried using it with promises in every way, but it still doesn't work. Having a look at the docs here I couldn't find anything that would help me.
That's indeed pretty bad.
This should be closer to what you need:
var userTags = [];
var self1 = self;
user_pref.once('value', function(preferenze) {
var promises = [];
preferenze.forEach(function(t) {
promises.push(ref.child(t.key).once('value'));
});
Promise.all(promises).then(function(snapshots) {
snapshots.forEach(function(snapshot) {
if (snapshot.exists()) {
userTags.push(snapshot.key);
}
});
})
this.myTags = userTags
this.findPoiByTag(this.myTags) //method I have to call when finished
});
What this does differently:
It loads each preference key with a direct look (removing the need for a deeply nested loop that was loading way too much data).
It puts all load of the categories into an array of promises.
It then calls your function after all promises have resolved.
In a controller function, I make some operations:
Get a list of organizations with a promise
In the then of this promise, I loop through each of them to extract some data and populate some of my controller attributes.
One of this operation is to call another promise to gather all users attached to this organization, with a loop inside of it to extract name and other stuff.
When I get ALL of it, so every organization has been parsed, and within them all users too, I must call a function to update my view.
I got it working by setting some flags (orgParsed and usersParsed) but I find it to be... a code shame.
I heard about a way of maybe doing this by using $q to wait for the two promises and maybe loops inside their "then" to be resolve before calling my view function. But I struggle applying this code change since the second promise use the result of the first to gather the organization ID.
Here is my current code:
this.getOrgData = function () {
return Service.getList().then(function (result) {
var orgCount = result.Objects.length;
var orgParsed = 0;
_.forEach(result.Objects, function (org) {
org.Users = [];
// Some logic here using 'org' data
Service.getUsers(org.Id, 0, 0).then(function (userResult) {
usersParsed = 0;
_.forEach(userResult.Objects, function (user) {
// Some Logic here using 'user.Name'
usersParsed++;
});
orgParsed++;
if (orgParsed === orgCount && usersParsed === userResult.Objects.length) {
self.sortMenuList(); // My view Function
}
});
});
$scope.$broadcast("getOrgData");
});
};
Do you see any way to trigger my self.sortMenuList() function only when I can be sure I got all users of every companies parsed in more elegant/efficient/safe way?
Yes, that counting should definitely be replaced by $q.all, especially as you did not bother to handle any errors.
this.getOrgData = function () {
return Service.getList().then(function (result) {
$scope.$broadcast("getOrgData"); // not sure whether you want that here before the results from the loop
return $q.all(_.map(result.Objects, function (org) {
org.Users = [];
// Some logic here using 'org' data
return Service.getUsers(org.Id, 0, 0).then(function (userResult) {
_.forEach(userResult.Objects, function (user) {
// Some Logic here using 'user.Name'
});
});
}));
}).then(function() {
self.sortMenuList(); // My view Function;
})
};
The problem you describe sounds like you want to wait until a certain amount of promises are all resolved, and then do something with the result. That's really easy when you use Promise.all():
this.getOrgData = function () {
return Service.getList().then(function (result) {
var promises = [];
_.forEach(result.Objects, function (org) {
org.Users = [];
// Some logic here using 'org' data
// Store the promise for this user in the promises array
promises.push(Service.getUsers(org.Id, 0, 0));
});
// userResults is an array of all the results of the promises, in the same order as the getUsers was called
Promise.all(promises).then(function (userResults) {
_.forEach(userResults, function(userResult) {
_.forEach(userResult.Objects, function (user) {
// Some Logic here using 'user.Name'
});
});
self.sortMenuList();
});
$scope.$broadcast("getOrgData");
});
};
I'm using the axios promise library, but my question applies more generally I think. Right now I'm looping over some data and making a single REST call per iteration.
As each call completes I need to add the return value to an object. At a high level, it looks like this:
var mainObject = {};
myArrayOfData.forEach(function(singleElement){
myUrl = singleElement.webAddress;
axios.get(myUrl)
.then(function(response) {
mainObject[response.identifier] = response.value;
});
});
console.log(convertToStringValue(mainObject));
What's happening of course is when I call console.log the mainObject doesn't have any data in it yet, since axios is still reaching out. What's a good way of dealing with this situation?
Axios does have an all method along with a sister spread one, but they appear to be of use if you know ahead of time how many calls you'll be making, whereas in my case I don't know how many loop iterations there will be.
You need to collect all of your promises in an array and then use Promise.all:
// Example of gathering latest Stack Exchange questions across multiple sites
// Helpers for example
const apiUrl = 'https://api.stackexchange.com/2.2/questions?pagesize=1&order=desc&sort=activity&site=',
sites = ['stackoverflow', 'ubuntu', 'superuser'],
myArrayOfData = sites.map(function (site) {
return {webAddress: apiUrl + site};
});
function convertToStringValue(obj) {
return JSON.stringify(obj, null, '\t');
}
// Original question code
let mainObject = {},
promises = [];
myArrayOfData.forEach(function (singleElement) {
const myUrl = singleElement.webAddress;
promises.push(axios.get(myUrl));
});
Promise.all(promises).then(function (results) {
results.forEach(function (response) {
const question = response.data.items[0];
mainObject[question.question_id] = {
title: question.title,
link: question.link
};
});
console.log(convertToStringValue(mainObject));
});
<script src="https://unpkg.com/axios#0.19.2/dist/axios.min.js"></script>
It's described in axios docs (Performing multiple concurrent requests section).
Before May 2020 it was possible to do with axios.all(), which is now deprecated.
Totally new to OOP in javascript, but Im trying and reading all I can.
Ive created a simple test javascript class called Invoices. Invoices only has two methods. One method fires the other. And this part seems to be working fine.
My problem lies with getting data objects from one method to another. I put a alert in the first method, (from my understanding) this alert should show data returned from the second method... but its not.
Any help would be greatly appreciated. Oh.. and I am using jquery as well.
Here is my codez.
function Invoices()
{
this.siteURL = "http://example.com/";
this.controllerURL = "http://example.com/invoices/";
this.invoiceID = $('input[name="invoiceId"]').val();
}
invoice = new Invoices;
Invoices.prototype.initAdd = function()
{
//load customers json obj
this.customersJSON = invoice.loadCustomers();
alert(this.customersJSON);
//create dropdown
}
Invoices.prototype.loadCustomers = function ()
{
$.post(this.controllerURL + "load_customers"),
function(data)
{
return data;
}
}
There are two problems with that. First of all, $.post is asynchronous; you'll have to adopt a callback scheme or use $.ajax to make it synchronous. Secondly, you probably meant to do this:
$.post(this.controllerURL + "load_customers", function(data) {
return data;
});
Note how the closure is in the parentheses of the function call.
As you were told the AJAX call is asynchronous, you would have to implement your initAdd in 2 step:
BeginInitAdd which would initiate the AJAX call
EndInitAdd which would be the callback for your AJAX call and perform the action depending on the data returned.
Invoices.prototype.initAdd = function()
{
//load customers json obj
this.xhrObj = invoice.loadCustomers();
alert(this.customersJSON);
}
Invoices.prototype.createDropdown = function (data) {
//create dropdown
this.customersJSON=data
}
Invoices.prototype.loadCustomers = function ()
{
return $.post(this.controllerURL + "load_customers"),
function(data)
{
//return data;
this.createDropdown(data)
}
}