views:

212

answers:

2

I think this issue goes beyond typical variable scope and closure stuff, or maybe I'm an idiot. Here goes anyway...

I'm creating a bunch of objects on the fly in a jQuery plugin. The object look something like this

function WedgePath(canvas){
    this.targetCanvas = canvas;
    this.label;
    this.logLabel = function(){ console.log(this.label) }
}

the jQuery plugin looks something like this

(function($) {
  $.fn.myPlugin = function() {

  return $(this).each(function() {

     // Create Wedge Objects
     for(var i = 1; i <= 30; i++){ 
      var newWedge = new WedgePath(canvas);
      newWedge.label = "my_wedge_"+i;
      globalFunction(i, newWedge]);
     } 
    });
  }
})(jQuery);

So... the plugin creates a bunch of wedgeObjects, then calls 'globalFunction' for each one, passing in the latest WedgePath instance. Global function looks like this.

function globalFunction(indicator_id, pWedge){

    var targetWedge = pWedge; 
    targetWedge.logLabel();

}

What happens next is that the console logs each wedges label correctly. However, I need a bit more complexity inside globalFunction. So it actually looks like this...

function globalFunction(indicator_id, pWedge){

        var targetWedge = pWedge; 

        someSql = "SELECT * FROM myTable WHERE id = ?";
        dbInterface.executeSql(someSql, [indicator_id], function(transaction, result){

            targetWedge.logLabel();

        })

    }

There's a lot going on here so i'll explain. I'm using client side database storage (WebSQL i call it). 'dbInterface' an instance of a simple javascript object I created which handles the basics of interacting with a client side database [shown at the end of this question]. the executeSql method takes up to 4 arguments

  • The SQL String
  • an optional arguments array
  • an optional onSuccess handler
  • an optional onError handler (not used in this example)

What I need to happen is: When the WebSQL query has completed, it takes some of that data and manipulates some attribute of a particular wedge. But, when I call 'logLabel' on an instance of WedgePath inside the onSuccess handler, I get the label of the very last instance of WedgePath that was created way back in the plugin code.

Now I suspect that the problem lies in the var newWedge = new WedgePath(canvas); line. So I tried pushing each newWedge into an array, which I thought would prevent that line from replacing or overwriting the WedgePath instance at every iteration...

wedgeArray = [];

// Inside the plugin...
for(var i = 1; i <= 30; i++){ 
    var newWedge = new WedgePath(canvas);
    newWedge.label = "my_wedge_"+i;
    wedgeArray.push(newWedge);
} 

for(var i = 0; i < wedgeArray.length; i++){
    wedgeArray[i].logLabel()
}

But again, I get the last instance of WedgePath to be created.

This is driving me nuts. I apologise for the length of the question but I wanted to be as clear as possible.

END

==============================================================

Also, here's the code for dbInterface object should it be relevant.

function DatabaseInterface(db){

 var DB = db;

 this.sql = function(sql, arr, pSuccessHandler, pErrorHandler){

  successHandler = (pSuccessHandler) ? pSuccessHandler : this.defaultSuccessHandler;
  errorHandler = (pErrorHandler) ? pErrorHandler : this.defaultErrorHandler;

  DB.transaction(function(tx){

   if(!arr || arr.length == 0){
    tx.executeSql(sql, [], successHandler, errorHandler);
   }else{
    tx.executeSql(sql,arr, successHandler, errorHandler)
   }

  });      
 }

 // ----------------------------------------------------------------
 // A Default Error Handler
 // ----------------------------------------------------------------

 this.defaultErrorHandler = function(transaction, error){
  // error.message is a human-readable string.
     // error.code is a numeric error code
     console.log('WebSQL Error: '+error.message+' (Code '+error.code+')');

     // Handle errors here
     var we_think_this_error_is_fatal = true;
     if (we_think_this_error_is_fatal) return true;
     return false;
 }


 // ----------------------------------------------------------------
 // A Default Success Handler
 // This doesn't do anything except log a success message
 // ----------------------------------------------------------------

 this.defaultSuccessHandler = function(transaction, results)
  {
      console.log("WebSQL Success. Default success handler. No action taken.");
  }    
}
A: 

i suspect your problem is the modifed closure going on inside globalFunction:

function(transaction, result){

        targetWedge.logLabel();

    })

read this

Andrew Bullock
I've spent a quite a bit of time trying to get my head around closures since I suspected myself that this would be the issue. I don't think I fully get it but my reasoning would be that by every time globalFunction is called, a new closure is created and whatever targetWedge is at the time is added to that closure, for that instance of that function call.But perhaps not.
gargantaun
again only a suspicion, but: as the `$(this).each()` loop iterates it creates a wedge variable, this gets passed down and is used in the callback function in globalFunction. the each() loop keeps iterating faster than the callbacks get called, which means on the 2nd each, the wedge reference has changed inside the first globalfunction callback
Andrew Bullock
+2  A: 

I would guess that this is due to that the client side database storage runs asynchronous as an AJAX call would. This means that it doesn't stops the call chain in order to wait for a result from the invoked method.

As a result the javascript engine completes the for-loop before running the globalFunction.

To work around this you could perform the db query inside a closure.

function getDataForIndicatorAndRegion(indicator_id, region_id, pWedge){ 
    return function (targetWedge) { 
        someSql = "SELECT dataRows.status FROM dataRows WHERE indicator_id = ? AND region_id = ?"; 
        dbInterface.sql(someSql, [indicator_id, region_id], function(transaction, result) {
            targetWedge.changeColor(randomHex());
        });
    }(pWedge);
}

This way you preserve pWedge for each execution. Since the second method is invoking it self and send what pWedge is right now as an argument.

EDIT: Updated the code from comments. And made a change to it. The callback function maybe shouldn't be self invoked. If it invoke it self the result of the function is passed as a argument. Also if it doesn't work, try passing the other arguments.

fredrik
This is so close I can taste it. I had to add "...})(targetWedge)" to the end of the anonymous onSuccess function for 'executeSql()' and it is indeed logging the correct label for the targetWedge. But two very strange things are happening now. Firstly console.log(result) generates an undefined error. Also, even though it's logging the wedge label (which suggests the anonymous function is being called), it's also calling the default onSuccess handler.Am I being really dense or is this a genuinely confusing issue?
gargantaun
First, do you mean if you do console.log(result) inside executeSql? Second could you edit you question with information about what you want to accomplish? What is the purpose of the Widgets?
fredrik
console.log(result) is called inside the onSuccess call back inside executeSql like this (actual code, so it looks a little different)`function getDataForIndicatorAndRegion(indicator_id, region_id, pWedge){ return function (targetWedge) { someSql = "SELECT dataRows.status FROM dataRows WHERE indicator_id = ? AND region_id = ?"; dbInterface.sql(someSql, [indicator_id, region_id], function(transaction, result){ targetWedge.changeColor(randomHex()); }(targetWedge) ); }(pWedge);}`
gargantaun
Please update the question with that code. Looks a bit fuzzy. But from what I can tell both indicator_id and region_id comes from the main function getDataForIndicatorAndRegion. You should add these also to the self invoking function. }(pWedge,indicator_id, region_id) to preserve them.
fredrik
sorry about that. I was trying to format that comment, but the comment formatting is useless and and the editing time expired.
gargantaun
Okey no worries. Didn't know the editing time could expire. But did it work if you preserved the other arguments?
fredrik
Sorry for the delay, busy day. Nope, that didn't seem to work. I think the original question has been answered by you (if you add my amend that I mentioned I'll accept your answer). I'm wondering wether the results stuff needs a separate question entirely? It feels like bad form to keep a question going like this.
gargantaun