After having release the jQuery Plugin Pattern 2.0 I encountered a few situations where the pattern would cause for my plugins to crash and burn - not a good thing to happen with a pattern you are trying to advocate. These were mere failures of that version that paved the way for the improved pattern. Behold version 2.1!
Goals
This pattern tackles a few wish-list items of mine that I could not find an easy solution to using the official jQuery plugin pattern. In this post I'm going to list all the goals I'm trying to achieve with this pattern so you can decide if this pattern is for you.
- Backward compatible with jQuery standard plugin authoring method
- Syntax would be $(selector).plugin(args)
- Make plugin chainable
- Allow this to refer to the selected element
- Allow methods to be called the jQuery way (as parameters to the plugin)
- Syntax would be $(selector).plugin(args)
- Allow Object Oriented Programming inside plugin
- Allow to expose the OOP object instead of the jQuery object
- Allow classical OOP concepts such as inheritance
- Allow classical OOP concepts such as inheritance
- Allow n-depth levels of methods a la OOP
- Allow plugin to act as a namespace
- Syntax would be $(selector).plugin.method()
- Make namespace easily changeable in the event of a naming conflict
- Syntax would be $(selector).plugin.method()
- Allow plugin to be called as a hybrid of standard jQuery mixed with OOP
- Syntax would be $(selector).plugin(args).method()
- Syntax would be $(selector).plugin(args).method()
- Make version a publicly exposed attribute as a standard
- Allow for an easy way to expose public interface
- Allow for a plugin initializer
- Encourage use of closure based getters and setters
- Promote a standardized persistent config accessor
If you have experience writing plugins you will quickly observe some of the above items would be difficult to achieve using the official pattern. I am not saying mine is better or worse, but my goal is to marry Object Oriented Programming with jQuery. That's right: I want to be able the pattern to be a mere wrapper around my Object Oriented code to allow it to be connected to other plugins and to be able to internally take advantage of jQuery's magic - such as its selectors.
Barebone Pattern
Below is a barebone version of the pattern that would usually be copy/pasted into your plugins and then filled in with your own code between the [BEGIN YOUR CODE] and [END YOUR CODE]. Take a time to study the code below and then we will examine a real-life example.
/* jQuery Plugin Pattern 2.1
* Author: Milan Adamovsky
*
* This pattern allows you to write jQuery plugins a lot faster, more consistently,
* and less error prone. Furthermore it allows namespacing, OOP integration, along
* with other technical benefits.
*
* There are two "global" (but local) helper methods available throughout each plugin
* and they are:
*
* getPluginClass()
* getPluginName()
*
* The getPluginClass() returns the definition of the entire plugin code. Usually
* this is not needed by the plugin author (you), but in the rare event you would
* need it, here it is. It is used internally by the plugin pattern.
*
* The getPluginName() returns the name of the plugin's jQuery namespace as defined
* by the initPlugin() method (which is something you, the author, defines).
*
* Attention to JSLint users: this code will not pass due to some advanced paradigms
* in use, though efforts have been taken to minimize these. Priority was given to
* functionality and syntax correctness so that YUI Compressor can properly minify
* the code.
*/
(function($)
{
initPlugin({
name : 'asyncLoader' // plugin name
});
//----plugin class -- BEGIN--------
function getPluginClass()
{
return function (args)
{
// Version - directly exposed to be access via $(...).plugin.version
this.version = '1.0';
// Public Methods - wrappers containing a reference to the actual function
this.someMethodA = someMethodA; // non-chainable method
this.someMethodB = _(someMethodB); // chainable method
this.rc = jQuerify; // required plugin pattern code
// getConfig - always used to contain class/plugin arguments
function getConfig()
{
return undefined; // we define this via setConfig()
}
// setConfig - always used to set class/plugin arguments, usually used indirectly via initConfig
function setConfig(args)
{
getConfig = function()
{
return (args);
};
}
// initConfig - sets default values via jQuery's extend()
function initConfig(args)
{
setConfig($.extend({
chainable : true, // false - this allows us to do something like $(...).plugin(args).method()
// this returns the object as the output
// true - otherwise, by default, it will assume jQuery native functionality of chainability
// this returns what jQuery expects for chaining
properties : {} // any properties that you want to be passed to the plugin.
}, args));
return (this);
}
//---[BEGIN YOUR CODE below]-------
// [Class methods / properties]
function someMethodA(args)
{
alert('This is our non-chainable method!');
return (this); // we return this to allow classic OOP
}
function someMethodB(args)
{
alert('This is our chainable method!');
// we don't have to return anything since the _() takes
// care of chainability.
}
// initConfig - gets called to set defaults for plugin
initConfig(args); // Pattern code - usually just copy/paste
// we put our logic after the method definitions to keep JSLint a
// little happier.
//---[END YOUR CODE above]----
function _ (fn) // local function that facilitates chainability
{
return function (args)
{
return this.prototype.rc(function ()
{
fn.call(this, args);
});
};
}
function jQuerify(args)
{
if (typeof args == 'function')
{
return $.fn.curReturn.each(args); // handles chaining of method
}
else
{
initConfig(args);
return getConfig().chainable // checks for chaining in main plugin
? $.fn.curReturn // returns chainability hook
: $.fn[getPluginName()]; // returns OOP object hook
// getPluginName() is defined in initPlugin()
}
};
};
}
if ($.fn.curReturn === undefined) // checks if a plugin has already loaded environment
{
$.fn.extend({ // if not then it extends jQuery object.
curReturn: null, // declares placeholder
jQueryInit: jQuery.fn.init // saves original jQuery init method
});
$.fn.extend({ // now we overwrite jQuery's internal init() so we
// can intercept the selector and context.
init: function( selector, context )
{
return jQuery.fn.curReturn = new jQuery.fn.jQueryInit(selector, context);
}
});
}
function initPlugin(args)
{
getPluginName = function()
{
return args.name; // sets getPluginName() to return jQuery plugin name
};
// console && console.log(["Initialize", args.name, "plugin."].join(' '));
try // meat
{
var classCode = getPluginClass(), // get a reference to the plugin OOP code
_p = new classCode({}); // instantiate plugin Class (which is OOP)
$.fn[getPluginName()] = _p.rc; // insert instantiated plugin into jQuery namespace
$.extend($.fn[getPluginName()].prototype,_p); // augment jQuery internals
$.extend($.fn[getPluginName()],_p); // same here.
}
catch (error)
{
alert(error); // in case something breaks let us know.
}
}
})(jQuery);
10 comments:
Hi, Milan!
For example: $('#check').asyncLoader();
How in "someMethodA" I can find object for $('#check')?
Milan, this is really interesting stuff. I read your previous posts on v1 and v2. There was a lot of discussion on those but nothing on this for some reason.
How has this pattern been working out for you? Is it the Holy Grail or should we be expecting another version soon?
Have you released any working plugins using this pattern that we could try?
Hello, Milan.
This pattern don't work with jquery 1.5 when selector find several objects.
Hi Milan,
I really like your design goal, but I cannot seem to get the plugins to work with jQuery 1.5.1.
Which version of jQuery are you using? I tried plugin version 1.0, 2.0, and 2.1, they all have some issue.
1.0 Passes the wrong 'this', the plugin does not crash anything, but the rest of the code is not working properly since it is expecting another DOM element (the selected one).
2.0 and 2.1 cause some internal jQuery crash, I copied both plugins, as-is, and i get the following error in Safari ( 5.0.3 ):
TypeError: Result of expression '(e||f)' [undefined] is not an object.
what if I have some public methods for the class that wouldn't need any "selector" to be passed in? For example: $.pluginName('methodName', args);
Hi, I've been very carefully (and slowly) reading your article son jquery plugin pattern. Very interesting stuff for someone just getting into pluin development (and on a deadline :s ).
When using the 2.1 version however, it seems to break jquery. I'm simply loading jQuery, the bare plugin. Then doing a simple :
jQuery(document).ready(function($) {
console.log($("div"));
});
trows an error in jquery.min.js :
"Uncaught TypeError: Cannot call method 'find' of undefined".
Any ideas?
Follow up : the issue seems to only occur with jquery 1.5.x. Switching to 1.4.x fixes the problem. Do you have a fix for this in your pattern ?
Thanks
Sebastien
I have a question: If I use jQuery(selector).myPlugin, I get the jQuerify function, right? This function has all methods of my class, so I can call jQuery(selector).myPlugin.someMethodA(). Cool. But in someMethodA, 'this' refers to jQuerify, right? So how do I get access to jQuery methods, like 'each'?
Here's a solution for using the pattern with jQuery 1.5 and 1.6:
http://ludw.se/blog/articles/19/patching-milans-jquery-plugin-pattern-for-jquery-16
Thanks Ludvig for the patch :) I will take some time and fix it on this page too !!!
Post a Comment