JavaScript hygiene in Drupal 7

Posted Sep 12, 2011 // 8 comments
James:

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?

About James

James O'Beirne is fascinated with the design, tools, and techniques that go into building quality software. He strives to create aesthetic, succinct solutions that delight clients. 

Before coming to Phase2, James did mathematics ...

more >

Read James's Blog

Comments

by Moshe Weitzman (not verified) on Mon, 09/12/2011 - 16:18

better block example

The modern way to add javascript from a block is with #attached. Your foobar_block_view_alter() is brittle. It will fail for cached blocks. See http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_pr...

by jobeirne on Mon, 09/12/2011 - 17:31

Thanks for the comment;

Thanks for the comment; that's a great catch. I've updated the blog accordingly.

by yakoub abaya (not verified) on Mon, 09/12/2011 - 16:34

global

how should global objects be defined ?

suppose my object is defined in one file : function ListCalc(ul_list) { this.list = ul_list; this.list.children().each(function(index,li_item) { li_item = isNaN(li_item) ? 0 : parseInt(li_item); } } and in another file i want : var mylist = new ListCalc($('ul.scores')); i can define ListCalc outside the function($) wrapper because it uses jquery, but if i do define it inside the wrapper then it can't be used in other files anymore

by ao2 (not verified) on Mon, 09/12/2011 - 16:44

D6 and D7 differences about jQuery

Hi,

just to be sure:

in the article at http://www.phase2technology.com/node/663/ you pointed out, it looks like that Behaviors do not need to be surrounded by (function($) {

}(jQuery));

this was OK because the examples were for D6 right? And in D7 the closure is really needed for the reason you specify about the '$' symbol, isn't it?

Thanks, Antonio http://ao2.it

by jobeirne on Mon, 09/12/2011 - 17:32

Right, the $-aliasing is only

Right, the $-aliasing is only needed in D7.

by yakoub abaya (not verified) on Mon, 09/12/2011 - 17:01

global objects

in order to define global objects, Drupal.js example should be followed : var ProjCalc = {} (function($){ ProjCalc.ListCalc = function(ul_item) { $(ul_item).children().each(function(index,li_item) { ... } } ProjCalc.ListCalc.prototype.sum() { ... } }(jQuery); then in other file : (function($){ var mylist = new ProjCalc.ListCalc($('ul.scores')); mylist.sum(); }(jQuery);

by rbayliss (not verified) on Tue, 09/13/2011 - 09:48

Context

One thing about the context variable that always trips me up is that when you use it in the second argument to a jQuery selector, it will only match what's inside the outer wrapper. So if context is the result of an ajax call that consists of the following markup: ⟨div id="myID"⟩ ⟨span⟩My Content⟨/span⟩ ⟨/div⟩

then searching for it with $('#myID', context) won't return anything.

by jobeirne on Tue, 09/13/2011 - 09:54

Thanks

Good distinction; thanks for posting.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
  • Allowed HTML tags: <a> <strong> <code> <p> <img> <ul> <ol> <li> <h2> <h3> <h4> <b> <u> <i>
  • You may insert videos with [video:URL]

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.