I'm trying to figure out how to accomplish this workflow, but can't seem to nail it. I've got n number of <select> elements on a page. When the page loads, for each <select> element, I need to make a $.get(...); call. Once all of those calls are done, then, and only then do I need to run an additional function. Here is some example code to better explain:
function doWork(selectEl) {
var getData = ...; // build request data based on selectEl
$.get('/foo/bar', getData, function (data) {
// Do something to selectEl with the result
});
}
function doMoreWork() {
// Do something with all the selects now that they are ready
}
$(function () {
// For each of the select elements on the page
$('select').each(function(index, selectEl) {
// Go do some AJAX-fetching of additional data
doWork(selectEl);
});
// Once *all* the $.get(...) calls are done, do more things
doMoreWork();
});
Using the code above, doMoreWork() is usually called before all of the async $.get(...); calls have had a chance to return; which is not what I want. I need to have all of the $.get(...); calls complete before doMoreWork() can be called. Basically I need a callback of sorts to execute once ALL of the $.get(...); calls in the above example have finished.
How would I go about accomplishing this?
Every time you call doWork, increment a counter.
Every time a response comes back, decrement the counter.
Have the callback invoke doMoreWork when the counter reaches 0.
var counter = 0;
function doWork(selectEl) {
counter++;
var getData = ...; // build request data based on selectEl
$.get('/foo/bar', getData, function (data) {
counter--;
if( !counter ) { doMoreWork(); }
});
}
function doMoreWork() {
// Do something with all the selects now that they are ready
}
$(function () {
// For each of the select elements on the page
$('select').each(function(index, selectEl) {
// Go do some AJAX-fetching of additional data
doWork(selectEl);
});
});
I would write a class something like:
function synchronizer(query, action, cleanup) {
this.query = query;
this.action = action;
this.cleanup = cleanup;
this.remaining = query.length;
this.complete = function() {
this.remaining -= 1;
if (this.remaining == 0) { this.cleanup(query); }
}
this.run = function() {
query.each(function(index, which) { action(which, this.complete); })
}
}
// Aargh. Expecting doWork() to call a passed-in continuation seems ugly to me
// as opposed to somehow wrapping doWork within the synchronizer... but I can't
// think of a way to make that work.
function doWork(element, next) {
var getData = ...; // build request data based on element
$.get('/foo/bar', getData, function(data) {
// Do something to element with the result, and then
next();
});
}
function doMoreWork(elements) {
// Do something with all the selects now that they are ready
}
new synchronizer($('select'), doWork, doMoreWork).run();
Keep track of how many Ajax calls have yet to complete, and execute doMoreWork() when there are none left.
$(function(){
var workLeft = $('select').length;
function doWork(selectEl) {
var getData = ...; // build request data based on selectEl
$.get('/foo/bar', getData, function (data) {
// Do something to selectEl with the result
// If done all work
if(!(--workLeft)){
doMoreWork();
}
});
}
function doMoreWork() {
// Do something with all the selects now that they are ready
}
// For each of the select elements on the page
$('select').each(function(index, selectEl) {
// Go do some AJAX-fetching of additional data
doWork(selectEl);
});
});
You may also want to catch ajax errors.
You can use jQuery's $.when to join together multiple Deferred objects to one:
$.when.apply($, $('select').map(function(index, selectEl) {
return $.ajax(....);
}).get()).done(function() {
// All AJAX calls finished
});
Basically, $.when takes multiple Deferred objects as each argument and wraps them together as one Deferred by keeping track of the number of completed sub-deferres, similar to how a couple of the answers here implemented it manually.
A more readable version of the above code is:
var requests = [];
$('select').each(function(index, selectEl) {
request.push($.ajax(....));
}
$.when.apply($, requests).done(function() {
// All AJAX calls finished
});
Maybe you could use the JavaScript underscore library's after function.
(note: I haven't tested this code)
var numberOfSelectElements = n;
var finished = _after(numberOfSelectElements, doMoreWork);
function doWork(selectEl) {
var getData = ...; // build request data based on selectEl
$.get('/foo/bar', getData, function (data) {
finished();
});
}
function doMoreWork() {
// Do something with all the selects now that they are ready
}
$(function () {
// For each of the select elements on the page
$('select').each(function(index, selectEl) {
// Go do some AJAX-fetching of additional data
doWork(selectEl);
});
});
Use Deferred:
function doWork(selectEl) {
var getData = ...;
// return Deferred object
return $.get('/foo/bar', getData, function (data) {
});
}
var selects = $('select');
function doItem(i) {
if(selects.length === i) return doMoreWork(); // if no selects left, abort and do more work
$.when(doWork(selects.get(i)).then(function() { // fetch and do next if completed
doItem(i + 1);
});
});
doItem(0); // start process
Since it looks like you're doing jQuery, you could use the $.ajaxStop event handler...
http://api.jquery.com/ajaxStop/
EDIT Said $.ajaxComplete instead of the correct $.ajaxStop... Fixed now...
Related
The closest answer I could find was this https://stackoverflow.com/a/17216555/2834734
The most common use for .then is chaining ajax requests:
$.ajax({...}).then(function(){
return $.ajax({...}); }).then(function(){
return $.ajax({...}); }).then(function(){
return $.ajax({...}); }).then(function(){
return $.ajax({...}); });
this can easily be done in a loop
However it's the looping procedure I'm having difficulty with plus I have some unusual circumstances.
A brief explanation is, I have an array of requests that I need to loop through, some will invoke an ajax load and others will not. I need them to run consecutively but also run a specific function call at then end.
Here is a simple(I hope) sample of my situation:
// Here is my flow of logic
var thingsToDo = new tasks(); // Initiate the constructor, explained below
// Loop through the requests array and process them consecutively
for (var i in thingsToDo.requests) {
$.when(thingsToDo.jqxhr).then(function() {
thingsToDo.requests[i].fn();
})
}
// Run my final function at the end of the chain.
$.when(thingsToDo.jqxhr).then(function() {
runOnceAllComplete();
});
This is the constructor class the above is based on.
// Constructor
function tasks() {
_tasks_ = this; // automatic global var
this.jqxhr = undefined; // Var to monitor ajax calls
this.requests = [ // list of tasks to run.
{
url: 'file1.php',
fn: function() {
_tasks_.load(this.url);
console.log('file1 loaded');
}
}, {
url: 'file2.php',
fn: function() {
_tasks_.load(this.url);
console.log('file2 loaded');
}
}, {
noUrl: true, // Note there is no file to load here
fn: function() {
console.log('no file here to load, but process something else');
$('body').css("background-color", "blue");
}
}, {
url: 'file3.php',
fn: function() {
_tasks_.load(this.url);
console.log('file1 loaded');
}
},
];
this.load = function(file) { // This runs the ajax call and resets this.jqxhr
this.jqxhr = $.get(file);
}
}
function runOnceAllComplete() {
alert('hooray!, we finished');
}
The tricky part I have is the requests are created dynamically so there can be 1-n many requests to perform, which is why I chose to loop, and they must be performed in that order.
As mentioned some requests will invoke an ajax call and others may not, this doesn't seem to break $.when().then(), but the problem is the loop continues before the promise is resolved and my final function happens before the final request.
Still trying to get my head around promises, the first time I've used them.
Try including return statement at fn , this.load ; adding .promise() chained to $("body") at fn to return a jQuery promise object ; using Function.prototype.apply() , $.map() at $.when()
fn: function() {
// added `return`
return _tasks_.load(this.url);
}
this.load = function(file) {
this.jqxhr = $.get(file);
// added `return`
return this.jqxhr
}
fn: function() {
console.log('no file here to load, but process something else');
// added `return` , `.promise()`
return $('body').css("background-color", "blue").promise();
}
$.when.apply($, $.map(thingsToDo.requests, function(task) {
return task.fn()
})).then(runOnceAllComplete)
See also Pass in an array of Deferreds to $.when() , What does $.when.apply($, someArray) do?
however I'm encountering a problem, using the .map() it doesn't wait
for each request to complete before processing the next one. I need
each one to complete before moving to the next.
Try using .queue() , which will calls functions in queue sequentially, and only when next is called at current function
$(thingsToDo).queue("tasks", $.map(thingsToDo.requests, function(task) {
return function(next) {
// call next function in `"tasks"` queue
// when current function completes using `.then(next)`
return task.fn().then(next)
}
})).dequeue("tasks").promise("tasks").then(runOnceAllComplete)
See .queue() , .promise() , Execute function queue in javascript
I have this code as a starting point.
// $ = jQuery
// groupAdata and groupBdata are arrays
function funcA(elem) {
for (f = 0; f < groupAdata.length ; f++) {
// this is an example on how this function calls other functions asynchronously.
elem.children('.partyA').each( function() {
this.innerHTML = "been here" + groupAdata[f];
});
}
}
function funcB(elem) {
// another function that fires more calls
for (f = 0; f < groupAdata.length ; f++) {
$.post(url, somedata, function(data) {
elem.children('.partyB').each( function() {
this.innerHTML = "will be there" + groupBdata[f] + data;
});
}
}
}
$(document).ready(function() {
$('.groupA').each(function () {
funcA(this);
});
$('.groupB').each(function (){
funcB(this);
});
});
function endofitall() {
// call this after all instances of funcA and funcB are done.
}
When running endofitall(), I'd like to be sure that all calls of funcA and funcB are done.
I take that Promises and jQuery.Deferred() would be a good/preferred approach but was not able to map the answers I found to this specific scenario. (It is part of a templating tool that fires multiple dom manipulators func[AB] for multiple DOM elements.)
You can use $.when().
Your goal should be to get to:
// call funcA, call funcB
$.when( funcA(), funcB() )
// when everything is done go on with the callback
.done(endofitall);
In the case of funcA (synchronous function there's no problem and it will work as is).
In the case of funcB (asynchronous) there are some things to consider. If it would be just one ajax call your code should be something like:
// This function returns a promise.
// When it's fulfilled the callback (in your case '.done(endofitall)')
// will be called.
function funcB(somedata){
return $.post(url, somedata);
}
As you are actually making more requests you have to return a resolved promise only when all calls have been fulfilled.
// an *Asynchronous* function, returns an array of promises
function funcB(elem, groupAdata) {
var allCalls = [];
// for each element in the array call the relative async
// function. While you're calling it push it to the array.
groupAdata.forEach(data, function(data){
allCalls.push( $.post(url, data) );
});
// allCalls is now an array of promises.
// why .apply(undefined)? read here: https://stackoverflow.com/a/14352218/1446845
return $.when.apply(undefined, allCalls);
}
At this point you can go for a flat and clear:
$.when( funcA(), funcB() ).done(endofitall);
As a rule of thumb: if you are making async requests try to always return a promise from them, this will help flatten out your code (will post some link later on if you want) and to leverage the power of callbacks.
The above code can surely be refactored further (also, I haven't used a lot of jQuery in the last few years, but the concept applies to any Js library or even when using no library at all) but I hope it will help as a starting point.
References:
$.when
A similar answer here on SO
Call endofitall() inside each iteration for funcA and funcB. Keep a counter and perform the actual work once the counter reaches the number signifying all the tasks are complete.
function funcA(elem) {
for (f = 0; f < groupAdata.length ; f++) {
// these calls are not async
elem.children('.partyA').each( function() {
this.innerHTML = "been here" + groupAdata[f];
});
endofitall();
}
}
function funcB(elem) {
// another function that fires more calls
for (f = 0; f < groupBdata.length ; f++) {
$.post(url, somedata, function(data) {
elem.children('.partyB').each( function() {
this.innerHTML = "will be there" + groupBdata[f] + data;
});
endofitall();
}
}
}
$(document).ready(function() {
$('.groupA').each(function () {
funcA(this);
});
$('.groupB').each(function (){
funcB(this);
});
});
var counter=0;
function endofitall() {
if(++counter==groupAdata.length + groupBdata.length){
//do stuff
}
So i'm using angularJS and$q service. But for simplicity i'm using $timeout since it creates a promise.
Question:
Is it possible to only return a promise when a conditional has been satisfied? For this example, I want to wait for carousel.params.caruselReady to return true before I move to next .then.
$timeout(function(){
if(Carousel.params.ready()){
return ready;
}
},0).then(function(ready){
//...do stuff..//
}
Carousel.params.ready is coming from Carousel.js a factory:
//this function gets called everytime a image has been loaded. when all images are rendered than carousel is ready
carouselElLoaded: function (result) {
var count = 1;
Carousel.params.pageRenderedLength += count;
if (Carousel.params.pageRenderedLength >= Carousel.params.pageLength) {
Carousel.params.carouselReady = true;
}
},
Lastly, carouselElLoad is being called from wa.pages.js (a directive)
$img.onload = function (e) {
var self = this;
$timeout(function () {
return self;
}, 0).then(function () {
Carousel.set.carouselElLoaded(e);
});
};
Currently, I'm using a $watch to achieve this but I was wondering if I could accomplish the same w/o a watcher.
You can use a promise instead of a boolean flag and it will do exactly what you want.
In Carousel.js define a promise names isCarouselReady and resolve it once the carousel is ready, your code should like something like this:
// During the initialisation of your factory
var carouselDeferred = $q.defer()
Carousel.params.isCarouselReady = carouselDeferred.promise;
//this function gets called everytime a image has been loaded. when all images are rendered than carousel is ready
carouselElLoaded: function (result) {
var count = 1;
Carousel.params.pageRenderedLength += count;
if (Carousel.params.pageRenderedLength >= Carousel.params.pageLength) {
carouselDeferred.resolve(/* What ever you'd like here */);
}
},
Now you all you have to do in order to use it is:
Carousel.params.isCarouselReady.then(function() {
// Your logic
});
Your last part would be nicer if it would look something like this:
$img.onload = function (e) {
$scope.$apply(function(){
Carousel.set.carouselElLoaded(e);
});
};
my first post here, hi everyone :)
i have this js code to access a json api:
function a() {
//does some things
//...
//then calls function b
b(some_params);
}
function b(params) {
//calls an objects method that makes an ajax call to an api and get the response in json
someObject.makeajaxcalltoapi(params, function(response) {
alertFunction(response);
});
}
function alertFunction(resp) {
console.log ("the response is: ");
console.log(resp);
}
this is working ok, but now i need to modify it in order to do this:
in function a(), instead of making a single call to b(), i need to call b() multiple times in a loop, with different parameters each time.
and then i want to call alertFunction() passing it an array with all the responses, but only after all responses have been received.
i have tried to use $.when and .then, after seeing some examples on deferred objects, but its not working:
function a() {
//does some things
//...
//then calls function b
var allResponses = [];
$.when(
anArray.forEach(function(element) {
allResponses.push(b(some_params));
});
).then(function() {
alertFunction(allResponses);
});
}
function b(params) {
//calls an objects method that makes an ajax call to an api and get the response in json
someObject.makeajaxcalltoapi(params, function(response) {
//alertFunction(response);
});
return response;
}
function alertFunction(allresp) {
console.log ("the responses are: ");
console.log(allresp);
}
any help?
UPDATE - ok finally got it working. i put here the final code in case it helps somebody else...
function a() {
//does some things
//...
//then calls function b
var requests = [];
//-- populate requests array
anArray.forEach(function(element) {
requests.push(b(some_params));
});
$.when.apply($, requests).then(function() {
alertFunction(arguments);
});
}
function b(params) {
var def = $.Deferred();
//calls an objects method that makes an ajax call to an api and get the response in json
someObject.makeajaxcalltoapi(params, function(response) {
def.resolve(response);
});
return def.promise();
}
function alertFunction(allresp) {
console.log ("the responses are: ");
console.log(allresp);
}
Here is one way to use $.when with an unknown number of AJAX calls:
$(function () {
var requests = [];
//-- for loop to generate requests
for (var i = 0; i < 5; i++) {
requests.push( $.getJSON('...') );
}
//-- apply array to $.when()
$.when.apply($, requests).then(function () {
//-- arguments will contain all results
console.log(arguments);
});
});
Edit
Applied to your code, it should look something like this:
function a() {
var requests = [];
//-- populate requests array
anArray.forEach(function(element) {
requests.push(b(some_params));
});
$.when.apply($, requests).then(function() {
alertFunction(arguments);
});
}
function b(params) {
//-- In order for this to work, you must call some asynchronous
//-- jQuery function without providing a callback
return someObject.makeajaxcalltoapi();
}
function alertFunction(allresp) {
console.log ("the responses are: ");
console.log(allresp);
}
When the form is submitted, I'm calling a function getPosts and passing through a variable str. What I'd like to do is get the data returned from that function.
// when the form is submitted
$('form#getSome').submit(function(){
var str = $("form#getSome").serialize();
var something = getPosts(str);
* This is where I'd like to get the data returned from getPosts()
return false;
});
// get the data
function getPosts(str){
$.getJSON('http://myurl.com/json?'+str+'&callback=?',
function(data) {
arrPosts = new Array();
$.each(data.posts, function(i,posts){
// build array here
});
return arrPosts;
});
};
I've tried many things, but have only gotten 'undefined' returned. I've tried console.log(something);, console.log(getPosts).
I'm missing something very fundamental here. Any help would be greatly appreciated.
EDIT:
What I'm trying to do is create a single function that would get posts. Then different events would call that function. I could then use that data. So one event may be submitting a form, another may be clicking a link, another lazy/endless scrolling. All could use the same getPosts function.
There's a lot of parsing out the results which amounts to a lot of lines of code. Was just trying to find a way to reuse that function. Do you think that would be possible?
$('a.thisLink').click(function(){
getPosts();
get the return from getPosts() and do something with it
});
$('form.thisForm').submit(function(){
getPosts();
get the return from getPosts() and do something with it
});
function getPosts(){
get the posts and return an array
}
Ajax requests are executed asynchronously, the callback function (function (data)) of getJSON is executed when the request ends, and returning a value in that callback has no effect, because is a nested function inside getPosts and its return value is never used.
Actually in your example, getPosts doesn't return anything and it ends its execution before the data is returned.
I would recommend you to work on your submit event handler, if you want to keep the getPosts function, you can introduce a callback parameter:
$('form#getSome').submit(function(){
var str = $("form#getSome").serialize();
getPosts(str, function (data) {
var array = [];
$.each(data.posts, function(i,posts){
// build array here
array.push(/* value to add */);
});
// array ready to work with...
//...
});
return false;
});
function getPosts(str, callback){
$.getJSON('http://myurl.com/json?'+str+'&callback=?', callback);
}
Edit 2: In response to your second comment, you could make another callback, that will be executed when the data has been processed by the first callback, and you can define it when you execute the getPosts function on the submit event handler:
$('form#getSome').submit(function(){
var str = $("form#getSome").serialize();
getPosts(str, reusableCallback, function (result) {
// result contains the returned value of 'reusableCallback' <---
});
return false;
});
function reusableCallback(data) {
var array = [];
$.each(data.posts, function(i,posts){
array.push(/* value to add */);
});
//...
return array;
}
function getPosts(str, callback, finishCallback){
$.getJSON('http://myurl.com/json?'+str+'&callback=?', function (data) {
finishCallback(callback(data)); // pass the returned value
// of callback, to 'finishCallback' which is
// anonymously defined on the submit handler
});
}
Edit 3: I think that the getPosts function and the "reusableCallback" function are strongly related, you might want to join them, and make the code easier to use and understand:
$('form#getSome').submit(function(){
var str = $("form#getSome").serialize();
getPosts(str, function (result) {
// result contains the processed results
});
return false;
});
function getPosts(str, finishCallback){
$.getJSON('http://myurl.com/json?'+str+'&callback=?', function (data) {
// process the results here
var array = [];
$.each(data.posts, function(i,posts){
array.push(/* value to add */);
});
//...
finishCallback(array); // when the array is ready, execute the callback
});
}
Your getPosts function looks incomplete, I'm no jquery expert but should it look something like:
function getPosts(str) {
$.getJSON('http://myexample.com/json?'+str+'&callback=?',function(data){
var arrPosts = [];
$.each(data.posts, function(i,posts){
... build array yada yada ...
});
return arrPosts;
});
}
The problem is that the $.getJSON callback function gets called when the get request returns the data, not inline with your function.