The ELC Community Blog
A knowledge exchange on Ruby on Rails and Agile Development
Create a jQuery plugin with a spin
by dpalm on September 16, 2008
There are many great tutorials out there on how to create plugins for jQuery, the sweetest javascript library of them all, but this one has a twist, or a spin actually.
When developing a webapp we often need to communicate to the user that a certain piece of the UI is "busy" or temporarily inactive. For heavily ajax-ed apps there can sometimes be several concurrent network requests on going at any one time. Sometimes the code we need to know what to mark as busy and what is ready for user interaction again can get a bit messy.
On my current project we're using an animated gif on top of a semi-transparent div overlay and call it a "spinner" (aka "thingy that spins"). Whenever a button, form or whole section of the app is "busy", we display a spinner on it.
I wanted something that could leverage jQuerys powerful selectors, was dead easy to and let us get rid of all that div-hiding/unhiding we had in our code we can "spin" stuff on the page with code like so:
1 $("#some_div").spin(); // add a spinner to the element with id "some_div"
2 $("form.special_forms li").unspin(); // remove the spinners on the LIs that are children of the FORMs having class "special_forms"
3 $("#data_table td").spin() // add a spinner to all TDs in the "data_table" table
Turns out extending jQuery is easy.
The core
1 $.fn.spin = function(){
2 return this.each(function(){
3 // More stuff to follow
4 });
5 };
The above snippet, while incomplete, is the bulk of a jQuery plugin.
The "fn" object allows us to extend jQuerys abilities to work on a collection of DOM elements. In this case we're adding a "spin" method to anything returned by a jQuery selector. Very very powerful.
The second line executes a function for each member of the collection. In our case we want to display a spinner on top of each of the elements our css selector returned.
Singletons
You can also add singleton functions directly to the jQuery object ($) itself:
1 jQuery.log_and_dance = function(){
2 log("It's time to dance now!");
3 dance("waltzer");
4 }
A common pattern is to use a singleton as a means to configure the plugin and implement the functionality as a method on the jQuery object (aliased as "$"), like so:
1 $.Spinner.reset_all_spinners();
2 $("li").spin();
3 $.Spinner.use_class("alternative_class");
Chaining
In my opinion one of the most innovative features of jQuery is the chaining idiom. All jQuery methods return the jQuery object itself, which means that at the end of any manipulation we can be certain we get the jQuery object back, so we can call another jQuery method, and then another and then another:
1 $("#some_container").hide().html("<marquee>I've missed you!</marquee>").fadeIn(300).fadeOut(500).show().spin();
Plugin skeleton
The above plugin skeleton doesn't do that (yet). We need one more piece of jQuery administration for this to work and the syntax might look a bit odd to the untrained eye. It's actually a very neat use of Javascripts anonymous functions. I'm not going to cover it here and now though, so but the interested reader can find out more through Google. Here's the complete jQuery plugin skeleton:
1 (function($){
2 $.fn.spin = function(){
3 return this.each(function(){
4 // Actual spinner code goes here
5 });
6 };
7 })(jQuery);
Spinnin'
Now for the actual spinner code itself, the "meat" we'll integrate with the plugin in just a second.
Nothing fancy here:
- create a div
- give it the CSS styling of your liking to make it "spinny" (animated gif for background, semi-transparent and whatever pleases you)
- position the new div on top of the element we want to mark "busy" in the page
1 var spun = $(this) // The element we're spinning
2 $("body").append(
3 $('<div class="my_spinner_class"></div>').css({
4 position: "absolute",
5 top: spun.offset().top,
6 left: spun.offset().left,
7 width: spun.outerWidth(),
8 height: spun.outerHeight()
9 })
10 );
The offset() and outerHeight/outerWidth methods are provided by jQuery and gives browser independent access to offsets and width/height (duh).
Minimal working plugin
Merging the last two snippets we get a working spinner plugin:
1 (function($){
2 $.fn.spin = function(){
3 return this.each(function(){
4 var spun = $(this) // The element we're spinning
5 $("body").append(
6 $('<div class="spinner_css_class"></div>').css({
7 position: "absolute",
8 top: spun.offset().top,
9 left: spun.offset().left,
10 width: spun.outerWidth(),
11 height: spun.outerHeight()
12 })
13 );
14 });
15 };
16 })(jQuery);
Where the CSS looks like this:
1 .spinner {
2 background: #fff url('../images/spinner.gif') no-repeat center center;
3 opacity: 0.75;
4 filter: alpha(opacity: 75);
5 -moz-opacity: 0.75;
6 -khtml-opacity: 0.75;
7 }
Done
More
There are many improvements that can be made to the above of course, but the intent here is to show off jQuerys extremely efficient syntax and how easy it is to leverage the internal DOM query engine for your own evil purposes.
I have developed the above core to include stuff like:
- configurable spinner class
- unspin() functionality
- debug messages
- fix for the bug in the code above that keeps adding spinners on subsequent calls
- more and better demos
- rails plugin wrapper
Come get it at:
git clone git://github.com/dvdplm/jquery-spinner-plugin.git
Timeline
- ssh tip of the day: faster ssh connection setup!
- Specing Thread Safety In Rspec
- Transform BitmapData into JPEGs (AS3, JPGEncoder, Rails, Rmagick)
- In the Land of the Magic Kingdom you will find... SPORTS!
- ELC Expands to NYC
- Create a jQuery plugin with a spin
- Investigating named_scope
- Liquid Coolness
- Ehcache for JRuby / Rails
- JS Routes plugin
- AWS-S3 gem extensions and Amazon's Copy API
Comments
I was about to write something similar when I found this post – very helpful.
I did find that I had problems with the unspin method. The problem I had is if the block/div that I was spinning over changed height/width, then you show the hidden the previously created spinner div with the previous height and width.
So to fix this, my unspin now always removes the spinner div: