Adventures in Engrish
Posts tagged Javascript
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!
CSS Technique: Morning Sunset
Mar 10th
After coming across an excellent article on generating full page images, I had the idea that it would be cool to blend between multiple images as a user scrolled down the page. The somewhat artistic conceit that a user could scroll a similar scene from morning to sunset, ironically came to me at sunset on Saturday and was finished by morning!
The effect is pretty simple to set up, though does require a specific bit of markup. We start with 2 images and set a z-index on them, such that the first one is in front of the second. With the full page image CSS rules, the first image fills the screen and the second is completely obscured.
<img id="morning" class="bg" style="z-index: 2;" src="morning.jpg" /> <img id="sunset" class="bg" style="z-index: 1;" src="sunset.jpg" /
For the CSS, In addition to the standard full page image rules, I add an additional higher z-index, plus a little bit of transparency This allows the content to sit on top of both of these images, and for the effect to be more pronounced.
div#content {
/* This is the only important rule */
/* We need our content to show up on top of the background */
position: relative;
z-index: 10;
/* Added some opacity to demonstrate the effect better */
opacity: 0.8;
filter: alpha(opacity=80);
}
So far, so what? We’ve arranged for a user to download an image he can’t see – not so good. The magic comes with a blending function tied to the scrollbar. The idea is that the top layer becomes more and more transparent as the user scrolls through the content.
In working with the Zend Framework, I’ve been getting to grips with Dojo and its supporting classes, so I was happy to see that Dijit had tools for getting the dimensions of the viewport. With this information, I was able to calculate the scroll ratio, which gave me a number ranging from 0 at the top of the page to 1 at the bottom.
Dojo also has a great style() function, which allows you to set opacity and have it “just work”, across all browsers, regardless of their non-standard filter() shennanigans. At the outset, the top layer image has an opacity of 1. Subtracting the scroll ratio from this allows it to be fully opaque at the top of the page, and fully transparent at the bottom.
dojo.subscribe("/window/scrolled", function(e){
// Calculate the scroll percentage, and adjust the opacity of the top layer, appropriately.
var vp = dijit.getViewport();
dojo.style("morning", {
"opacity": 1 - (vp.t / (document.documentElement.scrollHeight - vp.h))
});
});
There is some rate-limiting code going on to prevent the event firing continuously and slowing down the page. That came from a helpful Dojo Cookie by Peter Higgins over at Dojo Campus. I haven’t played fully with the rate limiting yet, but 50ms seemed to give a reasonable balance of subtle movement without overloading the page.
Degradation is variable – without Javascript, the user just sees the top image and there’s some overhead of the second image that is never seen. Without CSS, there’s more of a problem, as the images are inlined in the page, which pushes all the content down. If there is a full page image solution using background images only, I’d love to hear about it.
There’s a working, self-describing example of the effect so you can try it out for yourself. It works best in Chrome, with its superior Javascript handling, but works in all modern browsers to a reasonable degree.
The next step is to generalise the code, so that I can pass it an array of image URLs and have it automatically build the markup necessary to generate the effect. With more work on the blending function, I’ll be able to have it blend between multiple layers and potentially follow different rates – I might investigate Dojo curves for that.
I’m not aware of this technique being described anywhere else, but if there are other approaches to doing this, I’d be interested to see them.