How to use the new jQuery.sub to protect yourself from monkey-patching plugins

You may have heard that jQuery 1.5 has a rewrite of the ajax module making it easier to extend the built in functionality. You may also have heard that the change caused an issue when using the jQuery.validate plugin. This issue is that when using the validate plugin, many ajax requests stop working properly (try to go over JSONP).  Ironically enough this is because the validate plugin needed to extend the functionality of jQuery’s ajax module, but it did so by monkey patching the ajax method, making several assumptions about the api along the way.  On down the line (jQuery 1.5 for example), those assumptions are proven to be incorrect, something changes, and your code is broken.  It’s a pretty good case study for those in the “Don’t modify objects you don’t own” camp.

If you’re wanting to use both 1.5 and validate, fear not, jaubourg (author of the ajax rewrite) has forked the validate plugin adding in the minor change to not break ajax.  This plugin uses feature detection so it actually works against 1.5 as well as 1.4, so you can just use it as your master plugin.  It is my opinion that getting the corrected source is the best way to handle this conflict.  That said, there’s another feature in jQuery 1.5 called “sub” that I thought would be interesting to explore, and in this post I will describe how to use it to work around the existing broken jQuery validate plugin.

jQuery.sub is a method designed to provide a means to allow naming flexibility in your apis without risking naming collisions and to provide a means to safely monkey patch jQuery.  Now you may be thinking that I could rewrite validate now to use this new method, minimizing the code that is changed to validate.  I find this less interesting, so I didn’t do this.  Let’s say, though, that you have a strict requirement that you must use the jQuery.validate plugin off the Microsoft cdn and there’s absolutely nothing you can do about that requirement.  You must use the old code.  Well, you can actually do this safely using jQuery.sub and a little fancy footwork.

What you do is use sub to create an evil twin for jQuery and pass that twin to jQuery validate.

<script type="text/javascript">
(function () {
   var old = jQuery,
   jQuery = jQuery.sub();
   jQuery.oldjQuery = old;
 }());
 </script>
 <script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.7/jquery.validate.js" type="text/javascript" ></script>
<script type="text/javascript">
 //move validate over and switch back
 jQuery.oldjQuery.fn.validate = jQuery.fn.validate;
 jQuery = jQuery.oldjQuery;
</script>

Then, after validate has been added, we steal the validate method off our twin (in the code, there’s actually a superclass property that would do all the oldjQuery storing for me, but it’s undocumented) and switch in the real jQuery.  Essentially, this is the same thing as rewearing a dirty tshirt by flipping it inside out.  It’s “okay” as long as the dirty outside of the shirt doesn’t touch your clean body (Did I mentioned jaubourg’s fork?).  Validate was only able to monkey patch our evil twin, so our non-validate code is in the clear.  Additionally, validate will always access the evil twin via a closure, so it will continue to work as expected.

Validate, like pretty much every jQuery plugin, passes in a reference to jQuery and references that object in the plugin.

 (function ($) {
    //internals go here...
 }(jQuery));

This value of “jQuery” is frozen in time because it is a local value passed into the method.  If the author had instead chosen to access the global jQuery, this hack would not have worked.   Because it is a method parameter, though, all the internal code will point to that value of jQuery via the various closures in the validate code.

Now, there is still one remaining issue in the code as remote ajax validation (such as “username must be unique”) is still broken.  The problem is that validate assumes jQuery.ajaxSettings contains all the default values for jQuery.ajax.  This isn’t the case, though, so it ends up accidentally passing in information that indicates it wants to use JSONP.  To work around this, we can just change the jQuery.ajaxSettings of our evil twin so that it satisfies the expectations of validate.

<script type="text/javascript">
 (function () {
   var old = jQuery,
   ajaxDefaults = jQuery.extend({}, jQuery.ajaxSettings);
   delete ajaxDefaults.jsonp;
   delete ajaxDefaults.jsonpCallback;
   jQuery = jQuery.sub();
   jQuery.oldjQuery = old;
   //set up defaults with no jsonp
   jQuery.ajaxSettings = ajaxDefaults;
 }());
 </script>
/*...*/

The important thing to remember is that jQuery.sub uses mixins to create its subclasses instead of using prototypal inheritance.  This means that validate is passed the same ajax method that exists in real jQuery.  To put it another way, both jQuery and evilTwinjQuery.hasOwnProperty(“ajax”) and jQuery.ajax === evilTwinjQuery.ajax.  It also means that if it were to add in extensibility points such as through css hooks, it would add in those points into real jQuery as well (so maybe the evil twin metaphor isn’t so apt).  What happens after the monkey patching, though, is evilTwinjQuery has a new ajax method that then calls the real jQuery.ajax method.  Also, it has a different ajaxSettings that it uses to call the real jQuery.ajax.

Finally, in the case of validate, it’s not a terribly big deal to use this hack as validate returns a non-chainable object specific to validate’s api.  Had it returned the jQuery object from the method, then you’d need to be more aware of when you were expecting to talk to jQuery or evilTwinjQuery.  Either way, this thought experiment gave me a better idea of how jQuery.sub works and what it can and cannot do to insulate you from plugins.  Final script is below and you can also see the code with my test “services” in this gist:

<script type="text/javascript">
 (function () {
   var old = jQuery,
   ajaxDefaults = jQuery.extend({}, jQuery.ajaxSettings);
   delete ajaxDefaults.jsonp;
   delete ajaxDefaults.jsonpCallback;
   jQuery = jQuery.sub();
   jQuery.oldjQuery = old;
   //set up defaults with no jsonp
   jQuery.ajaxSettings = ajaxDefaults;
 }());
 </script>
 <script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.7/jquery.validate.js" type="text/javascript" ></script>
 <script type="text/javascript">
 //move validate over and switch back
 jQuery.oldjQuery.fn.validate = jQuery.fn.validate;
 jQuery = jQuery.oldjQuery;
 </script>
Advertisements

About joeldart

I am a 29 year old software developer living in Indianapolis, IN.
This entry was posted in dev, jQuery. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s