0

I just want to do three things in order.

My table has thousands of rows and takes a while to sort. I would like my "waiting" modal to display during the sort, but the page waits for the sorting to complete before attempting to display the modal.

Here's a jsfiddle. (You can increase or decrease bignumber to get a sorting delay you can see but isn't annoying.)

I first tried:

//CSS:   body.loading .waitingModal {display: block;}

$('body').toggleClass("loading");         // happens second!
sortTable(currentdirection,columnno);     // takes ages and happens first
$('body').toggleClass("loading");         // happens third

If I comment out the last line or cause an error before it, the table sorts, then the modal appears.

Thanks to this question, my next attempt worked once but then the loading modal never came again:

$('body').toggleClass("loading");   
setTimeout(function() {           //loading works but not if you click again
    sortTable(order,1);
    $('body').toggleClass("loading");
}

Web pages advising people suggest I should use jQuery.Deferred rather than entangle myself in callback hell, and since I couldn't see where to put a callback and callback hell doesn't sound fun I went along with it.

I read questions on How to use jQuery deferred and then vs done and most promisingly, this answer's Example 1 of chaining, but everyone seems to assume I have a deferred/promise already turning up from some ajax call or something.

My first attempt to chain some .then(...) calls didn't work, because literally nothing happened:

$.Deferred().then(function(){
    $('body').toggleClass("loading");    //doesn't happen
}).then(function(){
    sortTable(order,1);                  //doesn't happen
}).then(function(){
    $('body').toggleClass("loading");    //doesn't happen
});

Using the beforeStart parameter got my modal displayed, but nothing else:

$.Deferred(function(it){
    $('body').toggleClass("loading");    //happens first, hooray!
}).then(function(){
    sortTable(order,1);                  //doesn't happen
}).then(function(){
    $('body').toggleClass("loading");    //doesn't happen
});

I then tried to chain some deferred thens, resolving and passing the deferred explicitly, but I clearly don't understand how this works:

$.Deferred().then(function(it){
    $('body').toggleClass("loading");    //still happens second
    it.resolve();
    return it.promise();
}).then(function(){
    sortTable(order,1);                  //happens first
}).then(function(){
    $('body').toggleClass("loading");    //happens third
});

Lastly, I've tried resolving a deffered on the change event on the body

function loadingNow(){
    var deferred = $.Deferred();
    $('body').toggleClass("loading")     // happens
      .change(function(){
        deferred.resolve()
    });
    return deferred.promise();
}            
loadingNow().then(function(){
    sortTable(order,1);                  //doesn't happen
}).then(function(){
    $('body').toggleClass("loading");    //doesn't happen
});

Doesn't matter if I toggle the class after adding the change event handler:

function loadingNow(){
    var deferred = $.Deferred();
    $('body').change(function(){
        deferred.resolve()
    }).toggleClass("loading");    // happens
    return deferred.promise();
}            
loadingNow().then(function(){
    sortTable(order,1);                  //doesn't happen
}).then(function(){
    $('body').toggleClass("loading");    //doesn't happen
});

Honestly, I didn't expect it to be this difficult to persuade jQuery to do three things in order. Perhaps there's a much simpler thing I haven't thought of. I don't mind if it's callbacks or Deferred done right, but if it's brief and relatively clear that would be a huge bonus.

some guy
  • 23
  • 1
  • 5

1 Answers1

1

You almost had it. The big issue is that listening for the change event on <body> does not trigger when a class changes. That event is for form elements like text inputs.

Your logic will work with a listener that triggers on class changes, like DOMSubtreeModified, like so:

function loadingNow(){
   var deferred = $.Deferred();
   $('body').one('DOMSubtreeModified', function(){
       deferred.resolve()
   }).toggleClass("loading");
   return deferred.promise();
}           

$("#sortButNoLoading").click(function(){
   order *= -1;
   loadingNow().then(function() {
      sortTable(order,1);   
      $('body').toggleClass("loading"); //happens third
   })
})

Notes for proper implementation:

  1. The DOMSubtreeModified will fire any time anything changes anywhere on <body> or below, so make sure you stop listening for that once you fired the function in response to that. You can use $().one() instead of $().on() to automatically destroy the listener after it's fired once.
  2. As per this comment, DOMSubtreeModified is deprecated and you should migrate to something else.
  3. See the "working" fiddle here.
Bryce
  • 6,440
  • 8
  • 40
  • 64
  • Thank you very much indeed. Unfortunately, like my setTimeout version in [an updated jsfiddle](http://jsfiddle.net/pxworys0/14/), this works once, but on subsequent sorts the loading doesn't show again. I tried changing `one` to `on`, in your jsfiddle but that didn't help. – some guy Jun 23 '19 at 03:14
  • [This version of your fiddle seems to work every time](http://jsfiddle.net/hsdq1tb3/) – Bryce Jun 23 '19 at 04:46
  • Ah drat. That does indeed work in Chrome but fails subsequent times in Firefox which is my main browser. Here's me thinking that libraries like jQuery eliminated cross-browser incompatabilities. – some guy Jun 23 '19 at 12:49