Drupal JavaScripting

I was trying to find some docs on how to use Drupal's JavaScript behaviors system to send to some people at work and realized that two years after D6 was released it was still poorly documented. The JavaScript and jQuery page had good examples of how to get JavaScript onto the page from a module or theme but didn't really discuss what to do from that point. I spent some time adding some documentation to the page on drupal.org but wanted to put a copy here for Google's benefit.

After announcing the change on twitter Tim Plunkett pointed out that there were already some D7 docs so incorporated those.

JavaScript closures

It's best practice to wrap your code in a closure. A closure is nothing more than a function that helps limit the scope of variables so you don't accidentally overwrite global variables.

// Define a new function.
(function () {
  // Variables defined in here will not affect the global scope.
  var window = "Whoops, at least I only broke my code.";
  console.log(window);
// The extra set of parenthesis here says run the function we just defined.
}());
// Our wacky code inside the closure doesn't affect everyone else.
console.log(window);

A closure can have one other benefit, if we pass jQuery in as a parameter we can map it to the $ shortcut allowing us to use use $() without worrying if jQuery.noConflict() has been called.

// We define a function that takes one parameter named $.
(function ($) {
  // Use jQuery with the shortcut:
  console.log($.browser);
// Here we immediately call the function with jQuery as the parameter.
}(jQuery));

In Drupal 7 jQuery.noConflict() is called to make it easier to use other JS libraries, so you'll either have to type out jQuery() or have the closure rename it for you.

JavaScript behaviors

Drupal uses a "behaviors" system to provide a single mechanism for attaching JavaScript functionality to elements on a page. The benefit of having a single place for the behaviors is that they can be applied consistently when the page is first loaded and then when new content is added during AHAH/AJAX requests. In Drupal 7 behaviors have two functions, one called when content is added to the page and the other called when it is removed.

Behaviors are registered by setting them as properties of Drupal.behaviors. Drupal will call each and pass in a DOM element as the first parameter (in Drupal 7 a settings object will be passed as the second parameter). For the sake of efficiency the behavior function should do two things:

  • Limit the scope of searches to the context element and its children. This is done by passing context parameter along to jQuery:
    jQuery('.foo', context);
  • Assign a marker class to the element and use that class to restrict selectors to avoid processing the same element multiple times:
    jQuery('.foo:not(.foo-processed)', context).addClass('foo-processed');

As a simple example lets look at how you'd go about finding all the https links on a page and adding some additional text marking them as secure, turning <a href="https://example.com">Example</a> into <a href="https://example.com">Example (Secure!)</a>. Hopefully you can see another important reason for using the marker class, if our code ran twice the link would end up reading "Example (Secure!) (Secure!)".

In Drupal 6 it would be done like this:

// Using the closure to map jQuery to $.
(function ($) {
  // Store our function as a property of Drupal.behaviors.
  Drupal.behaviors.myModuleSecureLink = function (context) {
    // Find all the secure links inside context that do not have our processed
    // class.
    $('a[href^="https://"]:not(.secureLink-processed)', context)
      // Add the class to any matched elements so we avoid them in the future.
      .addClass('secureLink-processed')
      // Then stick some text into the link denoting it as secure.
      .append(' (Secure!)');
  };

  // You could add additional behaviors here.
  Drupal.behaviors.myModuleMagic = function(context) {};
}(jQuery));

In Drupal 7 it's a little different because behaviors can be attached when content is added to the page and detached when it is removed:

// Using the closure to map jQuery to $.
(function ($) {
  // Store our function as a property of Drupal.behaviors.
  Drupal.behaviors.myModuleSecureLink = {
    attach: function (context, settings) {
      // Find all the secure links inside context that do not have our processed
      // class.
      $('a[href^="https://"]:not(.secureLink-processed)', context)
        // Add the class to any matched elements so we avoid them in the future.
        .addClass('secureLink-processed')
        // Then stick some text into the link denoting it as secure.
        .append(' (Secure!)');
    }
  }

  // You could add additional behaviors here.
  Drupal.behaviors.myModuleMagic = {
    attach: function (context, settings) { },
    detach: function (context, settings) { }
  };
}(jQuery));

Dependency injection

In your code you do dependency injection for jQuery, like...

(function ($) {
  ....
}(jQuery));

We really should inject everything we are going to use. For example...

(function ($, Drupal, window, document, undefined) {
  ....
}(jQuery, Drupal, this, this.document));

In this case we are passing in everything we may use inside including window. This is important because some environments (like node) have a different name for the global object.

You'll also notice undefined in the function signature but nothing being passed in for it. This means it's really undefined no matter what. Plus it can be shortened to a single letter variable by a minifier.

If you aren't using one of these variables then you don't need to pass them in. And, by passing them in a minifier can shrink them easier. In Drupal this can have some size wins for as often as we use the global Drupal object.

Definitely

All good points. Actually been doing some of that in my code but I wasn't sure if that would complicate things too much in docs that—I realize now—are targeted at themers. The upside of including that would be that good defaults would be copied/pasted into lots of future code.

Oh, the only thing about doing undefined that way (which jQuery does) is that jsLint will complain about it... if you care about that type of thing ;)

Closures don't always protect the global scope

It's worth noting that it is actually possible to overwrite global variables inside of closures if you don't declare them with 'var' but just access them.

Your examples actually take advantage of this by declaring Drupal.behaviors.myModuleSecureLink -- the Drupal namespace is in the global scope and you are modifying it.

In your first example if you had just done:

window = "Whoops..."

It could have actually broken things in the global scope (except in Firefox and Chrome which don't allow you to redeclare `window`)

Absolutely

You're right, if you don't include the var it'd be treated as a global. I was in a bit of a hurry writing that so I probably oversold the benefits. After reading this I realized the correct name for the technique is the module pattern. Do you have suggestions for a description of the benefits and a better example?

The article you linked to has

The article you linked to has a great description of the module pattern (YUI's blog post about it http://yuiblog.com/blog/2007/06/12/module-pattern/ is also pretty good).

To take a stab at describing why closures can be valuable I'd point at Matt's comment about being able to minify more easily and also suggest some of the definitions from Douglas Crockford's "A Survey of the JavaScript Language" -- http://javascript.crockford.com/survey.html

Variables:

Named variables are defined with the var statement. When used inside of a function, var defines variables with function-scope. The vars are not accessible from outside of the function. [...]

Any variables used in a function which are not explicitly defined as var are assumed to belong to an outer scope, possibly to the Global Object.

Closures:

Functions can be defined inside of other functions. The inner function has access to the vars and parameters of the outer function.

So in my own words: closures are (generally) anonymous functions which allow you to safely create private variables used to perform a particular task. By passing in objects as parameters to the closure function (internalizing their scope), one can more safely use or modify these objects from within the protected scope of the closure without polluting the global scope.

jQuery Once?

// Add the class to any matched elements so we avoid them in the future.
.addClass('secureLink-processed')

Couldn't use this jQuery Once in Drupal 7?

Good call

Humm... yeah I think that'd be cleaner I wasn't aware that that'd been included in D7, and I was just doing a very straightforward D7 upgrade of the example. Thanks for the tip, I'll go update the d.o version.

Update: d.o docs are updated, I'm not going to bother updating these.