Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

Loading Facebook embeds via AJAX in a CTools modal dialog

oEmbed is a simple and popular protocol for embedding external media in web pages. It supports many types of content, including images, videos, and even "rich" content (i.e., HTML snippets). Unfortunately, the current Facebook embed mechanism does not work so well when the embed code is loaded dynamically on a page, e.g. via AJAX instead of statically.

To see this in action, I've created a test page that shows the difference between static and dynamic embedding. The static embedding automatically renders the post correctly, whereas loading the very same embed code using AJAX (here, via the Load button) does not. It is necessary to call FB.XFBML.parse() manually (here, via Refresh button) to nudge the embed code into rendering the post.

In my Drupal app, I needed to show the Facebook embed on a CTools modal dialog. How to trigger the call to FB.XFBML.parse() when the modal opens? Reading the code for ctools_modal_render(), I found hook_ajax_render_alter() which allows to send arbitrary AJAX commands to be executed on the browser side, upon reception of the modal's content. Here's how I used it:

// @file my_module.module
/**
 * Implements hook_ajax_render_alter().
 * Add a `facebook_refresh` command to make sure FB embeds are shown correctly.
 */
function my_module_ajax_render_alter(&$commands) {
  foreach ($commands as $command) {
    if ($command['command'] == 'modal_display') {
      $commands[] = array(
        'command' => 'refreshFacebook',
      );
    }
  }
}
// @file my_module.js
  /**
   * Facebook initialization callback.
   */
  window.fbAsyncInit = function() {
    // Wait until FB object is loaded and initialized to refresh the embeds.
    FB.XFBML.parse();
  }

 /**
   * Command to refresh Facebook embeds.
   */
  Drupal.ajax.prototype.commands.refreshFacebook = function(ajax, response, status) {
    if (typeof(FB) != 'undefined') {
      window.fbAsyncInit();
    }
  }

The front-end logic goes as follows: the first time the modal dialog is opened, the embed code causes the FB script gets loaded, which in turn calls window.fbAsyncInit(), where I call FB.XFBML.parse() as needed. But on subsequent openings of the modal dialog, window.fbAsyncInit() is no longer called, so my custom AJAX command refreshFacebook() takes over in that case to do the same.

The proverbial astute reader would ask why I don't rely on refreshFacebook() to do all the work, since it gets called every time, including the first time. The problem with the first time is that this command callback gets called before the FB script has finished loading, so the FB object does not yet exist at this time. Yes, tricky.

This is where things get really complicated. Trying the code above, the FB embed did show on the CTools modal, only to disappear 45 seconds later with a console message saying fb:post failed to resize in 45s! A bit of googling revealed that this is a known behaviour - but it seems no one had yet analyzed the problem to its root causes. So I pulled up the debug version of Facebook's JavaScript SDK and proceeded to decipher the code there. It turns out that the FB embed code creates an IFRAME with default width and height of 1000px, and it expects the rendering process of the actual post to resize its dimensions according to the post's display area. The IFRAME handler kicks off a 45 seconds timeout, at the end of which it hides the post and logs the message above. Only when a resize event is received does the IFRAME handler cancel the timeout.

The only way I was able to trigger this resize event was by re-initializing the FB script each time the CTools modal is loaded. So my JavaScript code became:

// @file my_module.js
  /**
   * Facebook initialization callback.
   */
  window.fbAsyncInit = function() {
    // Wait until FB object is loaded and initialized to refresh the embeds.
    FB.init({ xfbml: true }); // added this to avoid "fb:post failed to resize in 45s" message
    FB.XFBML.parse();
  }

This will cause some warnings in the log, like FB.init has already been called - this could indicate a problem. So far, I haven't found a problem with this approach but I welcome your suggestions.

I've spent the best part of 3 days debugging this unexpected behaviour, so I hope this helps someone! I do hope Facebook would make their embed code more robust though - for example, the Twitter embed behaves much better.

Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web