Working with JavaScript in Drupal can be a sometimes inconsistent experience, making the already important pursuit of organized code a bit more acute. This post chronicles a bit of exploration I’ve been doing on this topic. It waxes tangential, but that’s alright, as tangents are the space we sometimes discover larger problems and better solutions. It begins like this.
I’ve been trying to become a better JavaScript developer. As a front-end developer, JavaScript is a topic that cultivates programming skills in a language often used to craft spectacular interaction design, something any good front-end developer should be very interested in. In conjunction with the obvious deepening of my JavaScript knowledge, one of my areas of focus is the organization of JavaScript code, specifically within Drupal.
Organization is something we give a lot of thought to at Aten Design Group. We are fairly collaborative, and within this context, the merits of thoughtfully organized code are self-evident. We strive for clarity in the arrangement of our Drupal modules, the naming conventions used for our SASS components and the verbosity of our GIT commit messages and branches. Another such space is in the organization of JavaScript within our Drupal themes and modules.
Enter the JavaScript Module Pattern (not to be confused with a Drupal module, though the idea of reusable code persists). Rather than lumping code into a gargantuan main.js, something one frequently sees in theme development, we can create thoughtfully named and reusable JavaScript modules. It gives us a way to write and encapsulate JavaScript functions in a manner that denotes their specific usage, leading to a more easily maintained project.
For instance, we can write a JavaScript module that loads content into a modal popup box, independent of what content will actually be loaded. We can then use the module in, say, js/main.js where we’ve noted that on such and such page, such and such form should be loaded and displayed to the user, while on such and such other page, such and such other form should instead be displayed. When done right, debugging problems with the aforementioned interaction can be minimized, something intrinsic to better code organization.
return myJS;
}());
// This is js/main.js
Drupal.behaviors.myModule = {
attach: function (context, settings) {
// Use the terribly useful JavaScript Module.
myJavaScriptModule.moduleMethod();
}
};
*Note, this assumes that these files have been loaded in the right order.
Now, JavaScript modules can be somewhat sticky, in that they still don't solve some of the larger issues inherent to JavaScript. A much espoused creed in JavaScript is "Don't Pollute The Global Namespace! Exclamation!". JavaScript modules generally export their value into the global namespace, and while this isn't always disastrous, it can lead to strange bugs or code implosions. This is particularly true when working with code you didn't necessarily write, or when you expect your code to be reused by strangers. Another issue that persists is the handling of dependencies. If your JS module needs another JS module to function, you better make sure they are loading in the correct order. Now imagine this problem snowballing as one breaks more and more code out into JS modules.
These issues, amongst others, are addressed if one takes JS modules a step further, with Asynchronous Module Definitions. Dependency management is handled, as the function won't execute until all of its dependencies have loaded. Additionally, JS modules are loaded asynchronously, which means a more performant JavaScript experience.
define(['aDependency’], function (aDependency) {
// Define the module value by returning a value.
return function () {};
});
We’re getting warmer, but this is where things begin to get a bit sticky when working with Drupal, as once we have an AMD specified piece of code, we need a means by which to load it. A likely candidate for AMD module loading is RequireJS, but using RequireJS within Drupal is not simply a matter of dropping it into your theme or module. And thus, the ramble. It begins like this.
JavaScript Architecture in Drupal - Moving Forward
In Drupal, JavaScript lives all over the place. It exists in Drupal core, contributed and custom modules, and in Drupal themes. This makes for an interesting problem, in that JavaScript is not the sole problem domain of front-end developers, as it is often described. We have a PHP function for interacting with this, in the form of drupal_add_js(). This aggregates all JS into an array, where JavaScripts are loaded based on their respective weight in the array. Controlling these weights can be very cumbersome, particularly for the front-end developer.
Essentially, Drupal's JavaScript architecture has a completely unique and sometimes eccentric API for interacting with a larger problem domain. This brings to mind a term I’ve come across in the time I’ve been a Drupal developer. The term is Drupalism. A Drupalism is something that is needlessly unique to Drupal, in that it is a weird way we Drupal developers do things. Now, I’m not saying outright that drupal_add_js() is a Drupalism, but the JavaScript architecture of Drupal is not something one can instantly acclimate to. JavaScript in Drupal is not as malleable as it ought to be. Derivative of this is the fact that, as a front-end developer, I cannot easily update jQuery or use RequireJS for loading AMD modules.
As Drupal 8 continues to take shape, Drupal developers should more loudly discuss the management of JavaScript in Drupal. There are a few threads on drupal.org that discuss AMD in Drupal. And though it may be too late this go-around, we should consider creating a more pluggable JavaScript architecture, perhaps leveraging RequireJS for module loading. It occurs to me that implementing a widely used system like RequireJS might attract new front-end talent to Drupal. This would be analogous to a noted benefit in the decision to add Symfony2 to Drupal core, in that “The use of widely used libraries and techniques makes Drupal more approachable to new developers.”
Onward, to RequireJS
Though we cannot immediately reap all the benefits of AMD JavaScript modules and RequireJS in Drupal, we can still use these tools in a meaningful way. There is overlap between Drupal’s JavaScript architecture and RequireJS, but they aren't mutually exclusive. If our corner of Drupal (theme or module) can benefit from better JS organization, we can arrange and load AMD modules, and then funnel this conglomerate to the extant JavaScript API with drupal_add_js().
There are a great many tutorials on using RequireJS, and in an effort to keep the scope of this already rambling post a bit more concise, I’ll leave it to the reader to look to those. Of particular note is r.js, which will optimize AMD performance. Here, I’d like to describe special considerations in implementing RequireJS from within a Drupal theme.
Once we’ve arranged our JavaScript using the AMD pattern, we need only add the RequireJS script tag to our theme in order to load these modules. This can be done from template.php, as follows:
<br /> /<strong><br /> * Implements hook_preprocess_html().<br /> */<br /> function mytheme_preprocess_html(&$vars) {</p> <p> drupal_add_js(drupal_get_path('theme', 'prototype') . '/js/require.js', array('every_page' => TRUE, 'external' => TRUE));<br /> }</p> <p>/</strong><br /> * Implements hook_preprocess_html_tag().<br /> */<br /> function mytheme_preprocess_html_tag(&$vars) {<br /> if (isset($vars['element']['#attributes']['src'])<br /> && $vars['element']['#attributes']['src'] == '/' . drupal_get_path('theme', 'mytheme') . '/js/require.js') {<br /> $vars['element']['#attributes']['data-main'] = '/' . drupal_get_path('theme', 'mytheme') . '/js/main';<br /> }<br /> }<br />
This approach, though not perfect, is still relevant if one plans on implementing more complex JS in displaying or manipulating Drupal data. There are decisions to be made about what kind of interactions should occur with the larger architecture, but one tangent is enough for today. For now, it’s nice to know that RequireJS and drupal_add_js() can get along as we pursue a clean JavaScript architecture in Drupal.