One Plugin to end all jQuery Plugins

Well, I could not resist this catchy title, could I? Perhaps I could. In any case I have this idea and implementation where one jQuery plugin is sort-of-a “socket” (not a network socket, but a socket in the wall) into which you can plug-in your “plugs” which will do standard jQuery related stuff. Where “standard jQuery related stuff” is either changing the state of the dom , or either using the information from the dom to do something with it.

Example of the former is much loved “animation” stuff one can do woth jQuery. Example of the latter, could be collecting some information from dom entities selected with the jQuery and doing something with that information. Sending it to some server or just displaying it would qualify as “doing something with the information”.

As it turns out it it easier to use jQuery for changing the state of the dom, then it is to use information that it can provide from the dom, of the current page. This is because the latter involves your code which has nothing to do with the stuff that jQuery does. Strictly speaking this is not “difficult” , it is just not “natuarly flowing” as jQuery chains of commands do.

[sourcecode language=”javascript” light=”false”] // while we are chaning the state of the dom that jQuery instance is referencing all is nicely flowing
// including having the plugins in the chain, which change the state of the elements referenced
var $JQ = $("some selector").show(500).hide(500).show("slow").some_animation_plugin().some_font_plugin() ; // ad infinitum
// getting the stuff from the elements through jQuery , breaks the chain
var name = $JQ.find(":last").data("name","Jeresig").data("name") ; // have to stop here
var guts = $JQ.html() ; // and here
var msg = $JQ.text() ; // and here
//
[/sourcecode]

Perhaps this looks a bit trivial, and You are perhaps wondering what am I going on about here, and why are you reading this post? You are demanding and now I have to find an convincing use-case , to er… convince You. Ok, here we go. Consider writing a code that will “blanket” any user selected number of elements on the page, by placing one div “over” it. This div has to be just big enough to cover all the elements selected. We can make it transparent and/or write some text on it etc … Convincing enough for You? Good. So we have this clever algorithm which given set of overlapping squares, returns one square position and dimensions that “blankets” them all. Obviously we can imagine an plugin that does all of this. Walks through the current “stack” of selected elements, collects positions and dimensions of each , etc … And that is the point: plugin is required.

Each time you want to take something “out” form the jQuery object it is best to write a plugin that will take that “thing out” and then return it, or do something with it, and then return the jQuery instance (doing: return this;) so that Your beloved chaining is not broken.

[sourcecode language=”javascript” light=”false”] // a general plugin structure for taking "things out" without breaking a chain
jQuery.fn.extend ( {
use_and_proceed : function ( description_of_a_information_to_take_out, callback ) {
// somehow obtain the information
var the_information = somehow_obtain_the_information (description_of_a_information_to_take_out) ;
// use the information obtained
callback(the_information);
return this; /* so that chain can proceed */
}
} ) ;
// usage
$("whatever").use_and_proceed("length", alert ).hide("slow").css("top", 123).hide(500) ;
$("whatever").use_and_proceed(":last[id*=’something’]", alert ).hide("slow").css("top", 123).hide(500) ;
$("whatever").use_and_proceed(":last[~width]", collect_widths ).hide("slow").css("top", 123).hide(500) ;
// Nice. Our application logic is satisfied and jQuery chains are nice and clean…. ?
// Oops ?! Can we write generic somehow_obtain_the_information () , inside the use_and_proceed()
// plugin that will serve all the above ?
// And everything else that can be asked for ?
//….
[/sourcecode]

If we could do the above we could have one (very) nice framework that would allow us to re use the plugin above for virtually “anything”. The actual solution will be in the callback given to the plugin. For example we could try solve the “blanketing” ?

[sourcecode language=”javascript” light=”false”] function blanketing ( position, dimensions ) {}
$("whatever").use_and_proceed ("blanketing position and dimensions", blanketing ) ;
// Hm .. what information do we get out and how ?
// use_and_proceed has to be generic … Maybe we can change it and give "this" to callbacks ?
// for even more usefulness we can optionally select a subset form the current stack referenced by jQuery object
//
jQuery.fn.extend ( {
use_and_proceed : function ( subset_selector, callback ) {
if ( subset_selector ) {
// find and use the subset
callback( this.find( subset_selector ) ) ;
} else {
// or use the current jQuery instance
callback(this);
}
return this; /* so that chain can proceed */
}
} ) ;
// this will work for every callback that understands jQuery object
// Hurah ?! We have generic "mother of all plugins" ! Just give it different callbacks for different tasks …
function blanketing ( jq_object ) {}
$("whatever").use_and_proceed ( blanketing ) ;
//….
[/sourcecode]

Lovely … But. Will this work ? For starters, call to the callback() above may block the plugin or throw an exception. Fine. We can solve that.

[sourcecode language=”javascript” light=”false”] // Plugin : use
// MIT (c) 2010 by Dusan Jovanovic ( dbj@dbj.org )
jQuery.fn.extend ( {
use : function ( callback, subset_selector, error_handler ) {
var THAT = null ;
error_handler || ( error_handler = (window.console || window.alert) ) ;
if ( subset_selector ) {
// find and use the subset
THAT = this.find( subset_selector ) ;
} else {
// or use the current jQuery instance
THAT = this ;
}
var tid = setTimeout( function () {
try {
callback.apply(THAT) ;
} catch(x) {
error_handler(x);
}
} , 1 ) ;
return this; /* so that chain can proceed */
}
} ) ;
//….
[/sourcecode]

Renamed it to “use”. This will execute the callback on a different call-path (not thread), and will also handle the exception thrown , if any. error_handler is either user defined or console or alert.

[sourcecode language=”javascript” light=”false”] function c1 ( ) { alert( this.html() ); }
// use c1 on first element from the current stack
$("#toolbar").use(c1,":first") ;
// use c1 on the current stack
$("#toolbar").use(c1) ;
//….
[/sourcecode]

So far so good?

Perhaps. What is this good for? Ok, for starters what we have here is an “meta thing”. “Meta concept”, overarching abstraction of a familly of abstractions. An “zigot” of meta-programming. In this case “plugin-of-plugins”. I shall call this “Meta Plugin”. Ok, but why is this a “good thing”?

For example , jQuery pluginss are (badly?) missing namespaces. There is no inbuilt classification of plugins, nor is there a valid way to do so. Which in the presence of hundreds of plugins is a problem. In the presence of thousands of plugins it will be (is alredy?) a nightmare. And to change jQuery plugin mechanism itself, is already a nightmare because of legacy plugins.

But, consider if everybody will be using “Meta Plugin” as an template? Simple but ubiquitous measure, which will give an single level hierarchy of namespacing for templates. Simply use this pattern, but in implementing it give it “your name”.

[sourcecode language=”javascript” light=”false”] function c1 ( ) { alert( this.html() ); }
// Imagine that each company and individual is using "Meta Plugin" pattern
// to namespace their plugins.
$("#toolbar")
// use c1 he msft namespace of plugins
.msft(c1,":first")
// use c1 in the goog namespace of plugins
.goog(c1,":nth(2)")
// use c1 in the bbc namespace of plugins
.bbc(c1,":last")
// use c1 in the malsup namespace of plugins
.malsup(c1,":last") ;
// …
[/sourcecode]

All the “Meta Plugin” implementations above are identical, but are named differently. This gurantees (to some degree) re-usability of callbacks between different namespaces. Like the callback c1() in the code above. This approach can be easily standardised by jQuery.

[sourcecode language=”javascript” light=”false”] // MetaPlugin : MIT (c) 2010 by Dusan Jovanovic ( dbj@dbj.org )
jQuery.fn.extend ( { MetaPlugin : function ( callback, subset_selector, error_handler ) { /* implementation same as ‘use()’ plugin above */ } } );
// Above is to be official and inside jQuery itself

// inside Microsoft closure
// msft meta-plugin
jQuery.fn.extend ( { msft : function ( callback, subset_selector, error_handler ) { return this.MetaPlugin(callback, subset_selector, error_handler); } } );

// inside Google closure
// goog meta-plugin
jQuery.fn.extend ( { goog : function ( callback, subset_selector, error_handler ) { return this.MetaPlugin(callback, subset_selector, error_handler); } } );

// inside BBC closure
// bbc meta-plugin
jQuery.fn.extend ( { bbc : function ( callback, subset_selector, error_handler ) { return this.MetaPlugin(callback, subset_selector, error_handler); } } );

[/sourcecode]

Namespacing without namespacing. Again if ubiquitously used this simple mechanism will do a lot of good for jQuery. It is indeed “yet another level of indirection” which makes code slower, but it seems it will be well worth it.

Now. There was/is a lot of discussion on how to develop re-usable architecture for jQuery patterns, how to “sneak in” namespaces, etc. All of this has proven that jQuery plugin namespaces can not be done properly without changing jQuery itself. And jQuery rightly team has not shown inclination to do so.

This “plugin meta architecture” is very unobtrusive and is not stopping anyone indulging in the above kind of plugin mental activity. The only requirement is to move all of that activity and solutions onto the callback object used here. So, one can fire away and do her favourite “ninja” scripting, but all in and around a callback object.

Conclusion

There is an small(ish) set of “official” jQuery plugins which are maintained by jQuery team. Consequently there is de-facto standard on what is a “good plugin”. And consequently to that there is a small list of “official” requirements for such a plugin. Each and every point in the list of “official” requirements for a good plugin one can be solve easily with the properly developed callback object, to be used by MetaPlugin.

[sourcecode language=”javascript” light=”false”] // development of the callback object is important
// (and what is not?)
var my_callback = function ( options ) {
var meta_data,
private_function = function () {} ,
callback = function () {
meta_data = use_meta_data_plugin_or_not ( this ) ;
var some_option = options.something ;
// does not matter what is returned
// MetaPlugin does the rest
} ;
callback.secondary = function () { meta_data ; options.something; /* or whatever */ } ;
callback.default = { s1 : 1, s2: 2} ; // default public settings
return callback ;
}
[/sourcecode]

This is one of the several poosible valid callback object desings. Now let us see how is the callback object above solving the plugin requirements bellow.

  1. Claim only a single name in the jQuery namespace
    1. Solved, by using MetaPlugin
  2. Accept an options argument to control plugin behavior
    1. Using the callaback as above this is easy (too).
      [sourcecode language=”javascript” light=”false”] $("#toolbar").bbc( my_callback({ o1 : 1, o2: 2},":last") ;
      [/sourcecode]
  3. Provide public access to default plugin settings
    1. As made possible with the my_callback above. One just has to learn to use the callback object.
      [sourcecode language=”javascript” light=”false”] // made here or somewhere else
      var callback = my_callback({ o1:1 })
      // just javascript
      var s1 = callback.default.s1
      //…
      [/sourcecode]
  4. Provide public access to secondary functions (as applicable)
    1. [sourcecode language=”javascript” light=”false”] // just javascript
      callback.secondary() ;
      //…
      [/sourcecode]
  5. Keep private functions private
    1. As my_callback does…
  6. Support the Metadata Plugin
    1. As my_callback does, also

I thought it is better to have the solution for the “official” list of jQuery plugin requirements, here than in the comments bellow 😉
Of course one does not have to have all this “scafolding” to use MetaPlugin. Good old simple function will do, most of the time.

–DBJ