A Damn Simple Technique For Making Anything in Drupal Ajaxed*

09.02.2009

*well, probably anything that uses drupal.behaviors...

Today, I was doing a few experiments on how to get several giant CCK node forms to load and submit via ajax from a single custom page.When this technique actually worked on the first try, my exact words were "no f#cking s#it..." I'm sure I'm not the first to figure this trick out, but I have had a hard time finding people who've described it. Perhaps everyone besides me figured it out ages ago -- though if that's true, I don't want to know what horrible things have driven some of you to use your current techniques.

This technique seems especially ideal for integrating any existing drupal form (especially giant CCK forms with sortable, multiple value, file fields), and the results into some highly customized drupal based web gizmo. *IT REQUIRES NO PHP* beyond whatever calls you may have to make to get dependent JavaScript files available to your requesting page. This pattern will mostly degrade to drupal's default behavior if JavaScript isn't present.

The stupid simple strategy

We use javascript to take what we want from drupal's default behavior, and throw away everything else.

Below, is all you need to submit a giant CCK form and have it successfully post:

//node form just happens to be the ID shared by every node form
//replace #node-form with any form ID and the results are the same
$('#node-form').submit( function() {
  $.post($(this).attr('action'), $(this).serialize(), function(response) {
  // ...
  }
  // return false prevents the form from submitting regularly ... noob...
  return false;
});

Even though the user will not be redirected or see any results of this $_POST, jquery will still follow the default redirect to node/$node->nid or return the original form with validation errors -- this is actually a good thing.

$('#node-form').submit( function() {
  $.post($(this).attr('action'), $(this).serialize(), function(response) {
   // why not, lets replace our logo with the resulting node
    $('#logo').replace($(response).find(.'node');
  }
  return false;
});

There's a few downsides I see to this technique: for one generating entire pages is totally unnecessarily; since we are simply taking contents of $(.node) from the result's full document object, why load the entire page? The answer is: the page already exists, and its one less menu_callback, loading, or godforbid form behavior altering trick that will cause bugs down the road. Its easy, it works, and all but the minority of websites would need to worry about the performance implications.

Another downside is that since we are using jQuery, we are depending on classes and ids that may eventually change. I think this is a real risk, but i think the risk can mostly be avoided by making intelligent decisions about what selectors you use. Besides, the maintenance hassle is probably still less compared to many of the alternatives.

Below is a working example: load a node "edit" tab, submit, and refresh the node all via ajax.

The code snippet is cute as a button, though its handling of validation is sort of half-assed. Note that the vast majority of code is devoted to simply throwing around the response data, the jquery itself is elementary.

Sadly, for this to work with node form javascript, we actually have to call that node form from the page that we are requesting it. The implementation of hook_init required for CCK behaviors could be placed wherever you want so long as it executes before the final call to drupal_add_js(). I'd be interested to know if anyone has any better ideas for making various node JavaScript files available to the page that is requesting the form.

// not really where this should run, but among the few place I can be absolutely 
// certain it will work for demonstration purposes.
function hook_init() {
  // this is for a page
  // replace "page" with "your_node_machine_readable_name"
  // cause' machines are picky sons of bitches
  $node = new stdClass();
  $node->type = 'page';
  module_load_include('inc', 'node', 'node.pages');
  drupal_get_form('page_node_form', $node);
}
Drupal.behaviors.lazyAjax = function(context) {
  // ul.primary is the name of my primary local tasks menu
  $("ul.primary li a:not(ul.primary li a.lazyAjax-processed)", context).each( function() {
    $(this).addClass('lazyAjax-processed')
    if ($(this).text() == 'Edit') {
    // this class is added as mark of shame, letting all future calls of Drupal.attachBehaviors
    // know that to avoid processing this link (which would cause a node form to appear twice) 
      $(this)
        .lazyAjax('#node-form','.node');
    }
  });
};

$.fn.lazyAjax = function(source, target) { 
  return this.each( function() {
    $(this).click( function() {
      var url = $(this).attr('href');
      $(target)
        .slideUp(500)
        .empty();
      $.get(url, {}, function(response) {
        $(response)
          .find(source)
          .prependTo($(target));
        // this is why CCK file fields javascript works (assuming you followed directions and fake returned the form)
        Drupal.attachBehaviors($(target));
        var form = $(target)
          .slideDown(500)
          .find(source);
        $(source).submit( function() {      
          var form = $(this);
          $.post($(this).attr('action'), $(this).serialize(), function(response) {
            var errors = $(response).find('#messages .error');
            if ($(errors).text()) {
              $(form).prepend(errors);
            }
            else {
              var result = $(response)
                .find(target);
              $(form)
                .slideUp(500)
                .empty()
                .parent()
                .append(result)
                .slideDown(500);
            } 
          });
          // .click returns false so the user doesn't actually follow a link
          return false;
        });  
      }); 
      return false;
    });
  });
};
Note: * I admit I hate attempts at abstracting javascript away via PHP as javascript is so good at what it does, where as PHP always feels like an inelegant, cumbersome replacement that gets in the way. Just one mans opinion.

Comments

A suggestion

I'd be hesitant to use something like this when FAPI supports AHAH. It seems it would make more sense to restrict this JS to only making certain elements (e.g. the Edit link) AJAXified and leave the form's AJAX to FAPI (or make another JS function to take care of the form submission). I fear that something like this is a one off solution that can never be reused without rewriting parts of it.

Borrowing heavily from your code...

$.fn.lazyAjax = function(source, target) {
    return this.each(function() {
        $(this).click(function() {
            var url = $(this).attr('href');
            $(target)
                .slideUp(500)
                .empty();
            $.get('http://localhost' + url, {}, function(response) {
                $(response)
                    .find(source)
                    .prependTo($(target));
                Drupal.attachBehaviors($(target));
                $(target).slideDown(500);
            });
        return false;
        });
    });
};

And you'd make the call like this.

$("a[text=Edit]").lazyAjax('.node-form', '.node');

Of course, we can make this more flexible and allow you to pass in the transition effects and even the event listener. More importantly, I don't need to hack through the internals to use the code later.. I just call the function where necessary. :)

Thoughts?

-Brice Stacey
http://bricestacey.com

ahaah

your comments make me lol.

www.twitter.com/creativestable

AJAX module?

I believe that all this is automatically done using the ajax module. Here are clear instructions on how use it to ajaxify any form: http://drupal.org/node/349961.

Simply add the following to the form:

  '#ajax' => array(
    'submitter' => TRUE
  ),

Well that is simple <?php 

Well that is simple

<?php
 
'#ajax' => array(
   
'submitter' => TRUE
 
),
?>

I've checked the module out -- it seems useful for some things, but what i'm after is enough control that i can do any "arbitrary" behavior. How could that code translate into jquery that knows when I click a node's "edit" menu task i actually want the loaded the node form to replace the contents of .node, and then want to replace that form with the response's $(.node) if i get a successful post? For that matter I could use this method to make a comment's reply link return just the specific reply form, under the comment, and to use that "new" class from the result to make the submitted comment appear via ajax.

Easy to do in JavaScript -- not so much with php wrappers for javascript.

Side note i messed something

Side note i messed something up in translating the original code I was using to something general. Long story short, the file uploads in cck are messed up and are causing the form to submit for some reason. Should learn something interesting in tracking down the cause of that!