Setting JS Callbacks with Drupal, JSON and Some jQuery Sauce (for jCarousel controls)

Ethan Winn's picture

Technical Director

Blog

Setting JS Callbacks with Drupal, JSON and Some jQuery Sauce (for jCarousel controls)

share

Sometimes a few characters of code can make all the difference in the world. All coders know
the scenario: the code looks fine, all the right values are in the right
place, the code validates or compiles (or at least looks like it should), but
when we run it we get...nada. Maybe the source of the problem is a comma
following an array declaration (fine in PHP, but can be a killer in JS), an extra "$"
in a variable name, or - as in the case of trying to define a callback for jQuery
libraries like jCarousel using JSON - an extra pair of quotes around the callback
function value.

Unfortunately, the difference of a few quotes is not always so easily overcome.
In the case of extraneously quoted JSON function values, for instance, the
issue originates from within the drupal_add_js function and PHP's own
json_encode. Such situations can lead to desperate measures: grabbing the
drupal_get_js output and munging it with regular expressions or other hackish
means to make it do what we need. While use of such techniques is something of
a religious dispute among Drupalists, I'm happy to announce that there's also
another way to tackle JSON callbacks in Drupal, with nothing more out-of-line than
drupal_add_js and a bit of simple jQuery magic.

First, let's lay out some specifics. The particular challenge we faced in this
scenario was getting Drupal to output a JSON value for Drupal.settings that
looked something like this:

jQuery.extend(Drupal.settings, {
    "basePath": "",
    "jcarousel": {
        "ajaxPath": "jcarousel/ajax/views",
        "carousels": {
            "jcarousel-view--frontpage-slider--block-1": {
                "skin": "custom",
 
                ...[snip]...
 
                "selector": ".jcarousel-view--frontpage-slider--block-1",
 
                "initCallback": jcarousel_setPager,
                "itemVisibleInCallback" : { "onAfterAnimation": jcarousel_setActive }
            }
        }
    }
  }

It looks pretty straightforward at first, right? Most of the settings come
right out of the jCarousel module, with the exception of the callback
function settings which were added custom for the design of a frontpage slider.
So, we think, all we need to do is add a call to drupal_add_js(...,"settings") with
our additional settings in an associatve array like this (implemented in a views preprocess because we were theming a view):

function mytheme_preprocess_views_view__frontpage_slider__block(&$vars) {
  // add callbacks to setting for view
  $jcarousel_settings = array (
    'jcarousel' => 
    array (
      'carousels' => 
      array (
        'jcarousel-view--frontpage-slider--block-1' => 
        array (
          "initCallback" => 'jcarousel_setPager',
          "itemVisibleInCallback" => array(
            "onAfterAnimation" => "jcarousel_setActive",
          ),
        ),
      ),
    ),
  );
  drupal_add_js($jcarousel_settings,'setting');
}

But instead of giving us the code we want, we get something that's almost right
but off by just a few, measely quotes:

    "carousels": {
        "jcarousel-view--frontpage-slider--block-1": {
            "skin": "custom",
 
            ...[snip]...
 
            "selector": ".jcarousel-view--frontpage-slider--block-1",
 
            "initCallback": "jcarousel_setPager",
            "itemVisibleInCallback" : { "onAfterAnimation": "jcarousel_setActive" }
        }
    }

Of course when we run this we don't get any errors (it's still valid JSON and
jCarousel doesn't seem to really care that much about the fact that what it
thinks is a function turns out to be a string), it just doesn't work. Nothing
happens.

The bottom line is that this is fundamentally a limitation of PHP's json_encode
and there's just not a whole lot we can do to get around it using Drupal JS
settings. What we can do is add some inline JS using jQuery.extend's "deep
copy" mode to set additional JS settings values "by hand", as it were. What we
need is some code like this:

function mytheme_preprocess_views_view__frontpage_slider__block(&$vars) {
  drupal_add_js($slider_js,'inline');
  $slider_settings_js = <<<EOF
jQuery.extend(true,Drupal.settings, {
    "jcarousel": {
        "carousels": {
            "jcarousel-view--frontpage-slider--block-1": {
                "initCallback": jcarousel_setPager,
                "itemVisibleInCallback" : { "onAfterAnimation": jcarousel_setActive },
            }
        }
    },
});
EOF;
  drupal_add_js($slider_settings_js,'inline');
}

It's still a bit brittle and certainly not "right" down to the core. But at the
end of the day it passes the coding tests of being easily comprehensible by
other developers and building on standard methods without doing anything
eggregiously "wrong". Finally, it also demonstrates the importance of knowing
how to work with your full stack. The dangers of framework developers knowing
only the top-level functionality of their system, and not the SQL, CSS, jQuery
or other layers up which those top-level calls depend has been talked about a
lot. The crux of the issue is that when something goes wrong, it's most likely
an issue at one of those layers, and knowledge of those layers is required to
address the issue in the best way.