11

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.
Sirko
  • 72,589
  • 19
  • 149
  • 183

4 Answers4

10

Your options are as follows:

  1. Have the server put the ID into the response. This is the cleanest, but often you cannot change the server code.
  2. 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.
  3. 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
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • That is, what I had in mind as a fallback plan. However this means creating (altough dynamically) multiple callback function, which I wanted to avoid. – Sirko Mar 05 '12 at 17:25
  • @Sirko - Unless you can guarantee that you will ONLY ever have one JSONP request of this type in flight at a time, you have to do something like this to match up the state of the request with the return. By definition JSONP is pretty dumb. All it does is call a global function. And, you've already nixed making the server solve this for you. So, you have to store the id in an accessible location. If you want to uniquely match up the right ID with the right function call and there can be multiple function calls at once, then each function call must be globally unique. – jfriend00 Mar 05 '12 at 17:34
  • @Sirko - if you only have one JSONP call in flight at a time (and you can guarantee that's the case), then just store the ID value in a global variable and retrieve it from your callback. This is a brittle way to do it because a subtle change in code from someone else down the road could easily break it when they accidentally put two JSONP calls in flight at the same time and figuring out the problem at the time will be a mess. – jfriend00 Mar 05 '12 at 17:35
  • FYI, I know that YUI does something like this for it's JSONP processing in order to keep state attached to a given JSONP call. It's a bit ugly (JSONP is a giant hack in the first place), but the ugliness only has to be implemented once and then it's clean to use. – jfriend00 Mar 05 '12 at 17:39
  • @Sirko - I've summarized your options at the beginning of my answer. – jfriend00 Mar 05 '12 at 18:00
1

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.

user3805007
  • 61
  • 1
  • 1
0

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.

stackmagic
  • 164
  • 8
-1

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
        }
    }
}
JeB
  • 3
  • 4