Wednesday, April 16, 2008

Using ajax to load parts of a page in Drupal

I wanted to load some time-consuming content into Drupal nodes in the Millennium module, but didn't want the whole page to suffer for it. It looked rather hard at first, but I came across some code that made it clearer for me.

You will be putting funcionality in a module, so you'll need to know how to write a Drupal modules first.

First, you will need a DIV in your final page's HTML that will be updated via Ajax. It can be result of your module adding stuff to a node via the nodeapi hook, for example. I tagged mine like this:
<div class="millennium holdings load" id="nid-12345"> Text before loading goes here. </div>   
Also, you will need a backend portion that outputs HTML.

Now, remember your node or page can be listed along with others! (For example in search results showing teasers, Views, or the standard front page view in Drupal which shows teasers for all new published + promoted pages). So we will have to deal with multiple DIVs appearing from different pages, and write JS to deal with ALL of those.

You will have to write some JS to modify this div; I placed mine in the module's directory as millennium.js
// The following is Javascript

// Call millenniumUpdateNext() when page first loads.
$(document).ready(millenniumUpdateNext);

/**
* Looks for first div element with class "load" and updates it depending on its
*
id attribute. (DIVs should already have an id="nid-XXXX" attribute, where XXXX
* is the node's id.

*/
function millenniumUpdateNext() {
// Get the first div element with the classes we are looking for
var div = $("div.load").get(0);
if (! div) return;
var id = div.id;
if (! id || id.substr(0,4) != "nid-" ) return;

// Get the node id from the id attribute
var nid = id.substr(4); //nid-123456 --> 123456

/**
* URL to get information from. If you go here in your browser you get plain HTML
* depending on the nid argument. This URL is defined in the module under
* millennium_menu()

*/
var url = "/millennium_ajax?nid=" + nid;

// Show 'loading' message.
div.innerHTML = 'Loading...';

// Activate the ajax fetch from url. When done function(data) will get executed.
$.get(url, function(data) {
$("#"+id).hide(); // Hide
$("#"+id).html(data); // Replace 'loading' message with fetched HTML
$("#"+id).slideDown("slow"); // Un-hide it
// Now, "unmark" it so millenniumUpdateNext() skips this element
$("#"+id).removeClass("load");
millenniumUpdateNext(); // Start over for another element
}
);
}
Back in your module, you should implement two things to handle the ajax calls: an item in the _menu hook, and the handler function itself.

First, the item in the menu hook:
<?php
function millennium_menu($may_cache) {
$items=array();
// cached menu items here...
if(! $may_cache) {
$items[] = array(
'path' => 'millennium_ajax',
'callback' => 'millennium_ajax_handler',
'type' => MENU_CALLBACK,
'access' => true
);
}
// ... more stuff
return ($items);
}
?>
And now the handler function itself
<?php
/**
* This function is the one that takes a long time to run...
* This is the whole point; to not delay the whole page load waiting for this
* to be put inline with all the other page elements.
*/
function millennium_ajax_handler() {
$nid = intval($_GET['nid']);
$output = "<h2>You called
millennium_ajax with argument nid = $nid</h2>";
echo $output;
exit();
}
?>
Now, you have the server-side code, and you have the client Javascript code, now you just need to tell Drupal to add the Ajax-able bits to nodes, and also load in the Javascript. Let's do this.

First, to output some custom portions into existing nodes, you use [modulename]_nodeapi(), like this:
<?php
function millennium_nodeapi(&$node, $op, $teaser, $page) {
switch($op) {
case 'view':
// Load JS file in an orderly fashion.
drupal_add_js( drupal_get_path('module','millennium') . '/millennium.js');
// You can also load a custom CSS file here with drupal_add_css()...

// Add the custom content
$node->content['millennium_whatever'] = array(
'#value' => "<div class='millennium load' id='nid-". $node->nid ."'>This will be updated sometime after the page finishes loading.</div>\n",
'#weight' => 0,
);
break;
}
?>
This will add a DIV inside every node, with the classes millennium and load, and also an id attribute of nid-xxxxx

That's it! See how it shows on my test site showcasing the Millennium module.

2 comments:

Anonymous said...

thank you so much for this.

I was unaware that you could use ajax to load parts of a page in Drupal. I was actually looking for more sources until i found your blog.

These sources were of absolutely no help and just resulting in me complicating this more. Which obviously made things even worse. How come barely anyone knows of this? Its such a great and incredible tool!

there aren't many options to fix this problem either! so i was surprised when i couldn't find a proper solution! Thank you so much this post has helped very very much!

Cheers,
Jack

Anonymous said...

so helpful!