Sunday, May 22, 2011

Yet another Ajax toolkit

Here is a little dabbling we did at Accorpa to empower rapid development of ajax applications.

We needed a tiny library that uses jQuery to be used in Rails with two simple steps:

  1. Add a couple of custom attributes to some links/inputs/forms
  2. Include a javaScript file into the layout.

The solution had to allow developing the same site in Ajax and non-Ajax ways (in parallel) so we added a new custom MIME type 'text/ajax' and used it in our controllers whenever we needed it.

A full rails example will be coming soon. Here is the JavaScript file for starters:



This is the main function called whenever an ajaxified link is clicked

It calls loadInto for the actual work but prepares it first using custom attributes on the link
If the target is an image, its parent DOM element is used instead

Custom attributes handled:

  • 'rest' = the HTTP method to be used for the call
  • 'href' = the url to load
  • 'into' = the target element to update
  • 'from' = a JSON string stating what paremeters need to be fetched from the value of DOM elements

For example, a combo box can pass its value along with the call as follows:
{'option' : 'dom_element_id'}
where 'option' is the parameter to be appended
and 'dom_element_id' specifies the id of the combo box

  • 'parameters' = a JSON string stating extra parameters to be passed in the URL
  • 'evaluate' = a JavaScript string to be evaluated and the result added to the URL
  • 'also' = a JSON string stating additional calls that need to be made

For example, if you need to hide comments from their holder when an article is reloaded:
{'dom_element_id' : ''}
Possible values to be passed inside the JSON hash are:

  • '' = will clear the innerHTML of the element
  • 'hide' = will hide the entire element from the page
  • url = will load the url via ajax into the element

In case of using 'also' with a url, the same parameters passed to the original href url
will be passed to the other urls for convenience
If you need a parameter to be specific to one url only it has to be appended to that url

function ajaxClick(event) {    
 var el = $(event.target);
 if (el.is("img")) {el = el.parent(); };
 
 var into = $('#'+el.attr('into'));
 var method = el.attr('rest') || 'get';

 var parameters = el.attr('parameters') || ("{text: '"+el.val()+"'}");
 eval('var params = (' + parameters + ')');
 
 if (el.attr('evaluate')) {
  eval('var evalhash = (' + el.attr('evaluate') + ')');
  jQuery.each(evalhash, function(param, value) {
   if(value != '')
    params[param] = value;
  });
 }

 if (el.attr('from')) {
  eval('var hash = (' + el.attr('from') + ')');
  jQuery.each(hash, function(param, dom) {
     var from = $( "#"+dom );   
   if (from) {
    params[param] = from.val();
   } 
  });
 }
 var url = el.attr('href');
 jQuery.each(params, function(i, val) {
     url = append2URL( url, i, val);
  });
 
 loadInto(el.attr('into'), url, method,el);
 
 if (el.attr('also')) {
  eval('var hash = (' + el.attr('also') + ')');
  jQuery.each(hash, function(i, val) {
     into = $( "#"+i );
   
   if (val == '') {
    into.html( '' );
   } else if (val == 'hide') {
    into.hide(400);
   } else {
    loadInto(i, val, 'get',el);
   }
  });
 }
  
 return false;
}

The following function tests the target element for custom attribute 'insert'
Possible values:

  • No attribute = the default is 'into'
  • 'into' = replace the innerHTML with the ajax-loaded data
  • 'replace' = replace the outerHTML with the ajax-loaded data
  • 'before' = add the new data as new HTML just before the target in the DOM
  • 'after' = add the new data as new HTML just following the target in the DOM

function ajaxPlace( element, data ){
  var target = $('#'+element);
 target.show();
   
 if (!target.attr('insert') || (target.attr('insert') == 'into')) {
  target.html(data);
 }   
 if (target.attr('insert') == 'replace') {
  $("#" + element).replaceWith(data);
 }
 if (target.attr('insert') == 'before') {
  $("#" + element).before(data);
 }
 if (target.attr('insert') == 'after') {
  $("#" + element).after(data);
 }
}


The following shows an ajax waiting graphic in a suitable place according to the custom attribute 'insert'
Possible values:

  • No attribute = the default is 'into'
  • 'into' = replace the innerHTML with the ajax-loaded data
  • 'replace' = replace the outerHTML with the ajax-loaded data
  • 'before' = does nothing
  • 'after' = does nothing

It first of all triggers the custom event "ajaxBefore"

The loaded image should have a custom attribute of 'tmp' so it can later be removed/replaced when necessary

You can provide your own image here

function ajaxWait( element ) {
 
 $(document).trigger('ajaxBefore'); 

 var data = '';
 var target = $('#'+element); 
   
 if (!target.attr('insert') || (target.attr('insert') == 'into')) {
  target.show();
  target.html(data);
 }   
 if (target.attr('insert') == 'replace') {
  target.show();
  $("#" + element).html(data);
 }
 if (target.attr('insert') == 'before') {
 }
 if (target.attr('insert') == 'after') {
 }
}

This function handles newly loaded data according to custom attributes:

  • converts all links marked by 'into' to ajax links
  • converts all inputs and combo boxes marked by 'into' to ajax callers by adding onChange handlers
  • converts all forms marked by 'into' to be submitted via ajax
  • removes all images marked by 'tmp'

It finally triggers the custom event "ajaxLoad"

function ajaxify() {
  $('a[into]').removeAttr('onclick');
  
  $('a[into]').unbind('click' );
      
  $('a[into]').bind('click', ajaxClick); 

  $('input').unbind('change' );
  $('input').bind('change', ajaxRun );
  
  $('select').unbind('change' );
  $('select').bind('change', ajaxRun );

  $('img[tmp]').remove();

  $('form[into]').submit(function() {
  
  ajaxWait();
    
  $.post(this.action, $(this).serialize(), function(data) {      
   ajaxPlace( $(this).attr('into'), data );
   ajaxify();
  }, "script");
       
  return false;
  })

  $(document).trigger('ajaxLoad'); 
}


The simplest form of ajax call that will call the usual calls and trigger the usual events

You can call this directly from your javascript as a shorthand function

function loadInto(element, url, method, source_element) {
 
 if (method == 'delete') {
  if (!confirm('Are You Sure?')) 
   return false;
 }
 
 ajaxWait(element);
 
 $.ajax({
    type: method,
  url: url,
    success: function(data){

   ajaxPlace( element, data );

   ajaxify();
   if (source_element.attr('afterSend'))
    eval(source_element.attr('afterSend')+"(unescape(data))");

   return true;
    }
 });
 return true; 
}

This function prepares the document initially (calls ajaxify)
and sets up the necessary handler for the jQuery event 'beforeSend' to handle Rails
in the manner described by Accorpa.

For Rails, we need a custom MIME type 'text/ajax'
You may change or remove this line if you need another way of using Rails

You may add handlers for the custom events 'ajaxBefore' and 'ajaxLoad'
to handle special javaScript functions you need to apply
such as CKEditor, jQuery UI controls, table sorting scripts, etc.

$(document).ready(function() {
    
 jQuery.ajaxSetup({ 'beforeSend': function(xhr) {
  xhr.setRequestHeader("Accept", "text/ajax")} 
 }) 
 ajaxify();
})

A couple of utility functions fill in the spaces. You will not need to call these yourself.

function decode(url) {
     return unescape(url.replace(/\+/g, " "));
}

function append2URL(url, param, value) {
 var app = param+"="+value;
 url = decode( url );
 
    return url + (url.indexOf('?') != -1 ? "&"+app : "?"+app);
}

I don't think you can give this a try too easily. You may have to tweak it a little bit. After all, the goodies are yet to come. If you use RoR with jQuery, stay tuned.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.