AJAX Loading of any Block

mikeytown2's picture

For sites that like to use a cache to serve the full page (boost, drupal core cache, ect...), but still would like to have more dynamic content in a block; loading of blocks via ajax seems like a good way to accomplish this. I've already accomplished this with the boost module and I'm wondering if/how I should abstract this out to be a generalized solution.

Simple way

Below is the simple way to do it, for one or 2 blocks it works. For anything more then that it would probably have a negative impact since it's doing a full bootstrap for each block.

blocka.php

<?php
include_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

$block = module_invoke('statistics', 'block', 'view', 0);
print
$block['content'];
?>

put in a block

<div id="info">
</div>
<script>
$('#info').load('/blocka.php');
</script>

http://api.drupal.org/api/function/statistics_block

Another Example
blockb.php

<?php
include_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

$block = module_invoke('user', 'block', 'view', 3);
print
$block['content'];
?>

put in a block

<div id="infoa">
</div>
<script>
$('#infoa').load('/blockb.php');
</script>

http://api.drupal.org/api/function/user_block

More complicated way

Place 2 div's with the ID of boost-stats & boost-user where you would like the data to appear. I use a block to do this.

<div id="boost-stats"></div>
<div id="boost-user"></div>

Javascript
drupal_add_js($js_code, 'inline', 'footer');

$.getJSON(Drupal.settings.basePath + "$ajax_menu_callback", {nocache: "1"}, function(response) {
  $.each(response, function(id, contents) {
    if (contents == 'NULL') {
      $(id).parent().parent().hide();
    }
    else {
      $(id).html(contents);
    }
  });
});

PHP

<?php
 
include_once './includes/bootstrap.inc';
 
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

 
// Return Data
 
$json = array();
 
// Get stats block html.
 
$block = module_invoke('statistics', 'block', 'view', 0);
 
$block = $block['content'];
 
$json = array_merge($json, array('#boost-stats' => $block));

 
// Get user block html
 
$block = module_invoke('user', 'block', 'view', 3);
 
$block = $block['content'];
 
$json = array_merge($json, array('#boost-user' => $block));

 
// Send JSON Back
 
if (!empty($json)) {
    echo
json_encode($json);
  }
  exit;
?>

Notes:
Grabbing the info from the block cache is another way to do it; one that doesn't require a full bootstrap.
If JSON returns 'NULL' I have it set to hide that block.
Reason I add nocache=1 to the getJSON request url is because boost will soon be able to cache JSON objects. This tells boost to not cache that url.

So, whats the point?

Question is should we, the Drupal community pursue this idea of ajax blocks? It seems like it could be fairly useful for sites that use full page caching. All thats needed is a admin interface to load up module_invoke(). I'm throwing this out here as I don't have the time currently to do this; and it's useful in it's current form.

Comments

Authcache module

Jonah Ellison's picture

The Authcache module currently uses extensive use of Ajax to serve dynamic content for cached pages in this manner. While I have not encounter any serious limiations to using Ajax, there are still those who are against JavaScript having such a vital role in serving content.

If you take a look at ajax_authcache.php, there are a few methods it uses that may be good starting point:

  • Attempting to perform everything in a single request, instead of using multiple or separate, confined PHP files.
  • Requiring a custom HTTP header to help prevent CSRF (cross-site request forgery) attacks
  • Allowing the JSON response results to be cached using the max-age header
  • Modular functions

The process that Authcache uses is like this:

  1. authcache.js -- send request to ajax_authcache.php in this format: {"function_name" : "arguments"}
  2. ajax_authcache.php -- Call PHP functions _authcache_function_name("arguments") and return new JSON object: { "function_name" : "return value"} (note the "_authcache_" prefix to the function names ... this is for security reasons)
  3. authcache.js -- Receive new JSON object and call JavaScript functions _authcache_function_name("return value") to update page content or whatever else is required

This allows the Ajax phase to be fairly modular for easy updating and so developers may add their own custom dynamic content.

All these steps may seem like a fairly lengthy/slow process, but with the continued improvement of JavaScript engines in modern browsers and CPU speed increasing every year, it is actually fairly responsive (Authcache example: http://authcache.httpremix.com/).

Sweet

mikeytown2's picture

Yeah I forgot about authcache. The demo seems to do 2 ajax requests and the content type is of text/html. I just fixed my boost code so it uses drupal_json() instead of json_encode() / drupal_to_js(); you might want to do the same. Is there anyway to get your code down to 1 request per page? Also adding in a menu hook called ajax_authcache.php would be a nice trick, something I also just recently did; allows for the lazy to not have to copy the php file to the webroot.

Calling php functions from javascript seems a little scary to me (even with the authcache prefix); although it does allow for a lot.

Right now authcaches seems overly complicated, but then again I'll I'm doing is updating/populating a div with a blocks contents.

It may appear to be doing a

Jonah Ellison's picture

It may appear to be doing a second request because of the example block, but it is only doing one request since the example block Ajax request is cached by the browser (since that content can be cached and the first request cannot). Yeah, it's using drupal_to_js() in beta7, though I do need to change the content-type header, thanks for pointing that out!

Using hook_menu seems like a nice trick... but since it is going through the Drupal menu system, a full Drupal bootstrap is performed, and ajax_authcache.php needs to be as fast as possible since it is called on every Drupal page view when a user is logged in.

The overcomplication is really due to caching pages for authenticated users... there are so many little dynamic elements that must be accounted for! Upgrading for Drupal 7 is probably going to be pain, hah.

Boost + Authcache?

Jonah Ellison's picture

The hook_menu idea just inspired me on a new way to include ajax_authcache.php... since Authcache requires $conf['cache_inc'] to be defined, it can instead use Drupal's index.php bootstrap to include ajax_authcache.php and then exit before it finishes and return the JSON!

Is there a roadmap for Boost regarding caching pages for logged-in users? I am toying with the idea of what it would take to allow Authcache + Boost to run side-by-side (Authcache is actually two layers: preparing the final HTML for caching and then using a caching system to save/retrieve it).

Boost & caching logged-in users

mikeytown2's picture

There are no plans to have boost handle logged in users. It's hard enough only having to serve 1 version of a page. Boost & Authcache should be compatible, since they operate at a different level on the server. I enabled boost to work with drupal core cache, so there shouldn't be any issues.

Boost & Authcache for anonymous and authenticated users

juan_g's picture

Mike wrote:

Boost & Authcache should be compatible, since they operate at a different level on the server.

On the handbook page on Drupal performance resources, I've just added some detail on complementing Boost with the new Authcache module, using them for non-logged in and logged in users, respectively. I hope it's correct to assume that Authcache can be used in Cache Router's file or db modes in the case of shared hosting.

Good & bad

raintonr's picture

I like this idea and have thought about doing this. You don't have to do do it with JS of course - you could use iFrames for your blocks. Is there any reason you chose JS over iFrame?

One large-ish site I run has rotating ads on the left and/or random image. If I turn on caching then these become fixed on a per-page basis for anonymous users which isn't the best so a solution as described here would be nicer.

That said, I'd like to see some smarter mechanism that had all the rest of the page pre-built and only replaced certain sections within that page (the blocks for example). This could be a simple preg_replace call and should be pretty quick. I'm not smart enough to figure out how/if/when this could be done though, but caching of all but a couple of specific parts of a page would be nice for these cases.

JS over iframe

mikeytown2's picture

The magic of javascript is I only have to do 1 request, and that will populate as many div's as I want. With an iframe each content block will require a new http request. Using an iframe as a fall-back could be an option. Using javascript to replace the old stale content is another option; one that I like better then multiple iframes for performance reasons. Being able to choose the degradation method, would be ideal; let the site owner choose.

Looking at the request headers one could tell if it should return HTML to a iframe or JSON to jQuery; no need for 2 url's
http://www.rvaidya.com/blog/php/2009/02/25/get-request-headers-sent-by-c...

Many bootstraps per page view

kbahey's picture

This approach has advantages, but be aware that by using DRUPAL_BOOTSTRAP_FULL, you have bootstrapped Drupal fully TWICE for every page load. If you have more of such blocks, you will have more bootstraps, hence more work server side.

The sum may be a net gain or net loss depending on the complexity of the bootstrap vs. the savings from boost's caching.

Drupal performance tuning, development, customization and consulting: 2bits.com, Inc..
Personal blog: Baheyeldin.com.

Drupal performance tuning, development, customization and consulting: 2bits.com, Inc..
Personal blog: Baheyeldin.com.

Look at "More complicated way"

mikeytown2's picture

It uses JSON to load many blocks with one request. You could populate 10 blocks with 1 AJAX request.

In reference to this

gregarios's picture

In reference to this code:

<?php
include_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

$block = module_invoke('user', 'block', 'view', 3);
print
$block['content'];
?>

When I try to do this simple trick, my server gives me a 500 error when trying to access the PHP file. The PHP file is accessible, but it appears that the 'includes' directory can't be accessed? I'm not sure why it isn't working. Can anyone help? Please ask me the right questions if I haven't told you the right info.

If I replace the contents of the PHP file with

<?php
print "hello world";
?>
it can be accessed just fine.

Even if I give the 'includes' directory and contents full 777 access, the bootstrap file still cannot be viewed in my browser. (403 error)

Modules...

mikeytown2's picture

Some module break this
http://drupal.org/node/476280

You might have to create a custom bootstrap script if this is the case, or disable/unload the bad module.

Try turn on error reporting

johnla's picture

I think I had the same issue as you.

I added:
error_reporting(E_ALL);

and checked the php error logs. And saw that the bootstrap also required some other include files. I had to change the PHP current working directory so that the include files could be found.

$drupal_path = "../";
chdir($drupal_path); // did this twice because the file was 2 directories over the web root
chdir($drupal_path);

Doing that before boostrap fixed my issue. Good Luck

Simple Ajax Call

douglas.a.hatch's picture

Real nice tutorial. What if I don't need to load a block, just a variable? I'd like to dynamically retrieve the subtotal from Ubercart on page load, then tell the customer how much more they need to purchase to get FREE shipping. See example here (i.e. yellow box msg in middle-right):

http://chunkybling-sandbox.com/interchangeable-watches/watch-bands/test-...

Right now, I've got the following embedded in my site:

    
<?php
&#10;           $items = uc_cart_get_contents();&#10;           if (!empty($items)) {&#10;              foreach ($items as $item) {&#10;                    $total += ($item-&gt;price) * $item-&gt;qty;&#10;               }&#10;              if ($total &lt; 75.0) {&#10;                    echo t(&#039;&lt;p&gt;Add just &lt;span style=&quot;color:#b30000; font-weight:bold;&quot;&gt;@amount&lt;/span&gt; more to earn &lt;a href=&quot;http://chunkybling.com&quot;&gt;FREE SHIPPING&lt;/a&gt;!&lt;/p&gt;&#039;, array(&quot;@amount&quot; =&gt; uc_currency_format(75.0 - $total)));&#10;                }&#10;          }&#10;     
?>

But with boost enabled, the $$$ will get cached and not update on page load. Any suggestions would be appreciated.

Try adding your lines of code

criznach's picture

Try adding your lines of code one at a time. Which one causes it to fail? If it's the include, then I suppose it could be permissions. If it's the module_invoke call, try another module/block/delta combination - or even a custom block.

Block Content Replacement

mikeytown2's picture

Have code that will "update" multiple block contents via ajax request.
http://drupal.org/node/657826#comment-2923640
Previous ways only created the html via ajax, new way allows for in-place update.

It Works!

gregarios's picture

I can finally get this to work on my system using this code... thanks johnla for your directory tip:

<?php
$drupal_path
= "../../../../";
chdir($drupal_path);
include_once
'./includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
$block = module_invoke('user', 'block', 'view', 3);
print
$block['content'];
?>

My next question is this: How do I figure out what info needs to change in this part of the code to show the "latest poll block" output?

$block = module_invoke('user', 'block', 'view', 3);

Update: Ah... figured it out with some sleuthing in the "Blocks" settings for pathnames... IE: the poll block uses:

$block = module_invoke('poll', 'block', 'view', 0);

Ajax_Load module

markus_petrux's picture

In case the new content needs additional javascript files and/or stylesheets that are not already present on the page, you may need something like Ajax Load module to do that job for you.

ajax content

euricojardim's picture

Hi all,

What if the returned content is also ajaxified?

For example: Request an poll block, where the vote button is ajax!

I tried and it didn't work as expected. It postback as a normal button...

Any suggestion?

Thank you all... Regards, Eurico

The returned content needs to

stewsnooze's picture

The returned content needs to be pushed through live() http://api.jquery.com/live/

Full Fat Things ( http://fullfatthings.com ), my Drupal consultancy that makes sites fast.

Try this module

mikeytown2's picture

The 1.1 version of the Ajaxify Regions module is a lot better then the 1.0 version.

Configuration is at admin/settings/performance/ajaxify-regions. It works by the block ID from what I can tell.
Example is I have a view with blocks called homepage_sponsors. If I wanted to ajax 2 blocks I would put this in the "Ajax replace specific blocks:" section.
views-homepage_sponsors-block_*

The css ID for these blocks are
block-views-homepage_sponsors-block_1
block-views-homepage_sponsors-block_2

Another simple module

maximpodorov's picture

Another simple module is Ajax Blocks (fork of Ajaxify Regions).

This solution works for

superfedya's picture

This solution works for Shoutbox block? If yes, anybody know how (I don't known anything about php).
Thanks!

How to load content by ajax in lightbox/popup

sohail_arif's picture

HI ALL
I need some help regarding a feature that is present in http://easypetmd.com/node/57. When you click on a body part its features are presented in a pop up .Content is loaded via ajax in two tables present on right side when we select an option from right side. I need the same feature. Can anyone give me some similar example in jquery or javascript which i can use?

uploading

sciencex's picture

Question for the Simple Way example, where are you uploading the blocka.php file?

Thanks!

uc_ajax_cart, drupal cache, and boost in harmony

mabbus21's picture

hey all,

I found a fix for the problem following this example on drupal 7. i wrote a menu hook "/ajax-cart-load" that returns the function which loads the block:

function custom_module_load_cart(){
  include_once '/includes/bootstrap.inc';
  drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
  $block = block_load('uc_ajax_cart','delta-0');
  $block_content = _block_render_blocks(array($block));
  $build = _block_get_renderable_array($block_content);
  $block_rendered = drupal_render($build);
  if($user->uid || custom_module_cart_has_items()){
    print $block_rendered;
  }else{
    print '';
  }
}

As you can see I also created a function custom_module_cart_has_items() to only display bag if the anonymous user actually has items. we don't want to reload the bootstrap if we don't need to:

function custom_module_cart_has_items(){
  $cart = uc_cart_get_contents();
  $cartID = key($cart);

  if($cart[$cartID]->nid){
    return true;
  }
  return false;
}

Then on uc_ajax_cart.module, on line 358 put a false on the condition because they check for the cached block all the time. Basically what this does is disables the block caching for the ajax cart and always returns dynamic content.

...

  if (!$user->uid && $cacheEnabled > 0 && false) {
      $loadOnPageView = variable_get('uc_ajax_cart_cart_cached_load', 0);
...

Then on uc_ajax_cart.js I added an else clause in the reload function because we don't always have a pane for the cart. So here it is:

// Reloads standard Ubercart cart form from cart page.                                                                                                                         
function ajaxCartReloadCartView() {
  if (jQuery('#cart-form-pane').length) {
    jQuery('#cart-form-pane').parent().load(Drupal.settings.uc_ajax_cart.SHOW_VIEW_CALLBACK, ajaxCartInitCartView);
  }
//added by me
else {
      var anonCart = jQuery('.anon-cart');
      if(anonCart.length){
          anonCart.html('');
          anonCart.load('/ajax-cart-load');
      }
  }
}

I have a condition on the page.tpl.php file that if !$user->uid add a class of anon-cart to the container div.

now what im getting is all anon users are getting the same cart. if someone finds a solution please share or if i fix this i'll repost

************* UPDATE *******************

I noticed i had to pass $_SESSION['uc_cart_id'] on uc_cart_get_contents()

uc_cart_get_content($_SESSION['uc_cart_id'];

this will return the unique cart for each session :)

High performance

Group notifications

This group offers an RSS feed. Or subscribe to these personalized, sitewide feeds: