Adventures in Engrish
Posts tagged Plugin
Object Oriented jQuery Plugins Mk 2
Aug 26th
In a recent post, I outlined a method to abstract away the complexity of creating an encapsulated jQuery plugin. However, as was pointed out in the comments, there was a missing piece that didn’t allow for arguments to be passed through. More seriously, there was an issue with the binding of the facade function, which meant that only the last defined public function in the class could be called.
As an aside, if you need to bind a variable at the time that it is defined, it isn’t enough to define an anonymous function with a reference to the closure, as that can change by the time the function is called. The bug in my code was in the piece that bound the facade function to the set member functions.
for ( var i in template) {
if (typeof (template[i]) == 'function') {
result[i] = function() {
this.each(function() {
this[i]();
});
};
}
}
By the time the inner function is called, “i” has already been re-bound to the final function name in the template. The solution is to bind the inner function name to the outer function name at the time of definition, which we can do by wrapping it in (yet another!) function.
// Iterates through the set calling the specified function.
function makeIteratorFunction(f, set) {
return function() {
for ( var i = 0; i < set.length; i++) {
set[i][f].apply(set[i][f], arguments);
}
};
}
and then calling that function:
if (template) {
for ( var i in template) {
if (typeof (template[i]) == 'function') {
result[i] = makeIteratorFunction(i, result);
}
}
}
At this point, although “i” will continue to change as the loop continues, the function call is bound through the closure on “f” in the auxiliary function, which remains fixed. Google Chrome’s developer tools certainly make following all that a lot simpler! I’ve updated the plugin with these improvements, which also enables us to pass arguments through, finally allowing:
$('#foo').myplugin().publicMethodWithArguments('hello');
You can get the updated plugin here. Feedback welcomed!
Object Oriented jQuery Plugins
Aug 22nd
I’ve recently begun the process of porting some javascript library code from Prototype to jQuery, and on the whole it hasn’t been too problematic. I really like the element-centric nature of jQuery, whereas Prototype is more like a excellent set of useful static methods. There were only a couple of things I found myself really missing, and they were enumerables, and elegant plugin encapsulation. The first problem was solved with Xavier Shay’s nice enumerables plugin. The second one was more of a challenge.
The lack of encapsulation for jQuery plugins seems to be a common frustration, and there are lots of queries on the web along the lines of “how do I add public methods to a jQuery plugin”. The best solution I found came from Hector Virgen, which allows you to do something like this:
var pluginInstance = $('#foo').data('myplugin');
pluginInstance.publicMethod();
That’s pretty neat, but I didn’t really like the idea of having to go through the data object each time. A couple of other people in the comments felt the same way, but there didn’t seem to be any solutions forthcoming, so I had a crack. What follows is largely based on Hector’s code, so you should go and read that first before you go through this.
We start off with a basic plugin shell, slightly modified from Hector’s, per my taste:
(function($) {
var Celsus = Celsus || {};
Celsus.MyPlugin = function(element, options) {
// Private members
var elem = $(element);
var settings = $.extend({}, options || {});
// Private methods
function _privateMethod() {
console.log("This is a private method!");
}
return {
publicMethod: function() {
console.log(elem);
_privateMethod();
return true;
}
};
};
$.fn.myplugin = function(options) {
return this.each(function() {
var element = $(this);
if (element.data('myplugin')) {
return;
}
var myplugin = new Celsus.MyPlugin(this, options);
// Store the new plugin definition in a data object.
element.data('myplugin', myplugin);
});
};
})(jQuery);
This is a pretty good start. However, as Hector points out, the main issue is that the plugin returns a jQuery object to enable chaining. In many complex instances, chaining isn’t necessarily something you’re going to want to do, so we make a small sacrifice and forego that convenience. Instead, we are going to return a set of plugin instance objects:
$.fn.myplugin = function(options) {
var result = [];
this.each(function() {
var element = $(this);
if (!element.data('myplugin')) {
// Initialise
var myplugin = new Celsus.MyPlugin(this, options);
// Store the new functions in a validation data object.
element.data('myplugin', myplugin);
}
result.push(element.data('myplugin'));
});
};
At this point, it’s not looking too great. We’ve broken chaining because we no longer return a jQuery object, but if you try and call
$('#foo').myplugin().publicMethod();
it still doesn’t work. This is because, although each plugin instance has the publicMethod() function, they are contained inside a bare array. At this point we could actually do:
($('#foo').myplugin()[0]).publicMethod();
($('#foo').myplugin()[1]).publicMethod();
Or something similar with each(), but this is very messy. We need a bit of syntactic sugar. To achieve this, the next step is to take that array, turn it into something we can work with, and add a facade, so that every public function we’ve just mixed in is presented as an option on the plugin instance set. We do that by adding the following to the plugin definition:
result = $(result);
var template = result[0];
if (template) {
for ( var i in template) {
if (typeof (template[i]) == 'function') {
result[i] = function() {
this.each(function() {
this[i]();
});
};
}
}
}
Firstly, we convert the array to a jQuery object. Then, we look at the first instance in the set and use that as a template. It should contain all the public functions we’ve defined in our definition class, and all the instances are of the same type, so we can safely use the first entry’s template for all of them. We then enumerate through all the public functions and create a proxy or facade function on the set, which simply calls the closure of that function for each element in the set. This ensures that when you call
$('#foo').myplugin().publicMethod();
it is functionally equivalent to:
$('#foo').myplugin().each(function(instance) {
instance.publicMethod();
});
So that’s pretty cool. We’ve avoided namespace pollution, we can mix in any number of public methods, and private members and methods behave as you’d expect them to. It isn’t possible to access public variables in this manner of course, but that is easily remedied by using public getters and setters. We can even add in a reference back to the jQuery object, so we can get chaining back in in some form:
result.$ = this;
Which lets us do:
$('#foo').myplugin().$.addClass('bar');
This is useful when you’re doing plugin initialisation, but thereafter it’s a bit redundant as the actual plugin call simply returns an object which you then ignore. The only final point is that there’s quite a lot of boilerplate going on here just to get set up. In actual fact, the actions are pretty generic, so we can extract all that code and put it in its own plugin. A plugin to generate a plugin!
(function($) {
$.fn.encapsulatedPlugin = function(plugin, definition, objects, options) {
var result = [];
objects.each(function() {
var element = $(this);
if (!element.data(plugin)) {
// Initialise
var instance = new definition(this, options);
// Store the new functions in a validation data object.
element.data(plugin, instance);
}
result.push(element.data(plugin));
});
// We now have a set of plugin instances.
result = $(result);
// Take the public functions from the definition and make them available across the set.
var template = result[0];
if (template) {
for ( var i in template) {
if (typeof (template[i]) == 'function') {
result[i] = function() {
this.each(function() {
this[i]();
});
};
}
}
}
// Finally mix-in a convenient reference back to the objects, to allow for chaining.
result.$ = objects;
return result;
};
})(jQuery);
With this little plugin, our plugin initialisation code is a lot lighter:
$.fn.myplugin = function(options) {
return $.fn.encapsulatedPlugin('myplugin', Celsus.MyPlugin, this, options);
};
Not too bad! The actual code that specialises a plugin is tucked away in a neatly encapsulated object, we have access to all the public methods defined on it and multiple instances can happily live side by side and be invoked separately without trampling on each other. You can grab a copy of the plugin generating plugin from here. This is still new cod and there might be the odd glitch, so if you spot any, or have other ideas, be sure to leave a comment!
Do You Still Want Gengo?
Jun 30th
Gengo was (and remains) a project that I am very fond of. Originally developed for my own personal use to blog in Japanese as well as English, it turned into a fairly full-featured plugin that was used by quite a few people. Unfortunately, time pressures meant that I had to drop support for it a few years ago. Even so, every few weeks I get an email or comment (usually nothing to do with the post it was written against!) asking me to consider restarting development.
It might just be the alcohol talking, or end-of-financial-year madness, but with WordPress 3.0 recently out and a couple of requests already this week, I’m seriously considering starting development again. I do have a couple of projects I want to do though, so I only want to dedicate time to Gengo if it’s going to be used. My Japanese has sadly diminished to the point where I’m practically mono-lingual again, so I’m not going to be blogging in multiple languages any time soon, and it would purely be for others to use. So, if enough people email me or comment in the next week or so, I’ll give it a crack. Second time around, I’ll hopefully be able to design it more cleanly and more quickly, and WordPress’ plugin architecture hopefully hasn’t changed too much while I’ve been away.
So if you want to see a new version of Gengo, please let me know. On the other hand, I hear very good things about WPML, so If Gengo’s time has passed, feel free to say that too! I won’t have hurt feelings, promise
Fixing Mystique for Backtype Connect
Feb 6th
I’ve been playing around with WordPress themes and plugins and one that caught my eye was Backtype Connect, which aggregates discussions from around the web and places them as comments onto the original post. Unfortunately it wasn’t playing nicely with the Mystique theme I’m currently using, with no comments showing and the sidebar not appearing on posts where there was a backtype comment.
It turned out to be a minor error in the Mystique theme, which deals with how CSS classes are assigned to the sections that display the comments. Mystique has the following lines of code at the bottom of its mystique_comment_class function:
$class = join(" ", $classes);
echo apply_filters("comment_class", $class);
Mystique is trying to do the right thing here, by allowing plugins to manipulate the classes with the ‘comment_class’ hook, just like WordPress’ own function. However, the code is turning the $classes array into a $class string and passing that to the plugins. Backtype Connect (and presumably other plugins) expect those classes to be passed as an array leading to the following error:
Fatal error: [] operator not supported for strings
However, because this error occurs within an HTML tag it isn’t rendered correctly, which means it is hard to spot. Nevertheless, it causes PHP to stop executing immediately, leading to half-rendered pages.
Luckily, the fix is simple. Simply join the array after the filter has been called and everything works well. Future revisions of the theme will probably fix this, but in the meantime, you can use the WordPress plugin editor to replace the two lines above in mystique/lib/core.php with the following:
$classes = apply_filters("comment_class", $classes);
echo implode(" ", $classes);
And they should live together in harmony.