Staying sane with multiple, parallel version of jQuery in Drupal and supporting 3rd Party jQuery plugins
Staying sane with multiple, parallel version of jQuery in Drupal and supporting 3rd Party jQuery pluginsMonday, 21st Jan 2013The problem
As each major version of Drupal ships, the version of jQuery bundled with it in core is frozen. While this may be decoupled in the future, it's certainly true for Drupal 8 and below.
Upgrading jQuery to a newer version without extensive testing can break core functionality, so it is generally not recommended. This is exactly what the jQuery Update module attempts to do, it upgrades jQuery as much as possible before things in core start to fall apart. This approach is the simplest for a site builder to implement (glad I'm not maintaining that module though...) but is still has obvious limitations.
Integrating Third Pary jQuery plugins is not uncommon when working on a new project but the newer the plugins and the older the major version of Drupal you're working with, the more likely you're going to run into a fundamental incompatibility.
The only way to be guaranteed to always have the right version of jQuery on-hand for both Core and any arbitrary plugin at the same time is to learn how to run multiple versions of jQuery in parallel.
The solution
Luckily, jQuery natively supports running multiple versions in parallel through the $.noConflict()
method, and Drupal can be coaxed into rendering scripts in the required order to preserve its core jQuery functionality through a few different methods.
Unfortunately, if you're new to working with multiple, parallel jQuerys and/or javascript closures there is still a bit of a learning curve before the process becomes manageable. Simply having both jQuery objects initialised doesn't help you know when to use each one or how to ensure that your plugins are using the right version.
Step 1: Including the new version of jQuery
First read the core documentation on the subject.
If that looks too hard then there's a contrib module called jQuery Multi that is supposed to setup multiple jQuery objects for you. I haven't used it because:
- I have a "lite" version of that module already written into hooks in my template for new projects
- I don't like the idea of including an extra module for this functionality as I'm trying to avoid module-itis in new projects
That said, I can't see how it could possibly fail to do the job based on what I read in its source code. It is basically (in D7 at least) a wrapper around hook_js_alter()
to do more or less what the core documentation suggests after attempting to automatically detect different versions of jQuery in the file system.
The important thing is that whatever you do, you need to understand what $.noConflict()
actually does to avoid headaches down the track. It's very simple, really.
When jQuery is bootstrapping itself into existence one of the very first things it does is this (taken from jQuery 1.9.0):
// Map over jQuery in case of overwrite
_jQuery = window.jQuery,
// Map over $ in case of overwrite
_$ = window.$,
The $.noConflict()
method looks like this:
noConflict: function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
},
In this scope "jQuery" being returned is the current version of jQuery. So even if you don't use $.noConflict()
jQuery has a "cached" version of what used to be the values of the two variables that it creates/overrides. When you call $.noConflict()
you can think of jQuery as deleting itself, re-instating its cache of what used to be in the space that it was using and then returning a backup of what was deleted. It is your responsibility to save the backup returned in a useful variable if you want to keep using this version of jQuery.
Ie.
// Just "deletes" the version of jQuery currently aliased to the $ variable
// and re-instates what used to be there.
// This is sort of useless.
$.noConflict();
// Safely stores the "backup" of the current $ variable in another
// variable so we can use it later. This means we can bootstrap several versions
// of jQuery and save some of them to "backup" variables.
// This presumably saves a "backup" of jQuery 1.9 to a variable "jq19"
jq19 = $.noConflict();
Step 2: Ensure that your 3rd Party plugin is using the right version of jQuery
Now that you've saved a new version of jQuery to a variable, let's assume $jqNew
since I don't know what version you're using, you need to make sure everything "third party-ish" is using the new version and everything "core-ish" is using the old version.
Presuming you didn't touch the core version when you were doing step 1 it should be accessible through both $
and jQuery
variables.
When a jQuery plugin defines itself it will usually "extend" one version of jQuery and out-of-the-box it is rather unlikely to be extending the one bound to the $jqNew
variable. Since you can only use the plugin with the version of jQuery that it is extending, it's important to ensure that it is using the newer version that you've installed.
Unfortunately, it is impossible to know for sure how a plugin is referencing the jQuery object without opening up the plugin source code and having a look. Plugins will usually be distributed with a "development" version with lots of inline documentation and easy to read code and a "production" or "minified" version that is pretty much impossible for a human to read but has a much smaller file size. We need to make some minor modifications to the plugin file. You'll need to reference the development version in order to make modifications to the minified version, or you could modify the development version and re-minify it yourself if you know how.
When you open up your plugin you should see something like this not too far from the top of the file:
// Example 1: Simplest, not really "best practice" though.
// FYI, this is a "red flag" that the plugin might be shit.
$.fn.newPlugin = function() { ....
// Example 2: Equivalent, ever so slightly better.
jQuery.fn.newPlugin = function() { ....
// Example 3: This is more common. The plugin is wrapping itself in a "closure"
// to prevent the global namespace being polluted. You should be doing this too
// with your own code.
(function($, window, undefined) {
$.fn.newPlugin = function() { ....
})(jQuery, window);
// Example 4: This is a simplification of what happens in jQuery Mobile.
// Not sure if this is overkill or a required workaround for some issue with mobile devices.
(function(root) {
(function(jQuery) { ... plugin here ... })(root.jQuery);
})(window);
The first three examples can be accommodated without editing the plugin directly simply by wrapping the plugin code in a closure. This works nicely for both minified and development code.
// Works for example 1:
(function($) {
... plugin here ...
})($jqNew);
// Works for example 2 & 3:
(function(jQuery) {
... plugin here ...
})($jqNew);
The fourth example requires us to be a little more invasive. The plugin is explicitly referencing the version of jQuery bound to the window object ie. Even if we were to wrap it in a closure it would completely ignore the local version of jQuery inside the closure.
We have to edit the file to tell the plugin to use our version of jQuery bound to $jqNew
in the global scope rather than jQuery
. The result would look something like this in the unminified version:
(function(root) {
(function(jQuery) { ... plugin here ... })(root.$jqNew); // See what we did there?
})(window);
Sometimes it can be tricky to edit the minified version to match this behaviour. Ummm, hang in there >.< use a little trial and error if you need to in order to get it right.
Step 3: Use closures to upgrade your code without re-writing it
This can be an easy way to get a little performance boost if you're using a really old version of jQuery like the one shipped with Drupal 6.
Say you have some code that looked like this previously:
var element = $('.selector');
// Do stuff with element...
Provided that it doesn't interact directly with javascript behaviours defined by Drupal core or contrib modules you can make it run faster just by doing this:
// $ in this closure is version 1.9.0 of jQuery
(function($) {
var element = $('.selector');
// Do stuff with element...
})($jqNew);
For the sake of the sanity of future generations of coders taking over your project please make sure to document the version of jQuery you're expecting $
to be aliased to inside your closure.
Step 4: Be aware that you cannot interact with Core or Contrib with your new version of jQuery
This might seem obvious but it's important to realise that jQuery behaviour and events triggered by Drupal core and contributed modules will not be compatible with the new version of jQuery.
As an example, let's say that you want to respond to completed AJAX requests initiated by a contrib module like Views. If you wrapped everything in a closure as in Step 3, this won't work as $().ajaxComplete
will never fire:
(function($) {
var element = $('.selector');
// Do something with element...
// Respond to a complete AJAX request initiated by Views
$({}).ajaxComplete(function(e) {
// Do something when views AJAX completes
});
})($jqNew);
The easiest way that I've found to work with both versions simultaneously is to pass in the "new" version as $
or $new
or similar and the old version as jQuery
into your closure. For example, this is a working version of the previous example:
// $ is version 1.9.0 of jQuery
// jQuery is Drupal's core version of jQuery
(function($, jQuery) {
var element = $('.selector');
// Do something with element...
// Respond to a complete AJAX request initiated by Views.
// Use the same version of jQuery as Views.
jQuery({}).ajaxComplete(function(e) {
// Do something when views AJAX completes
});
})($jqNew, jQuery);
Syndicate: planet drupal