JavaScript hygiene in Drupal 7

Look, we need to talk. Your taste in music is really great and your friends are cool, but we’re having some issues. No, it’s not that I caught you watching reruns of Days of Our Lives when I got home from work early yesterday (but that doesn’t help); it’s… well, it’s your JavaScript hygiene in Drupal 7.

Maybe we can work this out. Let’s start with proper inclusion techniques.

Put your code in the right place

I’ve seen you sneaking module-specific JavaScript into theme_name/js for expedience, and that isn’t cool.

James O'Bierne
#Drupal | Posted

Look, we need to talk. Your taste in music is really great and your friends are cool, but we’re having some issues. No, it’s not that I caught you watching reruns of Days of Our Lives when I got home from work early yesterday (but that doesn’t help); it’s… well, it’s your JavaScript hygiene in Drupal 7.

Maybe we can work this out. Let’s start with proper inclusion techniques.

Put your code in the right place

I’ve seen you sneaking module-specific JavaScript into theme_name/js for expedience, and that isn’t cool. Unless you’re writing JavaScript that is unique to that theme, theme_name/js isn’t the place to put your code. If the JavaScript is unique to a module or feature instead, place it in module_name/js. That way, your modules are more self-contained and therefore more easily reusable.

Be stingy with .info includes

Unless you’ve written some JavaScript that should be executed on almost every page load, you shouldn’t include the JS file in the module or theme .info file. Let’s say you have some module called foobar that provides a few blocks and you want to accordionize the contents of these blocks, so you have some JavaScript that does this in a file called foobar_block_accordionizer.js. Having

 

1
scripts[] = foobar_block_accordionizer.js

in the foobar module’s .info file will pull down and run foobar_block_accordionizer.js on every page load, even if the JavaScript is only relevant for one block that only appears contextually and infrequently on the site.

This can result in excessive page load time, which, in large doses, can seriously impair user experience.

 

Instead, the better way to include foobar_block_accordionizer.js would be to find a hook that is unique to the foobar block and include the JS file in the body of that hook. For example, in foobar.module, we could include

 

1
2
3
4
5
6
7
8
function foobar_block_view_alter(&$data, $block) {
  if($block->module == "foobar") {
    $data['content']['#attached'] = array(
      'js' => array(drupal_get_path('module', 'foobar')
                    . '/js/foobar_block_accordionizer.js')
    );            
   }
}

Boom, tough actin’ Tinactin: the JS is only attached when the foobar_block_view_alter hook is run and the inner conditional is satisfied, i.e., whenever a block defined by the foobar module appears on a page, i.e., whenever the JavaScript would be of any relevance. Even if our block is cached, the JavaScript will still be included because of our use of the #attached property, which wouldn’t be the case if we were simply making a call to drupal_add_js. More on the use of #attached can be found within the documentation for drupal_process_attached. (Thanks to Moshe Weitzman for pointing this out.)

See, that wasn’t so tough. And now you’re saving your users time. And time is money, so you now you’re saving your users money. What a great developer you are.

Behaving like an adult

Let’s say you have another JavaScript file, awesome_cornifier.js, that stylizes each page by making a call to the Cornify API. Let’s look at what you’ve got in awesome_cornifier.js:

 

1
2
3
jQuery(document).ready(function() {
    // cornify call
});

Okay, we’ve got some cleanup to do. In Drupal 7, $ has been repossessed from jQuery to prevent namespace conflicts with other JavaScript frameworks. If we want to temporarily take back the dollar for syntactic brevity, we can do it like this:

 

1
2
3
4
5
6
7
(function($) {

    $(document).ready(function() {
        // cornify call
    });

}(jQuery));

So what happens when part of your freshly-styled page gets reloaded on account of on Ajax call? That portion of the page is re-rendered, and your Cornify stylings are all gone. Bummer.

Luckily, Drupal has a framework to circumvent this sort of thing, and it’s called Behaviors. Irakli wrote a post discussing Behaviors a while ago, which is a good overview of behaviors but a little out of date since the upgrade from Drupal 6 to 7.

Behaviors provide a way to register JavaScript functions with Drupal so that Drupal knows to rerun the functions should the DOM change. Let’s make our Cornify function into a behavior:

 

1
2
3
4
5
6
7
8
9
10
11
(function($) {

  Drupal.behaviors.modulename_awesome_cornifier = {
    attach: function(context, settings) {
      $('.stuff_to_cornify', context).each(function() {
          // cornify call
      });
    }
  };

}(jQuery));

The first thing to note here is the presence of the context variable. This defines the portion of the DOM that your script will be running on, and whenever an Ajax call is made through the Drupal Ajax framework, context will contain the region of the DOM that was modified. Neat, right?

The second thing to look at is the settings variable, which contains information defined by you for use by the function, which allows you to parameterize your behavior. To pass a setting to the behavior, we’d make a call like this at some point before the call to include the behavior:

1
2
3
$settings = array('modulename_behaviorname' =>
                  array('setting_name' => $setting_value));
drupal_add_js($settings, array('type' => 'setting'));

One caveat

Behaviors are awesome and all, but because they’re triggered on each Ajax request, that means that your function is running over and over again on the same page an unknown (but probably sizable) number of times. If your function has side-effects that should not be repeated, this isn’t a good thing.

Luckily, there’s a not-too-nasty workaround: the jQuery once plugin, which Drupal includes by default. We can use it like this:

 

1
2
3
4
5
6
7
8
9
10
11
(function($) {

  Drupal.behaviors.awesome_cornifier = {
    attach: function(context, settings) {
      $('.stuff_to_cornify', context).once(function() {
          // manipulating <strong>all kinds</strong> of state
      });
    }
  };

}(jQuery));

Much better

Doesn’t it feel nice having JavaScript that’s modular, pretty, and Ajax-responsive?

James O'Bierne