BEV Software Articles - jQuery Plugins.

Introduction

I had reached the point with my pages where I was including jquery.js, so it seemed ridiculous to continue using low-level HTML DOM calls in existing code to do what I could do more succinctly in the jQuery environment that was already there. This led me with a certain inevitability toward writing jQuery plugins.

Now there are lots of good plugin tutorials, including the one at jquery.com, but many of them seem to stop before they have covered enough detail, possibly assuming that you know JavaScript well enough to be able to figure it out for yourself. One of my requirements went beyond these, so I had to figure it out. Since I have done so, I should try to promulgate any little thing I have found out.

The plugin in question is one for horizontal scrolling. On my pages, I want to minimize the amount of scrolling down the page that visitors have to do. My solution threw up a few features that are not covered in the primary documentation. In some ways, this is an extension of the stuff you can find on Learning jQuery - What is this?.

The plugin I made has no methods that can be called after it is instantiated. It uses the fairly common jQuery techniques of modifying an existing DOM, and attaching event handlers to existing elements. Then it bows out. I will list the code to start with, and then explain.


// The customary anonymous function
(function( $ ) {

// 1. Not very customary, but we are defining a function, so we can define variables that are local to it.
var scrollpos = 0;
var numdivs = 0;
var slider;

// 2. And we can put our default plugin properties in the same scope
var defaults = {
   scrollWidth:800,          // In px
   paneHeight: 1000,       // In px
   hWidth: 20000,           // In em
   itemClass: 'hs-item',
   fwdBtnId: 'hs-fwd',
   bkBtnId: 'hs-bk'
};

// 3. This is straight out of the book, as per the jQuery web page documentation
$.fn.hScroll = function(options)
{
   if (options)
      $.extend(defaults, options);

// 4. But I don't see many examples of this, though nested functions are fine, and like other
//    JavaScript functions they are first-class citizens.
   function scroll(fwd)
   {
      if (fwd)
      {
         if (scrollpos == -defaults.scrollWidth*(numdivs-1))
            return;
         scrollpos -= defaults.scrollWidth;
      }
      else
      {
         if (scrollpos == 0)
            return;
         scrollpos += defaults.scrollWidth;
       }
       var t = { left: '' };
       t.left = scrollpos+"px";
       slider.animate(t, "fast");
   }
   function scrollAbs(n)
   {
       scrollpos = -(numdivs-n)*defaults.scrollWidth;
       slider.css("left", scrollpos+"px")
   }

   // Here we start the implementation bit
   numdivs = this.length;
   this.wrapAll('<div id="hs-slider___" style="width:'+defaults.hWidth+
                        'em; position:absolute; vertical-align:top;" />');
   slider = $('#hs-slider___');
   slider.wrap('<div id="hs-pane___" style="position:relative; overflow:hidden; width:'+
                             defaults.scrollWidth+'px; height:'+defaults.paneHeight+'px;" />');

   // Line up the items within the slider DIV
   $(this).each(function()
   {
      $(this).attr("style", "float: left; vertical-align:top; width:"+defaults.scrollWidth+"px;");
   });

//6.  Now reach out to existing elements to add events that allow us to control this thing
//    Notice that we can pass parameters to the event handler. The JS compiler will make snapshot
//    copies of any data it needs - that's the nature of a closure.
   $("#"+defaults.fwdBtnId).click(function() { scroll(false); });
   $("#"+defaults.bkBtnId).click(function() { scroll(true); });

   // Find things that might want to scroll to a particular item directly - more snapshot data.
   $(".hs-link").each(function()
   {
       var n = parseInt(this.id.substring(1));
       $(this).click(function() { scrollAbs(n); });
   });
};
})( jQuery );
This whole definition is wrapped in the

(function( $ ) {
...
})( jQuery );
thing that is generally described in the jQuery documentation as a closure. But I want to be a little pedantic about that. In this case, it is a closure, because at several points we hand out the addresses of functions. The fact that we use the wrapper though is probably not sufficient for the construct to qualify as a closure.

If all we did within it was to change the background colour of a few elements, then there would be no need for the compiler to treat it as a closure. All state information could be dumped as soon as the execution of the anonymous function completed. To be even more pedantic, a really clever compiler would also be able to figure out if a function that was handed out referred to any state variables within the wrapper. If it didn't, then the same applies - state information could be dumped. I think that in practice, most compilers for languages that support closures will retain the state information if a function address is handed out.

To expand on my numbered comments:

  1. Most examples don't illustrate this, but the anonymous function is just like any other. We can define local variables before or in between function definirions and executable instructions.
  2. That includes our customizable parameters - they are just variables like any other.
  3. Here we have made this into a closure, since we have handed out the address of a function.
  4. The scroll() function is nested within the main function here. I would work just as well if we had defined it in the same context as the variables like scrollpos.
  5. Here we hand out more functions as event handlers, As we do so, the compiler is obliged to take snapshots of the current state, so that the arguments to the nested functions are effectively reduced to constants each time we hand out a handler.
jQuery Plugin Alternative Implementations.

The above illustrates the method of creating a jQuery plugin where the following technique is used:


(function( $ ) {
   $.fn.hScroll = function() { ... };
})( jQuery );
There is of course an alternative. You can do something like:

(function( $ ) {
   $.somethingElse = function() { ... };
})( jQuery );
Where you attach a new method directly to the actual jQuery object. It's all down to the way you decide you want to provide context information to a function, as in:

myObject.memberFunction();      // access to data via this.dataItem
staticFunction(myObject);          // access to data via myObject.dataItem
My example today illustrates the two alternatives.

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
var myData = { a: 42, b: "universe" };

(function( $ ){
$.fn.oneWay = function(data)
{
   return this.html("First way - ID of my target is "+this.attr("id")+
                        ", my data is a: '"+data.a+"' b:'"+data.b+"'");
};

$.otherWay = function(target, data)
{
   return $("#"+target).html("Other way - ID of my target is "+
               target+", my data is a: '"+data.a+"' b:'"+data.b+"'");
}
})( jQuery );

$(function()
{
   // There's nothing I want to do at this point, the data is chosen by the user
});
</script>
</head>
<body>
<button onclick="myData = { a: 3.142, b: 'Pi' };">Choose alternate data</button>&nbsp;
<button onclick="$('#target').oneWay(myData).css('backgroundColor', '#aaffaa');">First way</button>&nbsp;
<button onclick="$.otherWay('target', myData).css('backgroundColor', '#ffaaaa');">Other way</button>
<p>
<div id="target" style="width:600px; height:20px; border:solid 1px;">
</div>
</body>
</html>
Here it is:

As you can see, it is a puzzling choice, and I think that at the end of the day it boils down to the look and feel of the semantics you want to achieve. You can get jQuery chaining either way.