Sunday, February 1, 2009

Scriptaculous and AJAX

I started with a task which seemed to be typical when script.aculo.us is used with Prototype Ajax.Request. The old content nicely disappears with one of scriptaculous effects, AJAX request is sent and when result is available it appears with another effect. Let's use Effect.SlideUp and Effect.SlideDown for these effects, and <div id='main_div'> for the content. Straight-forward solution looks like this:
new Effect.SlideUp('main_div', {
      afterFinish: function () {
        new Ajax.Request(url, {
            method:'get',
            onSuccess: function(transport){
              $('main_div').innerHTML=transport.responseText;
              new Effect.SlideDown('main_div');
            }
          })
      }
    });
Failure handling is omitted for brevity. This solution works, but has a significant problem: the request is sent only after the slide up effect is finished, so the user waits more than necessary. I wanted to send the AJAX request immediately, so the response might be ready when the slide up is finished. But it's impossible to know which will finish first.
My next try was to start slide up and immediately send the request, like this:
    new Effect.SlideUp('main_div');
    new Ajax.Request(url,
    {
        method:'get',
        onSuccess: function(transport){
             $('main_div').innerHTML=transport.responseText;
             new Effect.SlideDown('main_div', {queue: 'end'});
        }
    });
Unfortunately, it did not work correctly. If the response comes before the slide up vanished the main_div, it will be replaced with the new content for a moment, then disappear and come nicely with the slide down effect. So the problem here is that replacing the content and slide down must start only when both slide up and the AJAX are finished. I ended up with the following:
    var hideEffectComplete=false;
    var ajaxResult=null;
    new Effect.SlideUp('main_div', {
        afterFinish: function () {
            complete=true;
            if (ajaxResult != null) {
                $('main_div').innerHTML=ajaxResult;
                new Effect.SlideDown('main_div');
            }
        }
    });
    new Ajax.Request(url,
    {
        method:'get',
        onSuccess: function(transport){
          ajaxResult=transport.responseText;
          if (hideEffectComplete) {
             $('main_div').innerHTML=ajaxResult;
             new Effect.SlideDown('main_div');
          }
        }
    });
It works better, but it's long, ugly and redundant. Also it makes problems when a user makes a few actions fast, things are just messed up. Does anybody have a better idea how to synchronize AJAX and script.aculo.us?

7 comments:

Anonymous said...

I think it might work better to do the hide effect only if the ajax request was successful, such as:

new Ajax.Request(url,
{
method:'get',
onSuccess: function(transport){
var ajaxResult=transport.responseText;
new Effect.SlideUp('main_div', {
afterFinish: function () {
$('main_div').update(ajaxResult);
new Effect.SlideDown('main_div');
}
});
}
});

In my experience using effects with ajax in this way still leaves much to be desired though, since there is a time delay that the user doesn't know what's going on and may continue to 'click around'.

Much easier on the user (and the developer!) would be to show the ol' loading gif as a modal overlay over the screen before the call and hide it when it's done. You can see an example of what I mean if you do a search on mapquest. This should prevent continual clicking and messing up your effect queues.

Andrew Skiba said...

The loading gif animation can be done very reliably, I agree with you. I tried to create something that tricks the user to not notice the requests at all, but as you see it has to be polished yet.

Speaking of the code example, for me it looks more natural to not hide failed request. Except some really optional requests, like google suggest. If you go to inbox on GMail and the request fails, it does not pretend you did not click the button but tells about connection problems. What do you think?

Anonymous said...

My bad, I didn't really mean to make it look as if you would do nothing on a failed request, since these are snipets and not full code blocks sometimes I exclude things that I think should always be there. In the case of a failed request you can use the onFailure callback (the full request lifecycle is explained here better than I ever could: http://www.prototypejs.org/api/ajax/request). Using the above example:


new Ajax.Request(url,
{
method:'get',
onSuccess: function(transport){
var ajaxResult=transport.responseText;
new Effect.SlideUp('main_div', {
afterFinish: function () {
$('main_div').update(ajaxResult);
new Effect.SlideDown('main_div');
}
});
},
onFailure: function(){
//failure handling
alert('ajax request failed');
}
});

I'm not sure you'd ever reliably hide the request from all users since everyone has different connection speeds. These types of effects are more geared toward content that is already in the DOM so that the only bottleneck is the speed of the users hardware.

Andrew Skiba said...

I guess you are right, scriptaculous was designed to manipulate already available content.

I care about 90% of the situations when the server responses reasonably fast. In these cases, transition effects can hide the interaction with the server quite successfully. For long responses, I'll fall back to GIF animation as you suggested.

I hope to come back with more complete solution, covering both scenarios soon.

dwg said...

Try using the callback afterSetup to replace the content:

new Effect.SlideUp('main_div');
new Ajax.Request(url, {
  method:'get',
  onSuccess: function(transport){
    new Effect.SlideDown('main_div', {
      queue: 'end',
      beforeSetup: function() {
        $('main_div').innerHTML=transport.responseText;
      }
    });
  }
});

dwg said...

meant to say:

Try using the callback beforeSetup to replace the content...

Andrew Skiba said...

Thank you for good idea! I guess this will do the trick.