function returns after an XMLHttpRequest - javascript

Alright, I know questions like this have probably been asked dozens of times, but I can't seem to find a working solution for my project. Recently, while using jQuery for a lot of AJAX calls, I've found myself in a form of callback hell. Whether or not jQuery is too powerful for this project is beyond the scope of this question. So basically here's some code that shows what's going on:
function check_form(table)
{
var file = "/models/"+table+".json";
var errs = {};
var xhr = $.getJSON(file, function(json)
{
for (key in json)
{
var k = key;
var r = json[k];
$.extend(errs, check_item("#"+k,r));
}
});
return errs;
}
And... as you can probably guess, I get an empty object returned. My original idea was to use some sort of onReadyStateChange idea that would return whenever the readyState had finally hit 4. This causes my application to hang indefinitely, though. I need these errors to decide whether or not the form is allowed to submit or not (as well as to tell the user where the errors are in the application. Any ideas?
Edit. It's not the prettiest solution, but I've managed to get it to work. Basically, check_form has the json passed to it from another function, instead of loading it. I was already loading it there, too, so it's probably best that I don't continue to load the same file over and over again anyways. I was just worried about overloading memory. These files aren't absolutely huge, though, so I guess it's probably okay.

The inline-function in your $.getJSON call will be run when the Ajax call have finished. Do your work inside that (callback) function.

Your check_form function should accept a callback to execute when the information becomes available later.
function check_form(table, callback)
{
var file = "/models/"+table+".json";
var xhr = $.getJSON(file, function(json)
{
var errs = {};
for (key in json)
{
var k = key;
var r = json[k];
$.extend(errs, check_item("#"+k,r));
}
callback(errs);
});
}
Then, have callers of check_form provide a callback:
var table = getSomeTable();
check_form(table, function(errs)
{
for (var key in errs)
{
var value = errs[k];
// do something with key/value pair
}
});

Related

Calling an API based off a file synchronously in JavaScript

SOLVED: I solved my problem by doing each XMLHttpRequiest() recursively. Basically at the end of my xhr.onload, I would make another request and actively check if I've reach the end of my data - when I have I return.
I'm fairly new in JavaScript and have some familiarity with the D3 Library. I'm trying to read a CSV file from my computer using the D3 Library and sending specific information from my file to an API through an XMLHttpRequest().
With each call to the API which returns a JSON object to me, I store that object in a dataset for later use. I'm trying to have it so that my whole CSV file is read and processed before I work with the dataset, however I'm running into a problem since the API calls are asynchronous.
My code looks something like this:
var myData = [];
d3.csv("myFile.csv", function(data)
{
for (var i = 0; i < data.length; i++)
// Get appropriate data from data object
// Make API call with XMLHttpRequest() and store in myData array
});
// Handle fully updated myData array here
As it is, my code currently goes through my loop in almost an instant and makes all the API calls asynchronously and then proceeds to work on my data without waiting for anything to update.
Is there a way to ensure that my CSV file has been processed and all the API calls have returned before I can work with this dataset? I've tried callback functions and promises but had no success.
You can easily do this with a simple counter
var counter = 0;
var myData = [];
d3.csv("myFile.csv", function(data)
{
for (var i = 0; i < data.length; i++){
// Get appropriate data from data object
$.get("your/api/path", function(result){
counter++; // this is important. It increments on each xhr call made.
myData.push(result);
if(counter === data.length) cb(myData); // This will check if the current xhr request is the last xhr request. If yes then call our cb function.
});
}
});
function cb(data){
// This will run only when all the http requests are complete
// Do something with data
}
All this code does is, it makes sure that all of our requests should be completed first before calling our cb function (here you will write your further logic). This approach guarantees that cb will run only when all xhr requests are completed.
I think the answer in this post could help
d3: make the d3.csv function syncronous
You can as well use the Promise API.

getJSON jQuery won't fill object

So I've got some code that retrieves a series of objects from an API. When I try to store them in a global variable, it doesn't seem to do anything. Here's the code:
var current_corpus = {};
function page_init() {
$.getJSON("http://resource1.com", function(data) {
populate_collections(data);
populate_citations(data);
});
}
function populate_collections(collections) {
$.each(collections, function (i, item) {
current_corpus[item] = [];
});
}
function populate_citations(collections) {
$.each(collections, function (index, collection) {
$.getJSON("http://resource2.com/" + collection.collection_id, function(data) {
current_corpus[collection] = data;
console.log(current_corpus);
});
});
}
When this finishes, current_corpus is completely empty. Logging these items verifies that they're being returned from the resources I'm posting to. I think there's just something about the asynchronous nature of these calls that I'm missing.
The line
current_corpus[item] = [];
is superfluous I think as the line
current_corpus[collection] = data;
should do the same thing while also tying data to the key object. Either way at the end of these functions running trying to access current_corpus via the console just gives me back an empty object.
Resources for dealing with AJAX stuff like this would be appreciated as well.
It all depends on what you want to do when the ajax requests complete. The A in ajax stands for Asynchronous meaning that such requests are non-blocking -- i.e. they will run in the background as control moves to the next line. Which explains why you're seeing an empty object right after the functions that invoke the ajax requests.
You can confirm that your code is working fine or you can do something once all the requests complete by using the following code snippet:
$(function() {
$(document).on('ajaxStop', function() {
console.log( current_corpus );
//do something with the now fully constructed object
});
});

How can I pass data read from a JSON file to the rest of my Javascript code?

I'm new in the scripting and web world and have been trying to work through an issue I've been having. I am reading data from a local JSON file, and have been able to use jQuery.getJSON and jQuery.parseJSON successfully, but I am trying to use the data outside of the getJSON callback function and am having issues. I think it comes down to me not fully understanding the correct way to do this, and that's where I'm looking for your help. Here's my code:
var names = new Array();
$.getJSON('ferries.json', function(data) {
var jsondata = $.parseJSON(JSON.stringify(data));
var length = jsondata.nodes.length;
for (var i = 0; i < length; i++) {
names[i] = String(jsondata.nodes[i].name);
}
});
console.log('Names: ' + names[0]);
The final line returns undefined. If I were to write that line right after the for loop, it would return the desired value. Here's how the JSON file is structured:
{
"nodes":[
{
"name":"John"
},
...
{
"name":"Joe"
}
]
}
Any help would be appreciated - thanks!
Edit: One last thing, it seems that the final line (console.log(...)) executes before the $.getJSON bit, which confuses me as well.
$.getJSON runs asynchronously. The function that you pass to it is a "callback", which means that it gets called when getJSON comes back from doing its thing.
If you want to do something with the JSON data that you get back, you must wait for the callback to execute.
Also, on a side note, $.parseJSON(JSON.stringify(data)) is redundant. The data object is already a perfectly usable object with your data in it, but you're turning that object back into a JSON string and then immediately back into an object. Just use data as is. For more information, check out the jQuery API docs for getJSON.

Pass additional parameter to a JSONP callback

For a project of mine I need to do multiple calls to a (remote) API using JSONP for processing the API response. All calls use the same callback function. All the calls are generated dynamically on the client's side using JavaScript.
The problem is as follows: How do I pass additional parameters to that callback function in order to tell the function about the request parameters I used. So, e.g., in the following example, I need the myCallback function to know about id=123.
<script src="http://remote.host.com/api?id=123&jsonp=myCallback"></script>
Is there any way to achieve this without having to create a separate callback function for each of my calls? A vanilla JavaScript solution is preferred.
EDIT:
After the first comments and answers the following points came up:
I do not have any control over the remote server. So adding the parameter to the response is not an option.
I fire up multiple request concurrently, so any variable to store my parameters does not solve the problem.
I know, that I can create multiple callbacks on the fly and assign them. But the question is, whether I can avoid this somehow. This would be my fallback plan, if no other solutions pop up.
Your options are as follows:
Have the server put the ID into the response. This is the cleanest, but often you cannot change the server code.
If you can guarantee that there is never more than one JSONP call involving the ID inflight at once, then you can just stuff the ID value into a global variable and when the callback is called, fetch the id value from the global variable. This is simple, but brittle because if there are every more than one JSONP call involving the ID in process at the same time, they will step on each other and something will not work.
Generate a unique function name for each JSONP call and use a function closure associated with that function name to connect the id to the callback.
Here's an example of the third option.
You can use a closure to keep track of the variable for you, but since you can have multiple JSON calls in flight at the same time, you have to use a dynamically generated globally accessible function name that is unique for each successive JSONP call. It can work like this:
Suppose your function that generate the tag for the JSONP is something like this (you substitute whatever you're using now):
function doJSONP(url, callbackFuncName) {
var fullURL = url + "&" + callbackFuncName;
// generate the script tag here
}
Then, you could have another function outside of it that does this:
// global var
var jsonpCallbacks = {cntr: 0};
function getDataForId(url, id, fn) {
// create a globally unique function name
var name = "fn" + jsonpCallbacks.cntr++;
// put that function in a globally accessible place for JSONP to call
jsonpCallbacks[name] = function() {
// upon success, remove the name
delete jsonpCallbacks[name];
// now call the desired callback internally and pass it the id
var args = Array.prototype.slice.call(arguments);
args.unshift(id);
fn.apply(this, args);
}
doJSONP(url, "jsonpCallbacks." + name);
}
Your main code would call getDataForId() and the callback passed to it would be passed the id value like this followed by whatever other arguments the JSONP had on the function:
getDataForId(123, "http://remote.host.com/api?id=123", function(id, /* other args here*/) {
// you can process the returned data here with id available as the argument
});
There's a easier way.
Append the parameter to your url after '?'. And access it in the callback function as follows.
var url = "yourURL";
url += "?"+"yourparameter";
$.jsonp({
url: url,
cache: true,
callbackParameter: "callback",
callback: "cb",
success: onreceive,
error: function () {
console.log("data error");
}
});
And the call back function as follows
function onreceive(response,temp,k){
var data = k.url.split("?");
alert(data[1]); //gives out your parameter
}
Note: You can append the parameter in a better way in the URL if you already have other parameters in the URL. I have shown a quick dirty solution here.
Since it seems I can't comment, I have to write an answer. I've followed the instructions by jfriend00 for my case but did not receive the actual response from the server in my callback. What I ended up doing was this:
var callbacks = {};
function doJsonCallWithExtraParams(url, id, renderCallBack) {
var safeId = id.replace(/[\.\-]/g, "_");
url = url + "?callback=callbacks." + safeId;
var s = document.createElement("script");
s.setAttribute("type", "text/javascript");
s.setAttribute("src", url);
callbacks[safeId] = function() {
delete callbacks[safeId];
var data = arguments[0];
var node = document.getElementById(id);
if (data && data.status == "200" && data.value) {
renderCallBack(data, node);
}
else {
data.value = "(error)";
renderCallBack(data, node);
}
document.body.removeChild(s);
};
document.body.appendChild(s);
}
Essentially, I compacted goJSONP and getDataForUrl into 1 function which writes the script tag (and removes it later) as well as not use the "unshift" function since that seemed to remove the server's response from the args array. So I just extract the data and call my callback with the arguments available. Another difference here is, I re-use the callback names, I might change that to completely unique names with a counter.
What's missing as of now is timeout handling. I'll probably start a timer and check for existence of the callback function. If it exists it hasn't removed itself so it's a timeout and I can act accordingly.
This is a year old now, but I think jfriend00 was on the right track, although it's simpler than all that - use a closure still, just, when specifying the callback add the param:
http://url.to.some.service?callback=myFunc('optA')
http://url.to.some.service?callback=myFunc('optB')
Then use a closure to pass it through:
function myFunc (opt) {
var myOpt = opt; // will be optA or optB
return function (data) {
if (opt == 'optA') {
// do something with the data
}
else if (opt == 'optB') {
// do something else with the data
}
}
}

Javascript callback function issue

Background
I'm writing an asynchronous comment system for my website, after reading plenty of tutorials on how to accomplish this I started building one from scratch. The comments are pulled using a JSON request and displayed using Javascript (jQuery). When the user adds a new comment it goes through the hoops and finally is sent via AJAX to the backend where it's added to the database. In the success section of the AJAX request I had the script empty the comments, then repull the new list (including the new post) and redisplay them.
Problem
While that was all nice, since it's making the page much shorter, then much longer it messes up where the user is viewing the page. I wanted to have it readjust the page back down to the end of the comment list (where the add comment form is). It also re-enables the add button, which was disabled when the clicked it to prevent impatient people from spamming.
$('#commentList').empty();
getComments('blog', $('input#blogId').val());
window.location = "#addComment";
$('#comAdd').removeAttr('disabled');
While this worked all well and good in theory, it seemed that the browser was getting ahead of itself and processing the window.location before the getComments function was done. So I read a little more and googled it and it seemed people were saying (for similar problems) to use callback functions, so I came up with this:
$('#commentList').empty();
getComments('blog', $('input#blogId').val(), function() {
window.location = "#addComment";
$('#comAdd').removeAttr('disabled');
});
This generates no javascript errors according to FireFox, but nothing within the callback function is working, it's not re-enabling the button nor changing the window.location.
Any ideas? Better ways to go about it? Do I have a glaring typo that I can't seem to see?
Thanks!
Update
I was under the impression the callback functions were a standard thing you could use.
function getComments(type, id)
{
$.getJSON("/ajax/"+type+"/comments?jsoncallback=&id="+id, function(data) {
for (var x = 0; x < data.length; x++)
{
var div = $("<div>").addClass("comment").appendTo("#commentList");
var fieldset = $("<fieldset>");
var legend = $("<legend>").addClass("commentHeader");
if ( data[x].url == "" )
{
legend.text((x+1) + ' - ' + data[x].name);
}
else
{
$("<a>").attr({href: data[x].url}).text((x+1) + ' - ' + data[x].name).appendTo(legend);
}
legend.appendTo(fieldset);
$("<div>").addClass("date").text(data[x].timestamp).appendTo(fieldset);
$("<p>").addClass("comment").text(data[x].content).appendTo(fieldset);
fieldset.appendTo(div);
}
});
}
This is called on document ready. Pulling all the comments and displaying them inside the #commentList div. When the user submits his/her comment it performs an AJAX request to a PHP script that adds the new comment to the database, upon success of this I have this:
$('#commentList').empty();
getComments('blog', $('input#blogId').val());
window.location = "#addComment";
$('#comAdd').removeAttr('disabled');
Deletes all the comments from the page.
Uses JSON to request the comments again (including the users new one).
Moves the page to the #addComment anchor, which is where their new comment would be displayed.
Re-enables the add comment button.
The problem is that the browser does the window.location line before the getComments function is done rendering all the comments, so as the page grows the user isn't looking anywhere near their new comment.
I expect here the problem is your getComments() function (for which more detail is required). You're supplying a third argument being a callback but does the function actually use a callback? What is it doing?
Certain jQuery functions provide callbacks but this isn't an automatic feature. If you're waiting for a user to type a comment you need to trigger the relevant event when they click "Done" or whatever they do.
Ok, try this:
function get_comments(type, id, callback) {
$.getJSON("/ajax/"+type+"/comments?jsoncallback=&id="+id, function(data) {
for (var x = 0; x < data.length; x++) {
var div = $("<div>").addClass("comment").appendTo("#commentList");
var fieldset = $("<fieldset>");
var legend = $("<legend>").addClass("commentHeader");
if ( data[x].url == "" ) {
legend.text((x+1) + ' - ' + data[x].name);
} else {
$("<a>").attr({href: data[x].url}).text((x+1) + ' - ' + data[x].name).appendTo(legend);
}
legend.appendTo(fieldset);
$("<div>").addClass("date").text(data[x].timestamp).appendTo(fieldset);
$("<p>").addClass("comment").text(data[x].content).appendTo(fieldset);
fieldset.appendTo(div);
if (typeof callback != 'undefined') {
callback();
}
}
});
}
Note: the difference here is that a third argument is supplied to get_comments() which is a callback that'll be called at the end of your $.getJSON() callback. That'll give you the proper ordering you want.
I might also suggest not constructing the HTML like that but including it in your page and hiding/unhiding it as necessary. It tends to be much more performant that dynamic HTML and have less issues (eg new HTML, unless you use $().live() will not have relevant event handlers).
Edit: Made the callback optional as per the comments. With the above code you can call the function without or without the callback.
Simple. Re-enable the button and go to the anchor after you receive the request and process the information. Like so:
function getComments(type, id)
{
// ADDED
$('#commentList').empty();
$.getJSON("/ajax/"+type+"/comments?jsoncallback=&id="+id, function(data) {
for (var x = 0; x < data.length; x++)
{
var div = $("<div>").addClass("comment").appendTo("#commentList");
var fieldset = $("<fieldset>");
var legend = $("<legend>").addClass("commentHeader");
if ( data[x].url == "" )
{
legend.text((x+1) + ' - ' + data[x].name);
}
else
{
$("<a>").attr({href: data[x].url}).text((x+1) + ' - ' + data[x].name).appendTo(legend);
}
legend.appendTo(fieldset);
$("<div>").addClass("date").text(data[x].timestamp).appendTo(fieldset);
$("<p>").addClass("comment").text(data[x].content).appendTo(fieldset);
fieldset.appendTo(div);
}
// ADDED
window.location = "#addComment";
$('#comAdd').removeAttr('disabled');
});
}
Personal opinion: rather than fetching all comments, why not fetch comments from a certain date? When you load the page, include a server time in the response. The Javascript uses this to query behind the scenes (to automatically check for new comments). The JSON response includes a new server time, which is used in the next response.
How would you handle deleted comments? Easy: have a deleted_on column in your database table, query it, and spit that out in the JSON response along with new posts.
Suggestion: instead of #addcomment, ID comments by timestamp.

Categories

Resources