Creating a promise conditionally in AngularJS - javascript

I have an items array that I would be getting from localStorage.
var items = JSON.parse($window.localStorage.selectedResources)['server'];
var arr = [];
var idsArray = [];
angular.forEach(items, function (item) {
idsArray.push(item.id);
});
Then I fire an API call ...
//Make the API call
ds.getBillInfo(idsArray)
.then(function(response){
var serversList = [];
for (var key in response) {
// iterate over response
The problem is if the items array is empty, so does the idsArray.
Then the error says Cannot read property 'then' of undefined.
What I want to do is even if the idsArray is empty , I want lines to execute inside the then block thinking as there is no promise.
How can I do that ?
EDIT
If I do $q.all([ds.getBillInfo(idsArray)]) then there is no error.
The getBillInfo() looks like:
this.getBillInfo = function(idsArray){
if(!idsArray.length) return;
var segmentUrl = '';
for(var i =0;i<idsArray.length;i++){
if(i != (idsArray.length-1))
segmentUrl += 'ids='+idsArray[i]+'&';
else
segmentUrl += 'ids='+idsArray[i];
}
return HttpWrapper.send('/api/bill?bill=t&'+segmentUrl, {"operation": 'GET'});
};

In getBillInfo wrap your logic with new Promise and on empty array resolve it.
Something like:
self.getBillInfo = function(array){
var deferred = $q.defer();
if(array.length == 0){
deferred.resolve([]); // return empty list
}
else{
var segmentUrl = '';
for(var i =0;i<idsArray.length;i++){
if(i != (idsArray.length-1))
segmentUrl += 'ids='+idsArray[i]+'&';
else
segmentUrl += 'ids='+idsArray[i];
}
HttpWrapper.send('/api/bill?bill=t&'+segmentUrl, {"operation": 'GET'})
.then(function (response) {
deferred.resolve(response.data);
}
, function (error) {
deferred.reject(error);
});
}
return deferred.promise;
}
[EDIT]
Regards to #JC Ford point, since HttpWrapper returns Promise we can write above logic with different way as:
self.getBillInfo = function(array){
if(array.length == 0){
return $q.resolve([]); // return empty list;
}
else{
var segmentUrl = '';
for(var i =0;i<idsArray.length;i++){
if(i != (idsArray.length-1))
segmentUrl += 'ids='+idsArray[i]+'&';
else
segmentUrl += 'ids='+idsArray[i];
}
return HttpWrapper.send('/api/bill?bill=t&'+segmentUrl, {"operation": 'GET'});
}
}

inject the $q service so that it is accessible in getBillInfo(). You can then wrap a value in $q.resolve() to make a promise that returns that value. (That value could even be another promise.) So if your getBillInfo() function sometimes needs to return early without a value, just return an empty $q.resolve() instead to ensure you're always returning a promise.
this.getBillInfo = function(idsArray){
//This returns undefined and causes your error.
if(!idsArray.length) return;
//This returns a promise that resolves immediately and executes your .then() handler.
if(!idsArray.length) return $q.resolve();
//This returns a promise that rejects immediately and executes your .catch() handler
if(!idsArray.length) return $q.reject();
var segmentUrl = '';
for(var i =0;i<idsArray.length;i++){
if(i != (idsArray.length-1))
segmentUrl += 'ids='+idsArray[i]+'&';
else
segmentUrl += 'ids='+idsArray[i];
}
return HttpWrapper.send('/api/bill?bill=t&'+segmentUrl, {"operation": 'GET'});
};

Related

Wait an event to resolve a promise

I am using a node.js module that has a method without callbacks. Instead of it, has an event that fires when that method has finished. I want resolve a promise, using that event as callback securing me that method has been completed succesfully.
array.lenght on promise can be X. So, I need 'hear' X times the event to secure me that all methods has completed succesfully <-- This is not the problem, I am just telling you that I know this could happen
Event :
tf2.on('craftingComplete', function(recipe, itemsGained){
if(recipe == -1){
console.log('CRAFT FAILED')
}
else{
countOfCraft++;
console.log('Craft completed! Got a new Item #'+itemsGained);
}
})
Promise:
const craftWepsByClass = function(array, heroClass){
return new Promise(function (resolve, reject){
if(array.length < 2){
console.log('Done crafting weps of '+heroClass);
return resolve();
}
else{
for (var i = 0; i < array.length; i+=2) {
tf2.craft([array[i].id, array[i+1].id]); // <--- this is the module method witouth callback
}
return resolve(); // <---- I want resolve this, when all tf2.craft() has been completed. I need 'hear' event many times as array.length
}
})
}
At first lets promisify the crafting:
function craft(elem){
//do whatever
return Promise((resolve,reject) =>
tf2.on('craftingComplete', (recipe,itemsGained) =>
if( recipe !== -1 ){
resolve(recipe, itemsGained);
}else{
reject("unsuccessful");
}
})
);
}
So to craft multiples then, we map our array to promises and use Promise.all:
Promise.all( array.map( craft ) )
.then(_=>"all done!")
If the events will be fired in the same order as the respective craft() calls that caused them, you can use a queue:
var queue = []; // for the tf2 instance
function getNextTf2Event() {
return new Promise(resolve => {
queue.push(resolve);
});
}
tf2.on('craftingComplete', function(recipe, itemsGained) {
var resolve = queue.shift();
if (recipe == -1) {
resolve(Promise.reject(new Error('CRAFT FAILED')));
} else {
resolve(itemsGained);
}
});
function craftWepsByClass(array, heroClass) {
var promises = [];
for (var i = 1; i < array.length; i += 2) {
promises.push(getNextTf2Event().then(itemsGained => {
console.log('Craft completed! Got a new Item #'+itemsGained);
// return itemsGained;
}));
tf2.craft([array[i-1].id, array[i].id]);
}
return Promise.all(promises).then(allItemsGained => {
console.log('Done crafting weps of '+heroClass);
return …;
});
}
If you don't know anything about the order of the events, and there can be multiple concurrent calls to craftWepsByClass, you cannot avoid a global counter (i.e. one linked to the tf2 instance). The downside is that e.g. in two overlapping calls a = craftWepsByClass(…), b = craftWepsByClass() the a promise won't be resolved until all crafting of the second call is completed.
var waiting = []; // for the tf2 instance
var runningCraftings = 0;
tf2.on('craftingComplete', function(recipe, itemsGained) {
if (--runningCraftings == 0) {
for (var resolve of waiting) {
resolve();
}
waiting.length = 0;
}
if (recipe == -1) {
console.log('CRAFT FAILED')
} else {
console.log('Craft completed! Got a new Item #'+itemsGained);
}
});
function craftWepsByClass(array, heroClass) {
for (var i = 1; i < array.length; i += 2) {
runningCraftings++;
tf2.craft([array[i-1].id, array[i].id]);
}
return (runningCraftings == 0
? Promise.resolve()
: new Promise(resolve => {
waiting.push(resolve);
})
).then(() => {
console.log('Done crafting weps of '+heroClass);
});
}
Of course in both solutions you must be 100% certain that each call to craft() causes exactly one event.
You can look at the event-as-promise package. It convert events into Promise continuously until you are done with all the event processing.
When combined with async/await, you can write for-loop or while-loop easily with events. For example, we want to process data event until it return null.
const eventAsPromise = new EventAsPromise();
emitter.on('data', eventAsPromise.eventListener);
let done;
while (!done) {
const result = await eventAsPromise.upcoming();
// Some code to process the event result
process(result);
// Mark done when no more results
done = !result;
}
emitter.removeListener('data', eventAsPromise.eventListener);
If you are proficient with generator function, it may looks a bit simpler with this.
const eventAsPromise = new EventAsPromise();
emitter.on('data', eventAsPromise.eventListener);
for (let promise of eventAsPromise) {
const result = await promise;
// Some code to process the event result
process(result);
// Stop when no more results
if (!result) {
break;
}
}
emitter.removeListener('data', eventAsPromise.eventListener);
I need check if event 'craftingComplete' has fired many times as I
call tf2.craft. Doesnt matters any posible ID or if craft has failed.
I need to know if tf2.craft has finished and only why is checking
'craftingComplete' event
Given that we know i within for loop will be incremented i += 2 where i is less than .length of array, we can create a variable equal to that number before the for loop and compare i to the number within event handler
const craftWepsByClass = function(array, heroClass) {
return new Promise(function(resolve, reject) {
var countCraft = 0;
var j = 0;
var n = 0;
for (; n < array.length; n += 2);
tf2.on('craftingComplete', function(recipe, itemsGained) {
if (recipe == -1) {
console.log('CRAFT FAILED')
} else {
countOfCraft++;
console.log('Craft completed! Got a new Item #' + itemsGained);
if (j === n) {
resolve(["complete", craftCount])
}
}
})
if (array.length < 2) {
console.log('Done crafting weps of ' + heroClass);
return resolve();
} else {
try {
for (var i = 0; i < array.length; i += 2, j += 2) {
tf2.craft([array[i].id, array[i + 1].id]);
}
} catch (err) {
console.error("catch", err);
throw err
}
}
})
}
craftWepsByClass(array, heroClass)
.then(function(data) {
console.log(data[0], data[1])
})
.catch(function(err) {
console.error(".catch", err)
})

AngularJS promise after foreach

I have a problem with my promise after a foreach,
I would like to remove the object which has no parent from my database.
I read this post and I tried many things, for example this:
controller
project.getAll().then(function(results) {
return $scope.array_projects = results;
}).then(function(array_projects) {
$scope.results = [];
console.log(array_projects);
angular.forEach(array_projects, function(result) {
console.log(result);
var project = new Project(result);
var promise = project.getOne(result.ParentId);
$scope.result.push(promise);
promise.then(function(results) {
return results.length ? project:null;
});
// if(result.ParentId !== null) {
// console.log('ParentId');
// var promise = project.getOne(result.ParentId).then(function(results) {
// //console.log(results);
//
// if (results.length === 0){
// console.log('remove children');
// } else {
// $scope.results.push(project);
// console.log('parent exist, children added!');
//
// }
//
// });
//
// }
});
// after forEach
$q.all($scope.results).then(function(results){
console.log($scope.results);
// result is an array of mixed Project object or null value, just remove the null value and you're fine :)
var trueResults = [];
for(var i = 0; i < results.length; i++){
if(results[i]){
trueResults.push(results[i]);
}
}
// now use trueResults
console.log(trueResults);
})
});
model
var Project = function (properties) {
// Model
this.description = null;
this.file = null;
this.name = null;
this.ParentId = null;
this.path = null;
angular.extend(this, properties);
};
// With service
// ------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------
Project.prototype.getAll = function () {
return ProjectService.getAll();
};
Project.prototype.getOne = function (id) {
return ProjectService.getOne(id);
};
project.service.js
this.getAll = function (params) {
var projects = [];
return db.selectAll('projects').then(function(results) {
for(var i=0; i < results.rows.length; i++){
projects.push(results.rows.item(i));
}
return projects;
});
};
this.getOne = function (id) {
var project = [];
console.log(id);
return db.select('projects', {
"id": id
}).then(function(results) {
for(i=0; i < results.rows.length; i++){
project.push(results.rows.item(i));
}
return project;
})
};
But my console.log($scope.results); show '[]'
In my BDD I have :
id/id/ParentId/name/file/date
5 5 test crm-site.net /release.txt 2016-04-26 08:43:17
6 6 5 test2 crm-site.net /release.txt 2016-04-26 08:43:19
7 7 test3 crm-site.net /release.txt 2016-04-26 08:43:20
8 8 7 test4 crm-site.net /release.txt 2016-04-26 08:43:21
It seems like you are misunderstanding how to use $q.all.
Each time you call a project.getOne, you don't know when the result will come as it is asynchonious.
If you want to use the $q.all, you should push all the project.getOne in an array and $q.all(myArray). It is normal that your code only show [] because the .then aren't called yet and the push method haven't been called yet when you go through the console.
You wasn't so far of what you were trying just update your code with :
// in the foreach
if(result.ParentId){
var promise = project.getOne(result.ParentId);
$scope.result.push(promise);
promise.then(function(results) {
return results.length ? project:null;
}
}
// after forEach
$q.all($scope.results).then(function(results){
// result is an array of mixed Project object or null value, just remove the null value and you're fine :)
var trueResults = [];
for(var i = 0; i < results.length; i++){
if(results[i]){
trueResults.push(results[i]);
}
}
// now use trueResults
})
The explanation is that $q.all take an array of promise not objects, he can't wait that you fill an array, because he can't know what criteria makes you're array "ready" unlike a promise where the resolve will be called.

Angularjs promise not resolving in time

I am trying to retrieve data for use in a service that can be used throughout my app. The problem is that I can't get the data to be resolved in time for the routine that uses it. Here is the routine that uses it:
function getTranslation(lookup, language) {
var i = 0;
if (vm.translations == null) {
//vm.translations = getAllTranslations();
dataService.getTranslations()
.then(function (data) {
vm.translations = data;
});
}
var len = vm.translations.length;
for (var i=0; i < len; i++) {
if (vm.translations[i].labelName == lookup) {
if (language == "English") {
return vm.translations[i].english;
} else {
if (language == "Spanish") {
return vm.translations[i].espanol;
}
}
}
}
return null;
}
Here is the calling method within that service:
function getAllTranslations() {
return vm.translations = dataService.getTranslations()
.then(function (data) {
vm.translations = data;
return vm.translations;
});
}
And here is the method in the dataService:
function getTranslations() {
return $http.get('/api/labeltext')
.then (getTranslationComplete)
.catch(getTranslationFailed);
function getTranslationComplete(response) {
var deferred = $q.defer();
return response.data;
}
function getTranslationFailed(error) {
alert("XHR failed for frequent pawner report: " + error.responseText);
}
}
I am still learning angularjs and want to be able to populate the data in the service and then call it from other controllers. However, when I get to my for loop the array is empty and only gets populated after its completed.
That is because the promise will not be resolved before your for loop fires. By placing the loop within the .then(), you will have access to the response and your loop values will be defined. This is not DRY since there would be code duplication if you add an else to the function and would have to add in the same loop code. For that, I would refactor the loop into an external function and just call it from within the proper areas of getTranslation().
function getTranslation(lookup, language) {
var i = 0;
if (vm.translations == null) {
//vm.translations = getAllTranslations();
dataService.getTranslations()
.then(function (data) {
vm.translations = data;
var len = vm.translations.length;
for (var i=0; i < len; i++) {
if (vm.translations[i].labelName == lookup) {
if (language == "English") {
return vm.translations[i].english;
} else {
if (language == "Spanish") {
return vm.translations[i].espanol;
}
}
}
}
});
}
return null;
}
Since your data originates from a promise ($http), all of your subsequent code that needs to access that data has to be within a then function.
angular.controller('myController', function(dataService) {
var translationsPromise;
/**
* Caches translations from /api/labeltext and performs a lookup
* #param lookup
* #param language
*/
function getTranslation(lookup, language) {
if (translationsPromise == null) {
translationsPromise = dataService.getTranslations()
}
translationsPromise.then(function(data) {
vm.translations = data;
var len = vm.translations.length;
for (var i = 0; i < len; i++) {
if (vm.translations[i].labelName == lookup) {
if (language == "English") {
return vm.translations[i].english;
} else {
if (language == "Spanish") {
return vm.translations[i].espanol;
}
}
}
}
});
}
getTranslation('Some Label', 'English').then(function(translation) {
// The translation that was found is accessible in this block
});
});
When data is asynchronously derived, it's always better to cache a promise rather than the data. Thus, should another request for the same data be made before the original promise is resolved, then that promise can be retrieved from cache, and returned or otherwise exploited. The same is not true of cached async data, which is not guaranteed to have arrived when another request is made.
function getTranslation(lookup, language) {
if (!vm.translationsPromise) {
vm.translationsPromise = dataService.getTranslations();
}
return vm.translationsPromise.then(function (data) {
var lang = {'English':'english', 'Spanish':'espanol'};
return data.reduce(function(str, item) {
return (str !== '') ? str : (item.labelName == lookup) ? item[lang[language]] : str;
}, '');
});
}

How to fix my promise return here for this function?

I have an array of tags which may contain up to 3 items.
Next I pass tags into my getTagQuotes function and am trying to set a promise variable to it. Right now I'm getting promise is undefined.
// If there are tags, the wait for promise here:
if (tags.length > 0) {
var promise = getTagQuotes(tags).then(function() {
console.log('promise =',promise);
for (var i=0; i<tweetArrayObjsContainer.length; i++) {
chartObj.chartData.push(tweetArrayObjsContainer[i]);
}
chartDirective = ScopeFactory.getScope('chart');
chartDirective.nvd3.drawChart(chartObj.chartData);
});
}
else {
chartDirective = ScopeFactory.getScope('chart');
chartDirective.nvd3.drawChart(chartObj.chartData);
}
^ The goal above is to make sure that arrays in tweetArrayObjsContainer have been filled and pushed into chartObj.chartData before I draw my nvd3 chart.
Below is my getTagQuotes function which does another loop through the tags (up to 3) and calls another service GetTweetVolFactory.returnTweetVol which makes the actual API call to retrieve data.
I have code which checks if the we're on the final loop step, then calls the formatTagData function.
Inside formatTagData is where I fill the tweetArrayObjsContainer.
// Check for and GET tag data:
////////////////////////////////////////////////////////////////////
function getTagQuotes(tags) {
console.log('getTagQuotes called with',tags);
var deferred = $q.defer(); // <- setting $q.defer here
var url = 'app/api/social/twitter/volume/';
for (var i=0; i<tags.length; i++) {
var loopStep = i;
rawTagData = [];
GetTweetVolFactory.returnTweetVol(url+tags[i].term_id)
.success(function(data, status, headers, config) {
rawTagData.push(data);
if (loopStep === (rawTagData.length - 1)) {
// formatTagData fills the tweetArrayObjsContainer with data:
formatTagData(rawTagData);
deferred.resolve(); // <-- last step, so resolve
return deferred.promise;
}
})
.error(function(data, status) {
return 'error in returning tweet data';
});
}
function formatTagData(rawData) {
tweetArrayObjsContainer = [];
for (var i=0; i<rawData.length; i++) {
var data_array = [];
var loopNum = i;
_(rawData[i].frequency_counts).reverse().value();
for (var j=0; j<rawData[loopNum].frequency_counts.length; j++) {
var data_obj = {};
rawData[loopNum].frequency_counts[j].start_epoch = addZeroes(rawData[loopNum].frequency_counts[j].start_epoch);
data_obj.x = rawData[loopNum].frequency_counts[j].start_epoch;
data_obj.y = rawData[loopNum].frequency_counts[j].tweets;
data_array.push(data_obj);
}
var tweetArrayObj = {
"key" : "Quantity"+(i+1),
"type" : "area",
"yAxis" : 1,
"color" : tagColorArray[i],
"values" : data_array
};
tweetArrayObjsContainer.push(tweetArrayObj);
}
}
}
Your getTagQuotes function should return a promise with the $q service, check it out.
It should look like this:
function getTagQuotes(tags) {
var deferred = $q.defer();
(...)
// When the action is finished here, call resolve:
deferred.resolve();
(...)
// And in the end, return the promise
return deferred.promise;
}
And finally, in your main code, you'll do:
var promise = getTagQuotes(tags).then(function(){
console.log('promise =',promise);
// Fill chartObj with tweet data arrays
(...)
});
This function has no return statement.
function getTagQuotes(tags)

How to pass a promise to a controller in AngularJS

I'm having trouble understanding promises. I have a function $scope.startJob that when called, it calls areThereAvailableSegments in the SegmentsService. The idea is that areThereAvailableSegments should return true or false, so that I can handle this behavior in the controller, but not sure how to do this.
areThereAvailableSegments calls getSegments(), which requests text segments from the database. getSegments returns a promise, and I'm trying to handle this promise with .then in areThereAvailableSegments, but not sure how to pass to the controller.
Here's my Segments factory, that returns the $resource object, segmentsfactory.js:
angular.module('appApp')
.factory('SegmentsFactory', function ($resource) {
return $resource('http://localhost:3000/api/v1/segments/:id');
});
Here's my Segments service, segmentsservice.js:
angular.module('appApp')
.service('SegmentsService', function (SegmentsFactory) {
// gets segments from mongo
this.getSegments = function(jobId) {
return SegmentsFactory.query({ job_id: jobId }).$promise;
};
// should return true/false if there are available segments
this.areThereAvailableSegments = function(jobId) {
var dump = [];
// queries for all segments
this.getSegments(jobId)
.then(function(segments) {
// dumps segments status in a temp array
for (var i = 0; i < segments.length; i++) {
dump.push(segments[i].status);
}
// returns true if there is an 'available' in the temp array
if (dump.indexOf('available') >= 0) {
return true;
} else {
return false;
}
});
};
});
And here's my controller, main.js:
$scope.startJob = function() {
if (SegmentsService.areThereAvailableSegments(jobId)) {
console.log('there are available segments, then trigger texteditor');
} else {
console.log('no segments to translate');
}
};
When executing, console shows "no segments to translate", because SegmentsService is not returning true or false.
How to solve this?
the function areThereAvailibleSegments should return a promise as well.
And then in your controller you have a then case and have your if-else there.
this.areThereAvailableSegments = function(jobId) {
var dump = [];
// queries for all segments
return this.getSegments(jobId) //Return here
.then(function(segments) {
// dumps segments status in a temp array
for (var i = 0; i < segments.length; i++) {
dump.push(segments[i].status);
}
// returns true if there is an 'available' in the temp array
if (dump.indexOf('available') >= 0) {
return true;
} else {
return false;
}
});
};
since your success function returns true or false the promise from areThereAvailibleSegments-function will have that boolean.
Then in you controller:
SegmentsService.areThereAvailableSegments(jobId).then(function(available){
if (available) {
console.log('there are available segments, then trigger texteditor');
} else {
console.log('no segments to translate');
}
});
Theres no reason to have both the service and factory. Remove the factory and move that code into the service. As for your problem, you will have to use the $q module to return a promise. https://docs.angularjs.org/api/ng/service/$q
this.areThereAvailableSegments = function(jobId) {
var dump = [];
var deferred = $q.deferred;
// queries for all segments
this.getSegments(jobId)
.then(function(segments) {
// dumps segments status in a temp array
for (var i = 0; i < segments.length; i++) {
dump.push(segments[i].status);
}
// returns true if there is an 'available' in the temp array
if (dump.indexOf('available') >= 0) {
deferred.resolve(true);
} else {
deferred.resolve(false);
}
});
return deferred.promise;
};
You will then have to deal with the promise in the controller.
I would set it up like this...complete the promise in your factory and then call the data in the controller.
In your services js:
.factory('SegmentsFactory', function ($resource) {
var APIRequest = {};
APIRequest.getSegments = function(id){
segmentsAPI = $resource('http://localhost:3000/api/v1/segments/'+id'', ;
return segmentAPI.get().$promise.then(function(segments){
console.log(segments);
return segments;
});
};
return APIRequest;
}]);
And then in your controller:
var dump = [];
$scope.startJob = function() {
if (SegmentsService.areThereAvailableSegments) {
APIRequest.getSegments($scope.id).then(function(data){
if (data.error){
//error stuff here
} else {
if (data.segments.length > 0){
for(var i=0; i < segments.length; i++){
console.log(segments);
dump.push(segments[i].status);
}
};

Categories