1

I have a web applciation based on apache, php, js/jquery. One module of the application is initialized when the document is loaded. That sequence calls an init() funcion that performs a number of tasks, amongst others it fetches two html dialogs via ajax and inserts them into the existing page. A reference to those dialogs is saved as variables, so that I don't have to specify jquery selectors throughout my scripts but can simply use those variables instead. This work fine, except that there are very few cases (now and then...) when the variables are 'undefined' when trying to bind a handler to an element inside the fetched dialogs. Which is odd, because apart from that bind the dialogs are usable throughout the application. This suggests that the ajax calls succeed, but aparently there is some kind of race condition, so that now and then the variables are only set after the bind attempt.

In my understanding this should not happen, since the bind is done in the .done() part of a when() and should only be executed after the ajax retrieval of the dialogs has finished inside the when().

Aparently I am missing something fundamental here. Anyone got a hint for me ?

The following cites code excerpts from the working implementation. They may appear syntactically invalid as some parts, but that is just due to the removal of code parts not relevant here. The unshortened code works fine.

The variables:

Shorty.Tracking.Dialog.List:{};
Shorty.Tracking.Dialog.Click:{};

The initializing sequence:

$(window).load(function(){
  //...
  var dfd = new $.Deferred();
  $.when(
    // load layout of dialog to show the list of tracked clicks
    Shorty.Tracking.init()
  ).done(function(){
    // bind actions to basic buttons
    Shorty.Tracking.Dialog.List.find('#close').on('click',function(){
      // ...
    });
    // ...
  });
  // ...
})

The shortened init function:

Shorty.Tracking.init:function(){
    // ...
    var dfd=new $.Deferred();
    // two dialogs are used by this plugin
    var dialogs={
      'list':  Shorty.Tracking.Dialog.List,
      'click': Shorty.Tracking.Dialog.Click
    };
    // load dialogs from server
    $.each(['list','click'],function(i,dialog){
      if (...){
        // ...
        dfd.resolve();
      }else{
        // load dialog layout via ajax and create a freshly populated dialog
        $.ajax({
          type:     'GET',
          url:      OC.filePath('shorty-tracking','ajax','layout.php'),
          data:     { dialog: dialog},
          cache:    false,
          dataType: 'json'
        }).pipe(
          function(response){return Shorty.Ajax.eval(response)},
          function(response){return Shorty.Ajax.fail(response)}
        ).done(function(response){
          // create a fresh dialog
          // insert it alongside the existing dialogs in the top controls bar
          $('#controls').append(response.layout);
          switch (dialog){
            case 'list':
              Shorty.Tracking.Dialog.List=$('#controls #shorty-tracking-list-dialog').first();
              break;
            case 'click':
              Shorty.Tracking.Dialog.Click=$('#controls #shorty-tracking-click-dialog').first();
          } // switch
          dfd.resolve();
        }).fail(dfd.reject)
      } // else
    }); // each
    return dfd.promise();
  },

With the answer of Bergi I managed to apparently remove the original problem. Until now I could not detect any more failed bind attempts. However I could not follow that suggestion completely: in his answer he suggested to remove the switch statement in the initialization method in favor of a direct assignment. This certainly is much more elegant, but that won't work. I have the impression there is some missunderstanding on my side about how javascript handles references and/or functions stored in variables.

Maybe you, Bergi, or someone else can shed some explaining light on this:

This is the modifed initialization method from above:

  init:function(){
    if (Shorty.Debug) Shorty.Debug.log("initializing tracking list");
    // check if dialogs already exist
    if (   $.isEmptyObject(Shorty.Tracking.Dialog.List)
        && $.isEmptyObject(Shorty.Tracking.Dialog.Click) ){
      // two dialogs are used by this plugin
      var dialogs={
        'list':  Shorty.Tracking.Dialog.List,
        'click': Shorty.Tracking.Dialog.Click
      };
      // load dialogs from server
      var dfds=$.map(dialogs,function(obj,dialog){
        // load dialog layout via ajax and append it to the collection of dialogs in the controls
        return $.ajax({
          type:     'GET',
          url:      OC.filePath('shorty-tracking','ajax','layout.php'),
          data:     { dialog: dialog},
          cache:    false,
          dataType: 'json'
        }).pipe(
          function(response){return Shorty.Ajax.eval(response)},
          function(response){return Shorty.Ajax.fail(response)}
        ).done(function(response){
          // create a fresh dialog and insert it alongside the existing dialogs in the top controls bar
          $('#controls').append(response.layout);
//           obj=$('#controls #shorty-tracking-'+dialog+'-dialog').first();
          switch(dialog){
            case 'list':
              Shorty.Tracking.Dialog.List=$('#controls #shorty-tracking-list-dialog').first();
              break;
            case 'click':
              Shorty.Tracking.Dialog.Click=$('#controls #shorty-tracking-click-dialog').first();
              break;
          } // switch
        })
      }) // map
      return $.when.apply(null, dfds);
    }else{
      // dialogs already loaded, just clean them for usage
      Shorty.Tracking.Dialog.List.find('#list-of-clicks tbody tr').remove();
      new Deferred().resolve();
    } // else
  },

In the middle you see the assignment statement commented out and currently replaced by the switch statement below. I tried several variations, like obj.element and so on but failed. Outside the function the variables Shorty.Tracking.Dialog.List and Shorty.Tracking.-Dialog.Click remain empty.

I am an absolute newbie to web stuff ans js/jquery. but I am certainly curious to learn more about this way of handling references.

arkascha
  • 41,620
  • 7
  • 58
  • 90

1 Answers1

2

That's the problematic code:

var dfd=new $.Deferred();
$.each(['list','click'], function(){
    ...
    dfd.resolve/fail();
});

You create only one Deferred, but resolve it multiple times. So when the first resolution happens, the second Ajax would not have been finished.

So, instead use two Deferreds, and combine them through jQuery.when(). Also, I don't think you will need to create Deferreds yourself. Just take those two you get from the pipe() calls.

function init() {
   var dialogs={
      'list':  Shorty.Tracking.Dialog.List,
      'click': Shorty.Tracking.Dialog.Click
   };
   var deferreds = $.map(dialogs, function(obj, dialog) {
       return $.ajax(...).pipe(...).done(...
           // really, don't use switch here:
           obj.element = response.layout;
       }); // done() returns the Deferred
   });
   return $.when.apply(null, deferreds);
}

...

$.ready(function($) {
    init().done(...);
});

OK, I need to explain about the switch-Statement. It's true, the values in the dialogs object will be assigned from [uninitialized] variables and hold that value (undefined), when the fields are reassigned the Shorty values won't change. There are no "pointers" in Javascript. I thought only it would be useful to use the current dialog variable in a member operator, instead of a switch statement. To assign to Shorty.Tracking.Dialog.List and .Click, you'd need to use something like

var dialogs = ["list", "click"];
var shortcut = Shorty.Tracking.Dialog; // some object
for (var i=0; i<dialogs.length; i++) {
    // loop over them,
    // do the ajax stuff
    var element = $(repsonse.layout); // get the added element from the response
    $('#controls').append(element);

    shortcut[dialogs[i]] = element;
}

(eventually use a snippet from How do I make the first letter of a string uppercase in JavaScript?)

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • oops, indeed, you are right. I did not see that! Thanks. I will give it a try right now and keep an eye on that. Sounds reasonable that this is the cause of the problem. – arkascha Jul 28 '12 at 20:00
  • Ok, after quite some time I got this working, party. First of all thanks for pointing out that stupid mistake. However there are things left I don't understand and fail to solve more elegant. I appended the current state of the initialization function to the original question, along with what is still puzzling me. Maybe you could take a look again ? Tanks ! – arkascha Jul 29 '12 at 10:46