I have a given collection of items to be processed; each item must wait for the completion of the previous one.
By collection of items I mean an array of integer values.
By "processing" I mean make a POST to a HTTP server, passing the integer value of each array element.
I've found something that looks like waht I'm looking for: doSynchronousLoop.js but I wonder if there are alternatives.
If your site may pause rendering while doing the requests, here's a solution with jQuery:
// process 5 items
for (var i = 0; i< 5; i++) {
// ajax request done with jquery
$.ajax({
async: false, /* this makes it execute synchronously */
url: "the url to handle item #i",
type: "POST",
success: function(msg) {
// process data for item #i
}
})
}
Edit: you can solve it asynchronously, too:
items = [put your items here]
current_item = 0
function processItem() {
if (current_item == items.length) {
// list processing finished
return;
}
$.ajax({
async: true,
url: "the url to handle item #current_item",
type: "POST",
success: function(msg) {
// process data for item #current_item
processItem();
current_item++;
}
})
}
Don't miss to put the variables in a scope, I just left them in global scope to make the example easier to understand.
See also the docs: jQuery.ajax
Related
I'm working with an application that uses DataTables to generate a HTML table which is populated by data from an ajax request.
This is fairly simple:
var substancesTable = $('#substancesTable').DataTable({
"processing": true,
"serverSide": true,
"searching": false,
"ajax": {
"url": "/get-substances.json",
"method": "POST",
"cache": false,
"dataSrc": function (json) {
// Update non-Datatables UI elements and perform other functions based on the ajax response
$('#numSubstances').html(json.recordsTotal);
drawOptionsButton(json.isFiltering);
// Must return data for DataTables to work
return json.data;
}
},
// ...
});
There is a callback which DataTables provides, called rowCallback (https://datatables.net/reference/option/rowCallback) which allows post processing of table rows after the table has been drawn. The key thing here is that it's after the ajax request to /get-substances.json; the table must be populated with data because this callback is used to manipulate data within it at that point.
Within rowCallback I'm providing an array of row ID's in my table - that is to say ID's which correspond to <tr> elements inside #substancesTable - and I go on to expand these rows. I can do this manually by hardcoding in an array of row ID's, e.g.
var substancesTable = $('#substancesTable').DataTable({
// ...
"rowCallback": function(row) {
var id = $(row).find('td:first').text();
var index = $.inArray(id, ['4', '7']); // hardcoded array
if (index !== -1) {
var tr = $(row).closest('tr');
var row = substancesTable.row( tr );
row.child.show();
tr.addClass('active');
}
});
The array I've hardcoded means rows 4 and 7 are expanded after the table has been populated, which is equivalent to the user clicking on them.
The problem I have is that I don't want to hardcode the array. The application stores the equivalent of var index in Redis (cache) meaning that we can grab the data easily even if the user leaves the page. So I have added a second ajax request (outside the var substancesTable... block) to obtain the Redis data. This makes an ajax request to populate an array, activeRows:
var activeRows = [];
$.ajax({
url: '/view-substance/get-active-rows',
method: 'post',
cache: false,
}).done(function(data) {
activeRows = data;
console.log(activeRows);
});
I understand that the nature of ajax means my code is asynchronous. In some cases the ajax request shown above will complete before the DataTable is drawn, so I get the console.log(activeRows) appearing before the table is rendered, and in other cases it happens afterwards.
What is the correct way to make this second ajax request such that the values from it can be used in place of the hardcoded array? I appreciate I will need to convert the response to an array (since it's still JSON in the console.log statement). But my question is focused on where to put this code such that it can be used reliably inside rowCallback?
I have read How do I return the response from an asynchronous call? and understand about the async nature. I can't work out how to structure this to be used in a callback that's already part of an ajax request.
The application uses DataTables version 1.10.16 and jquery 3.2.1
You can actually use the ajax option to:
Make the first AJAX request which retrieves the active rows.
Once the active rows are retrieved, make the second AJAX request which retrieves the table data.
Example: (see full code and demo here)
var activeRows = [];
function getActiveRows() {
return $.ajax({
url: '/view-substance/get-active-rows',
type: 'POST',
dataType: 'json'
...
}).done(function(data){
activeRows = data;
console.log(activeRows);
});
}
function getTableData(data, callback) {
return $.ajax({
url: '/get-substances.json',
type: 'POST',
dataType: 'json',
'data': data // must send the `data`, but can be extended using $.extend()
...
}).done(callback); // and call callback() once we've retrieved the table data
}
$('#example').dataTable({
ajax: function(data, callback){
getActiveRows().always(function(){
getTableData(data, callback);
});
},
rowCallback: function(row, data){
...
}
});
UPDATE
In the above example, I separated the AJAX calls into two different functions mainly to avoid long indentation in the ajax option when you call the $('#example').dataTable(). The code would otherwise look like:
var activeRows = [];
$('#example').dataTable({
ajax: function(data, callback){
// 1. Retrieve the active rows.
$.ajax({
url: '/view-substance/get-active-rows',
type: 'POST',
dataType: 'json'
...
}).done(function(res){
activeRows = res;
console.log(activeRows);
}).always(function(){
// 2. Retrieve the table data.
$.ajax({
url: '/get-substances.json',
type: 'POST',
dataType: 'json',
'data': data // must send the `data`, but can be extended using $.extend()
...
}).done(callback); // and call callback() once we've retrieved the table data
});
},
rowCallback: function(row, data){
...
}
});
I used the .always() so that the table data would still be retrieved in case of failures in retrieving the active rows.
You could solve your problem with Promises. Promises are objects that help you manage and coordinate asynchronous tasks. Your case would look something like this:
var activeRows = [];
var substancesTable = $('#substancesTable').DataTable({
// ...
});
var pDataTable = new Promise(function(resolve, reject){
// you wan't to resolve just once,
// after the table has finished processing the received data.
// (You may want to change draw to an event that's more suitable)
substancesTable.one('draw', resolve);
});
var pOpenRows = new Promise(function( resolve, reject ){
$.ajax({
url: '/view-substance/get-active-rows',
method: 'post',
cache: false,
}).done(function(data) {
// you can either save your rows globaly or give them to the resolve function
// you don't have to do both
activeRows = data;
resolve( data );
});
});
// Here we basically create a third promise, which resolves (or rejects)
// automatically based on the promises in the array.
Promise.all([pDataTable, pOpenRows])
.then(function( values ){
// expand your table rows here
// Note: if you gave your rows to the resolve function you can access
// them here in values[1] (pDataTable's data would be in values[0])
});
In case you want to learn more about Promises:
https://developers.google.com/web/fundamentals/primers/promises
https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://scotch.io/tutorials/javascript-promises-for-dummies
For details on browser support you may look here:
https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://caniuse.com/#search=Promise
I am having problems in nesting ajax calls with jQuery to wikipedia api.
The HTML is simple, just an input form and a button:
<input id='search-input' type="text" class="form-control">
<button id="search-button" type="button" class="btn btn-primary">Search</button>
<div id ='outputDiv'></div>
THe search button has an event listener that fires a function that grabs data from wikipedia API:
...
searchBtn.addEventListener('click', searchwiki);
...
function searchwiki(){
let searchTermObjects =[]
let term = inputfield.value;
let titleUrl = search_term(term);
createWikiObject(titleUrl).then(function(){
searchTermObjects.forEach(function(elem){
addExtrctToObj(elem)
})
}).then(function(data){
append_result(searchTermObjects)
})
}
function createWikiObject(titleUrl){
return $.ajax({
type: "GET",
url: titleUrl,
dataType : 'jsonp',
async: true,
error : function(ermsg){
console.log('error in searching',ermsg)
},}).then(function(data){
for(let i = 0; i < data[1].length; i ++){
searchTermObjects.push({
'title': data[1][i].replace(/\s+/g, '_'),
'description': data[2][i],
'url': data[3][i],
})
}; // this for loop should push each result as an object to an array named searchtTermObjects, and i am planning to use this array in the next ajax call to add another property named extract to each object in array
}
);
}
function addExtrctToObj(obj){
console.log(obj)
return $.ajax({
type: "GET",
url: get_text(obj['title']),
dataType : 'jsonp',
async: true,
error : function(ermsg){
console.log('error getting text',ermsg)
}
}).then(function (data){
let pageID = Object.keys(data.query.pages);
if(data.query.pages[pageID].hasOwnProperty('extract')){
obj['extract'] = data.query.pages[pageID].extract;
}
// this function adds the extracted text for each article ,
// the searchTermObjects now looks something like:
/ [{'title':...,'url':...,'description':...,'extract':..},{...}]
})
};
function append_result(termsObjectsArray){
// this function should loop through the searchtermobjects and append leading text for each object in the array to the Output div under the button
for (let i = 0; i < termsObjectsArray.length; i++){
let newDiv = document.createElement('div');
HOWEVER, Object.keys(termsObjectsArray[i]) returns only three keys at this time, and doesn't see the extract key'
console.log(Object.keys(termsObjectsArray[i]))
newDiv.classList.add('wiki-result');
newDiv.innerHTML = termsObjectsArray[i]["extract"];
HERE is where i get error -- the inerHtml of newDiv has value UNDEFINED
outputDiv.appendChild(newDiv);
}
}
// the api calls are formed with these functions:
let base_url = "https://en.wikipedia.org/w/api.php";
function search_term(term) {
let request_url = base_url + "?action=opensearch&search=" + term + "&format=json&callback=?";
return request_url;
}
function get_text(term){
let request_url = base_url + "?action=query&prop=extracts&exintro=&format=json&titles=" + term; // explaintex= returns plaintext, if ommited returns html
return request_url;
}
afetr I console.log(searchTermObjects) i get what i need, the array with objects that have all 4 properties with correct names, but I don't understand why the append_result function doesn't see the 'extract' key.
Next to the logged object in the console is the 'i' sign that says 'Value below was evaluated just now' , and there I have what I wanted -- every search result as an object with title, url, description, and extract keys.
copy this code to your IDE to see if you can help me with finding solution.
I believe the issue you're having is that you're attempting to return a Deferred object, and there's nothing to return yet because of the deferral.
return $.ajax({
type: "GET",
url: get_text(obj['title']),
dataType : 'jsonp',
async: true,
error : function(ermsg){
console.log('error getting text',ermsg)
}
})
The async value is true, so the code is moving on before the request is finished, and you're getting a null value back.
Try setting async: false and see if you get a better response. As pointed out by Andrew Lohr in the comments, this is not a good way to solve the problem, it will only tell you if that is the problem.
If it is, then I would recommend not breaking the request up into multiple functions. You should just chain the AJAX calls, using the deferral approach. It would be structured like this:
$.ajax({ ... }).then(function(data){
// ... do something with the data ...
// Make your followup request.
$.ajax({ ... }).then(function(data) {
// ... finalize the response ...
});
});
Also consider using the context option in the ajax call to pass in a callback method that can be fired once the chain is complete.
I have an ajax request that gets called several times based on the number of request objects in an array. The order in which these objects are in inside of the array is important, and needs to be reflected in a dynamically generated list in that same order. When the server sends back each response I update a <ul> as shown below.
$.ajax({
type: 'POST',
url: baseServiceURL + 'report/',
processData: false,
dataType: 'json',
contentType: 'application/json',
data: payload,
crossDomain: true,
})
.done(function (response) {
updateUI(response);
})
.fail(function (jqXHR, textStatus) {
// handle failure
});
var updateUI = function (response) {
// Update the drop-down list
$('#dropdown').append('<li><a class="dd-option" data-value="' + response.ReportName + '" data-path="' + response.ReturnURL + '" href="#">' + response.ReportName + '</a></li>');
// do more stuf...
};
How can I dynamically build the list in such a way to where the response display in the proper order? One thing I have done is add a order param to the request who's value is the index of the request object in the array. My thought is my service can send that value back in the response so the javascript can act on it.
EDIT: The question here is asking basically the same thing except rather than using a getJSON command and appending divs I'm using a post and appending <li> elements.
There are two possible strategies here.
Update your UI immediately upon receiving response and then re-render if a new value is received
Wait until all ajax replies have finished and then render your UI
For (1) you should just keep a running total of all items
var $dropdown = $('#dropdown');
var renderDropdown = function(reports) {
//use lodash or underscore.js here cause implementing this manually is annoying
var sortedSelections = _.sortBy(reports, 'order');
var htmlPerItem = sortedSelections.map(function(item) {
return '<li><a ..... </li>';
});
$dropdown.html(htmlPerItem.join(''));
}
var reportSelections = [];
payloads.map(function(payload) {
$.ajax({ ... })
.then(function(response) {
reportSelections.push(response);
renderDropdown(reportSelections);
})
})
for (2) you can use jquery $.when
var gettingResults = payloads.map(function(payload) {
return $.ajax({ .... });
});
$.when(gettingResults).then(function() {
//the first arg will be response1, the second response2, etc
var reportSelections = _.sortBy(arguments, 'order');
renderDropdown(reportSelections);
});
Note in (1) you render once per item but get an updating view as items come in. In (2) you render only once but have to wait until all loading is complete.
Of course a variation of (1) is that you don't re-render anything, but merely insert items into the correct location as they are loaded. That's going to be more difficult to debug and more code so I leave it as an exercise for the reader (use jquery's $.fn.data to store the original item with the element).
I have simple autocomplete input field with Javascript like this:
$('#search').on('keyup', function () {
var query = $(this).val();
$.ajax({
type: "GET",
url: "/search",
data: { query: query }
}).done(function (results) {
showSearchResults(results);
});
});
Sometimes first call takes more time then second or third and results are overridden.
How can I make sure that results only from the latest successful call are displayed?
I mean if I got response from call #3 - I no longer care about calls #1 and #2 and don't want them to override results of call #3.
Ajax function is in default asynchronous it means that many of functions can run on same time. If You wrote 3 letters it will run 3 times, after 3 keyups. If you want to run function in sequence just add setting async: false.
$.ajax({
type: "GET",
url: "/search",
async: false,
data: { query: query }
}).done(function (results) {
showSearchResults(results);
});
But i think You should add some delay, so function will not run immediately after every keyup, just after last one.
I suggest that you bind an incremental id to each ajax request that you send. When you get a response, just check that it carries last given id.
let lastXHRid=0; // tracker of last sent ajax request
$('#search').on('keyup', function () {
let reqXHR = $.ajax({ // create a variable out of $.ajax returned value
type: "GET",
url: "/search",
data: { query: $(this).val() }
});
lastXHRid++; // increment the XHR counter
reqXHR.id = lastXHRid; // attach id to the request
reqXHR.done(function(results, status, respXHR) {
if ( respXHR.id == lastXHRid ){ // compare id of received and last sent requests
showSearchResults(results);
}
});
});
(Edit: I initially suggested tracking the unix timestamp of last sent request, but as #seth-battis suggested in the comments, a sequence number is far enough. As a bonus, I also debugged my sample snippet!)
I have some code that looks like this (I left out the parts that are unimportant for the question):
$.ajax({
type: "POST",
url: "prepXML.php",
data: "method=getartists&user=" + userName + "&amount=" + amount,
dataType: "xml",
success: function(xml) {
$("artist", xml).each(function(){
// calculate and output some stuff and then call getTracks()
getTracks(artistName, artistNamePOST, artistPlaycount, divId, artistId);
});
}
});
function getTracks(artistName, artistNamePost, artistPlaycount, divId, artistId){
$.ajax({
type: "POST",
url: "prepXML.php",
data: "method=gettracks&user=" + userName + "&artist=" + artistNamePOST + "&playcount=" + artistPlaycount,
dataType: "xml",
success: function(xml){
// calculate and output some stuff
});
}
});
When this is run, it calls getTracks() fifty times and makes quite some (server) CPU load in very short time until it all gets done. What I would like to do is to group AJAX getTrack() queries by for example 5 at a time, wait until those five are done, then call the next five, wait until the next five are done, call the next five etc. The purpose of this would be to make fewer queries at basically the same time (less and more evenly spread CPU load).
I am unsure how or even if this can be done since it kind of partially beats the point of AJAX, but I would still like to make this work if possible. Could someone please point me into the right direction? Thank you.
To better understand what I need this for and what the app does, here is a link to the app. it can be tried out with any lastfm nick (if you don't have one, you can use mine - "pootzko"). I hope I am allowed to put the link in the post(?), if not, feel free to remove it..
I'd consider only retrieving track info for artists that a user has clicked on. Or possibly retrieving all of the data via a single request (perhaps then after it is retrieved process it in batches via setTimeout()).
But something along the lines of the following might work to do only five requests at a time:
$.ajax({
type: "POST",
url: "prepXML.php",
data: "method=getartists&user=" + userName + "&amount=" + amount,
dataType: "xml",
success: function(xml) {
var artists = $("artist", xml),
i = 0,
c = 0;
function getTracksComplete() {
if (--c === 0)
nextBatch();
}
function nextBatch() {
for(; c < 5 && i < artists.length; i++, c++) {
// artists[i] is the current artist record
// calculate and output some stuff and then call getTracks()
getTracks(artistName, artistNamePOST, artistPlaycount, divId, artistId,
getTracksComplete);
}
}
// Optional - if you need to calculate statistics on all the artists
// then do that here before starting the batches of getTracks calls
artists.each(function() { /* do something */ });
// Kick of the first batch of 5
nextBatch();
}
});
function getTracks(artistName, artistNamePost, artistPlaycount, divId, artistId,
callback){
$.ajax({
type: "POST",
url: "prepXML.php",
data: "method=gettracks&user=" + userName + "&artist=" + artistNamePOST + "&playcount=" + artistPlaycount,
dataType: "xml",
success: function(xml){
// calculate and output some stuff
},
complete : function() {
if (callback) callback();
});
}
});
The above was just off the top of my head (so I didn't have time to build it to scale or to paint it), but the idea is that instead of using .each() to loop through all of the artists at once we will cache the jQuery object and do a few at a time. Calling the function nextBatch() (which is local to the success handler and thus has access to the local variables) will run a loop that calls getTracks() only 5 times, but starting processing from where we left off the previous time. Meanwhile getTracks() has been updated slightly to accept a callback function so when its ajax call completes (and note we do this on complete rather than success in case of errors) it can let the main process know that it has finished. Within the callback we keep track of how many have completed and when they all have call nextBatch() again.
Either include all the track information in the data that is returned from getartists OR only call getTracks when someone wants to see the tracks for a specific Artist.
e.g.
Display all the Artists, and have a "View Tracks" options. Only once this is clicked then you get the tracks.
To me this is the best option because if you are loading up ALL the tracks for ALL the artists, then there is a lot of data that that probably isnt needed. No one is going to want to look through all the artists and all the artists tracks (unless the specifically want to).
My solution is to process all the artist data into the array and then start executing .ajax() requests in the batches of 5. When those 5 requests are done, next batch is executed, etc.
HERE is a working demonstration.
// just success changed
$.ajax({
type: "POST",
url: "prepXML.php",
data: "method=getartists&user=" + userName + "&amount=" + amount,
dataType: "xml",
success: function(xml) {
var data = [];
// prepare the data
$("artist", xml).each(function(){
data.push({ name: artistName /** rest of data */ } );
});
// start processing the data
processData(data, 0);
}
});
// process the data by sending requests starting from index
function process(data, index) {
var xhrs = [];
for (var i = index; i < index + 5 && i < data.length; i++) {
(function(data) {
xhrs.push($.ajax({
type: "POST",
url: "prepXML.php",
data: "method=gettracks&user=" + data.name /** rest of the data */
dataType: "xml",
success: function(xml) {
// calculate and output some stuff
}
}));
})(data[i]);
}
// when current xhrs are finished, start next batch
$.when.apply(this, xhrs).then(function() {
index += 5;
if (index < data.length) {
process(data, index);
}
});
}
It's difficult to clearly understand the logic of you application, but I think a better way should be collecting previously all the data you need to send (50 entries in a JSON object) and then call getTracks function only once, passing only one JSON object.