Callbacks with parameters - javascript

I have jQuery function:
$('#fetch').click( function() {
...
GetPage( curPage, items, DrawItemSet( items ) )
});
As you can see, I want the callback function DrawItemSet be executed from inside GetPage function. Sometimes, I need to call not DrawItemSet but some other functions.. I just want to any function I need to be called from GetPage function. It is necessary to do some afterwork after (as you may guesses) I get the page. In this example I want to display some data. In other cases I want not to display some text, but, for example, swap information got from GetPage function. In that case, my idea was to write something like:
...
GetPage( curPage, items, SwapItemSet( oldItems ) )
As fo now, everything seems to be ok.
Here's my GetPage listing:
function GetPage( pageNum, collection, callback ) {
console.log( 'Starting to get page number ' + pageNum )
$.ajax({
url: 'http://www.xxxxxxxxxxx.php?page=' + pageNum,
type: 'get',
dataType: '',
success: function(data) {
...
console.log( 'Curpage=' + curPage + ' MaxPage=' + maxPages )
if( curPage < maxPages ) {
curPage = curPage + 1
GetPage( curPage, collection, callback )
}
else {
console.log( 'passed' )
callback()
}
}
});
}
As you can see this function calls itself while there are some pages left to fetch, and if there's no page left, the callback function should be called to do some stuff. The callback example you can see below (just for testing):
function DrawItemSet() {
console.log( 'DrawItemSet called.' )
...
}
My problem is that in theory everything looks ok, and I should get in the console DrawItemSet as the last message meaning that the callback function was called after the fetching is being finished. Well.. instead of it I get the following:
DrawItemSet called.
Starting to get page number 1
Curpage=1 MaxPage=2
Starting to get page number 2
Curpage=2 MaxPage=2
passed
And this means that somehow the callback is the first function being executed.
How so?!

When you write DrawItemSet( items ) you are calling DrawItemSet immediately and then passing it's return value in as callback. That is why it is called first. You are calling it before GetPage is even called.
There are a few ways to pass it in as a callback with arguments. The most common approach is to pass in an anonymous function like so:
GetPage( curPage, items, function(){ DrawItemSet( items ) } );
In modern browsers you can also use bind:
GetPage( curPage, items, DrawItemSet.bind(null, items) );
Or you can use the jQuery version of bind called proxy:
GetPage( curPage, items, $.proxy(DrawItemSet, null, items) );

Related

WeakMaps and AJAX Calls

While putting to practice what I've learned so far about ES2015 with Babel, specifically about WeakMaps, I came upon a problem I don't know why it's not working.
I have a WeakMap defined to house data coming from an AJAX call that's only triggered if the value of said WeakMap is undefined.
This is what I came up to:
class User {
constructor( id ) {
id = Number( id );
if( id <= 0 || isNaN( id ) ) {
throw new TypeError( 'Invalid User ID' );
}
_id.set( this, id );
}
getID() {
return _id.get( this );
}
getData() {
let _this = this;
if( _data.get( _this ) === undefined ) {
_this.loadData().done( function( data ) {
// JSON is indeed successfully loaded
console.log( data );
_data.set( _this, data );
// WeakMap is indeed set correctly
console.log( _data.get( _this ) );
});
}
// But here it's undefined again!
console.log( _data.get( _this ) );
return _data.get( _this );
}
loadData() {
return $.get({
url: '/users/' + _id.get( this, data ),
});
}
}
let _id = new WeakMap;
let _data = new WeakMap;
// ---------------
var user = new User( 1 );
console.log( user.getID(), user.getData() ); // 1 undefined
As far as i know, I am setting the WeakMap data correctly, as the User ID is being set and can be retrieved, but the User Data coming from AJAX, although is indeed being set inside the jQuery.done() can't be accessed outside of it.
what i'm doing wrong?
I don't understand JavaScript to the point saying this is the right solution or not but I've searched A LOT, reading countless questions here in Stack Overflow with immense answers that fails to put things ins simple ways so, for anyone interested:
class User {
constructor( id ) {
id = Number( id );
if( id <= 0 || isNaN( id ) ) {
throw new TypeError( 'Invalid User ID' );
}
_id.set( this, id );
}
getID() {
return _id.get( this );
}
getData() {
if( _data.get( this ) === undefined ) {
_data.set( this, this.loadData() );
}
return _data.get( this );
}
loadData() {
return $.getJSON( CARD_LIST + '/player/' + _id.get( this ) );
}
}
let _data = new WeakMap;
// ---------------
var user = new User( 1 );
user.getData().done( function( data ) {
console.log( data );
})
It's not what I had in mind initially and I don't have knowledge to explain the "whys" but, at least this is a palpable working example that I humbly hope that will helps someone else who's trying to extract info from extremely long answers and/or unhelpful/unguided comments.
See How do I return the response from an asynchronous call? for a (lengthy) primer of how to use the result of an asynchronous operation like Ajax.
The short answer is: you cannot use an asynchronously-fetched result if you're synchronously returning a value. Your getData function returns before any Ajax call resolves, so the synchronous return value of getData cannot depend upon any asynchronously-fetched value. This isn't even a matter of "waiting" long enough: the basic principle of JavaScript event handling is that the current function stack (i.e., the current function, and the function that directly called that function, etc.) must resolve entirely before any new events are handled. Handling an asynchronous Ajax result is a new event, so your done handler necessarily will not run until well after your getData function execution is ancient history.
The two general ways to solve your issue are:
Make the Ajax call synchronous
Make the Ajax-fetched value accessible asynchronously outside of getData
You can do #2 by either passing a callback into getData, like
getData(funnction(data) { console.log("If this is running, we finally got the data", data); }
function getData(callback) {
_this.loadData.done(function(data) {
_data.set(this, data);
// etc. etc.
callback(_data.get( _this ));
});
}
So, now the getData is itself asynchronus, does this force you to rewrite any use of `getData you already have, and cause propagation of asynchronous patterns all over your code? Yes, it does.
If you want to use return a promises instead, you could do
getData().then(function(data) {
console.log("Got the data", data);
});
function getData() {
return _this.loadData().then(function(data) {
_data.set(this, data);
// etc. etc.
return _data.get( _this );
});
}
this works because promises allowing chaining then calls. This simply chains then inside and outside the get data function: one then callback is queued inside getData, and the next is queued outside getData. The return value of the first callback is used as the argument of the second. The return value of then is the original promise, so I've simply used the form return mypromise.then(...), which returns the same object as if I'd just done return mypromise (but obviously I haven't set up a callback in that case).

Passing Params to a Window[callback] Function

I have some code that requests some JSON from an API. When data is returned, per the documentation, it sends back a callback function that is to be used to parse the data at the top level. After the call is made, I have the following code to capture the data and process it:
var callback = 'functionUsedInApiCall';
window[callback] = newCallBackFunction;
How would I go about passing custom params to the callback function above as the data is being returned?
In order to capture the data, I must write the callback function like this:
function newCallBackFunction(root) {
//root is the data
}
Any help would be greatly appreciated.
Are you talking about JSONP? If so, you don't call the callback or pass in the argument at all, the code returned by the API does.
E.g., your code:
window.myCallback = newCallbackFunction;
function newCallbackFunction(data) {
// use the data
}
(I'm assuming this isn't at global scope, hence assigning to the window object.)
...plus your code for initiating the JSONP call, which is usually appending a script element to your page with a URL containing the name of the callback ("myCallback" in the above).
Their response will look like this:
myCallback({
// data here
});
...which, when it arrives, will run (because it's the content of a script element), and will call your function. This is how JSONP works.
If you want to include further arguments for the function, all you do is have the callback they call turn around and call your target function, e.g.:
window.myCallback = function(data) {
newCallbackFunction(data, "foo", "bar");
};
function newCallbackFunction(data) {
// use the data
}
Now when their code calls the global myCallback, all it does is turn around and call newCallbackFunction with that data and the arguments you specify.
Those arguments don't have to be literals as in the above. Here's an example with a bit more context, using a closure:
// Assume the url already contains the name of the callback "myCallback"
function doJSONP(url, niftyInfo, moreNiftyInfo) {
var script;
// Set up the callback
window.myCallback = function(data) {
// Runs when the data arrives
newCallbackFunction(data, niftyInfo, moreNiftyInfo);
};
// Trigger the request
script = document.createElement('script');
script.src = url;
document.documentElement.appendChild(script);
}
Ideally, though, when doing JSONP you auto-generate the name of the callback each time so that it's specific to the request (in case you have two outstanding requests at the same time):
// Assume the url ends with "callback=" and we append the name of the
// callback function to it
function doJSONP(url, niftyInfo, moreNiftyInfo) {
var cbname, script;
// Get a callback name
cbname = "callback_" +
new Date().getTime() +
"_" +
Math.floor(Math.random() * 10000);
// Set up the callback
window[cbname] = function(data) {
// Remove us from the window object
try {
delete window[cbname];
}
catch (e) { // Handle IE bug (throws an error when you try to delete window properties)
window[cbname] = undefined;
}
// Runs the function
newCallbackFunction(data, niftyInfo, moreNiftyInfo);
};
// Trigger the request
script = document.createElement('script');
script.src = url + encodeURIComponent(cbname);
document.documentElement.appendChild(script);
}
Parameters in javascript are passed as an Array, so you can pass the parameters you need, or even complete functions that will add per case functionality in your callback.
You could do the following:
function newCallBackFunction(root /*your data*/, paramsHash /*a hash array with optional parameters*/)
{
//if arg1 can be found in the hash
if( paramsHash['arg1'] ]
{
//do something that requires arg1
}
else if( paramsHash['arg2'] )
{
//do something that requires arg2
}
return root;
}
And in your main code:
var hash = new Array();
hash['arg1'] = 'str1';
hash['arg2'] = 1;
hash['arg3'] = new Car(); //or any other object you want
It is also possible to just declare some parameters and supply them to your function only when needed:
function newCallBackFunction(root, param1, param2)
{
if( param1 ) { /* similar to first example */ }
}
Or finally just pass whatever parameter you want and read them from the arguments table
function newCallBackFunction(root)
{
for( int i = 1; i < arguments.length; i++ )
//do something with the parameters you pass beside root
}
And in main code:
newCallBackFunction( root, param1, param2, param3 );
I hope I covered you!

What is the purpose of "self.each(callback, array)" in the jQuery source code?

jQuery's .each function takes exactly one argument- a function. And yet, In this piece of jQuery code, we see the following:
if ( callback ) {
self.each( callback, [ responseText, status, jqXHR ] );
}
Two args are being passed to .each. I'm assuming the values in brackets are params to the callback function, but I'm not clear on why this is possible and why someone would do this rather than calling the function directly? :
if ( callback ) {
self.each( callback(responseText, status, jqXHR) );
}
"I'm not clear on why this is possible and why someone would do this..."
They wouldn't. It's for internal use only: https://github.com/jquery/jquery/blob/1.6.2/src/core.js#L248-253
// Execute a callback for every element in the matched set.
// (You can seed the arguments with an array of args, but this is
// only used internally.)
each: function( callback, args ) {
return jQuery.each( this, callback, args );
},
This behavior could change at any time.
To be clear, normally your callback to each gets two arguments:
the i (property name or index) of the collection as the first
the value of the item at i as the second
But sometimes internally they want to use the each iterator, but they have no need for those arguments, and instead they want to substitute their own.
That's what's happening. You can see here that if the internal args property has been given a value, they do a slightly different iteration of the collection passing the args that were given.
So they do:
callback.apply( object[ name ], args )
...instead of:
callback.call( object[ name ], name, object[ name ] )
...or slightly something different but effectively the same for an Array-like collection.
You can test it on your own:
// normal usage
$('p').each(function( a, b, c ) {
// will show the index, the element, and undefined for each "p" element
console.log( a, b, c );
});
// internal usage
$('p').each(function( a, b, c ) {
// will show 1, 2, 3 once for every "p" element
console.log( a, b, c );
}, [ 1, 2, 3 ] );
But again, this behavior isn't for public use, and could change without warning.
RightSaidFred has answered part of this question – how the each call works. Here's why it's needed.
if ( callback ) {
self.each( callback(responseText, status, jqXHR) );
}
That code won't work because it will call callback once, and then use the result of that as the argument of the each call. It is the approximate equivalent of
callback(responseText, status, jqXHR);
self.each ( true );
The next option is to do this with an anonymous function:
self.each (function() {
callback(responseText, status, jqXHR);
});
This will work fine. However you have missing functionality. Your callback is run the correct number of times, but there is nothing to differentiate each iteration. You might as well do this:
for (var i = 0; i < self.length; i++) {
callback(responseText, status, jqXHR);
}
By using each as the original code does, when callback is called, the relevant element in the array is used as the "context" – in practical terms, the value of this within the function. This means that the code can differ based on which element in the selection was passed.

Passing a callback and other parameters to jQuery ajax success?

I wrote a function which I'm allowing a callback to be passed to. I then want to execute that with both the ajax repsonse, and another parameter.
My problem is the the callback is executed, and when I step through the code, it looks like the function is called with the proper parameters, however when actually stepping in to the callback function, the first parameter takes the value of whatever I assign to the second argument, and the second argument is undefined.
Here is my function:
namespace = {
fetch : function(sr, count, callback) {
srCount = count ? count : '25'
var sub;
if (sr == 'frontpage'){
sub = '';
}else{
sub = 'foo/' + sr + '/';
};
$.ajax({
url: "http://www.example.com/"+sub+ ".json?count=" + count,
dataType: "json",
success: function(msg)
{
callback.call(msg, count)
}
})
};
Now, when I call it like this:
mynamespace.fetch($(this).attr('href'), 25, true, another namespace.createPost);
I would expect callback.call(msg, count) to resolve to callback.call(/*the ajax response*/, 25);
However, when I run it, I get msg == 25 and count == 'undefined'. I'm at a loss for why...
.call calls a function with explicit context given by the first argument, so callback.call(msg, count) calls the callback function with msg set as context (the this value inside the callback function for this call) and count as a first argument.
So you probably want callback( msg, count ) or callback.call( namespace, msg, count ); which means that this inside the callback will refer to namespace for that call.
$.ajax() function has a context parameter, which you can use for this purpose.
$.ajax({
url: "http://www.example.com/"+sub+ ".json?count=" + count,
context: "hello",
dataType: "json",
success: function(msg)
{
// here 'this' will be the 'hello' you specified for context while making the call.
callback.call(msg, count)
}
})

How do I wrap executions of asynchronous (callback-based) functions into a synchronous function in Javascript?

I'm trying to write a function in Javascript (with jQuery, if you want):
function fetchItem(itemId) { return /* ??? */; }
This function relies on a second, predefined and unmodifyable function that looks like this:
function load(callback) { /* ... */ }
This function is asynchronous. After calling it, it fetches n items via XHR, then when they have arrived, stores them in the DOM, then invokes the callback.
fetchItem uses a simple jQuery selector (irrelevant here) to check the DOM for the element with itemId and calls load if the item isn't there yet. Rinse and repeat.
My problem is that I want to wrap multiple asynchronous calls of load into my synchronous fetchItem function, which should return the DOM element with itemId after it has made enough load calls.
Pseudo code, if load was synchronous:
function fetchItem(itemId):
while not dom.contains(itemId):
load()
return dom.find(itemId)
My first attempts at doing this in Javascript, which probably display a lot of misconceptions about Javascript's closures and execution model: ;)
function fetchItem(itemId) {
var match = undefined;
function finder() {
match = $(...).get(0);
if(!match) {
load(finder);
}
}
finder();
return match;
}
Obviously, this fails because the return is executed before the first callback. Also, as you can see I had some problems getting match back out to fetchItem. Is it properly protected by the closure here? Would this work if fetchItem was executed multiple times in parallel, assuming that load supports this (and doesn't mix up the DOM)?
I'm probably missing a perfectly good pattern here, but I don't really know what to google for...
You need to make fetchItems async too and provide it a callback, something like this should probably work (warning untested!):
function fetchItems(itemIDS, callback, matches) {
if (!matches) { // init the result list
matches = [];
}
// fetch until we got'em all
if (itemIDS.length > 0) {
var id = itemIDS[0]; // get the first id in the queue
var match = $(id).get(0);
// not found, call load again
if (!match) {
load(function() {
fetchItems(itemIDS, callback, matches);
});
// found, update results and call fetchItems again to get the next one
} else {
matches.push(match); // push the current match to the results
itemIDS.shift(); // remove the current id form the queue
fetchItems(itemIDS, callback, matches);
}
// we have all items, call the callback and supply the matches
} else {
callback(matches);
}
}
fetchItems(['#foo', '#bar', '#test'], function(matches) {
console.log(matches);
})
I would simply gave your fetchItem function as a callback to load. Like this:
function fetchItem(itemId, callback):
if not dom.contains(itemId):
load(fetchItem)
else:
callback(dom.find(itemId))
callback() is a function that does rest of the job when necessary element appears in the DOM.
That is impossible. You cannot create synchronousness from asynchronousness. Why do not you add a callback to your fetchItem-function as well?
Seems like everybody agrees that I need to introduce my own callback, so here's my (so far final) working solution:
var MAX_FETCH_MORE = 3;
/*
* Searches for itemId, loading more items up to MAX_FETCH_MORE times if necessary. When
* the item has been found or the maximum reload count has been reached, the callback
* is invoked, which is passed the DOM object of the item wrapped in a jQuery object, or
* undefined.
*/
function executeWithItem(itemId, callback, fetchCycleCounter) {
// initialize fetchCycleCounter on first iteration
if(!fetchCycleCounter) fetchCycleCounter = 0;
console.debug('iteration ' + fetchCycleCounter + '/' + MAX_FETCH_MORE);
// try to find the item in the DOM
match = $('div[data-item-id="' + itemId + '"]').get(0);
if(match) {
// if it has been found, invoke the callback, then terminate
console.debug('found: ' + match);
callback($(match));
} else if(!match && fetchCycleCounter < MAX_FETCH_MORE) {
// if it has not been found, but we may still reload, call load() and pass it
// this function as the callback
console.debug('fetching more...');
load(function() {executeWithItem(itemId, callback, fetchCycleCounter+1);});
} else {
// give up after MAX_FETCH_MORE attempts, maybe the item is gone
console.debug('giving up search');
}
}
// example invocation
executeWithItem('itemA01', function(item) {
// do stuff with it
item.fadeOut(10000);
});
Thanks to everybody for encouraging me to introduce another callback, it hasn't turned out looking so bad. :)

Categories

Resources