A Damn Simple Technique For Making Anything in Drupal Ajaxed*
*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.