Loading...

Gracefully Degrading Widgets, Part 2: Navbars & jQuery Plugins Comments

I recently met up with a friend and former coworker to help him ramp up his jQuery skills quickly. In the process I got to play with a few things I hadn’t bothered to pick up previously myself!

First of these was Google’s Ajax Libraries API. While worthless for when you need to be offline (as I ended up being, since the Borders where we met has no free wifi, and I am cheap/skint), it’s nice and easy to include scripts on your pages without needing to maintain the files yourself. Luckily I thought to download local copies and installed Google Docs offline shortly before leaving to meet with the dude.

The second interesting thing I picked up, I actually picked up after the meeting, as I both wanted to learn it and wanted to share with my friend. That something is How to Author a jQuery Plugin, which is what we’re going to look at today. We’ll be building a flexible drop-down navigation menu which will work with or without JavaScript thanks to CSS.

Here’s the working example: MultiNav

Previously, I discussed some of the fundamental ideas behind Gracefully Degrading Widgets, but let’s quickly delineate them again here:

  • Focus on content above interactivity – if that means sacrificing the latter for the former, so be it
  • Present most important content first – sometimes it is necessary to hide some content to maintain page layout, the most important still shows
  • Write minimalist markup – this leads to fewer chances of screwing up and makes it easier on any consumers of your code
  • Write stylesheets to hide interactive content by default, and override in your JavaScript

We’ll also be focusing on reusability and flexibility, and we’ll discover that part of this is covered by virtue of the fact we’re creating our widget as a jQuery plugin. jQuery plugins are automatically applicable to any selector style available in jQuery, and so our widget will immediately be available to instantiate among common classes of a page, or to elements with individually unique IDs. You can even apply multiple instances on the page at simultaneously. In addition to these flexible instantiation capabilities, we’ll be building the widget to take a number of options to use different effects, use different classes if the default class names are not appropriate or would conflict with existing names on the page, and so on.

On with the show already! First off let’s create some simple markup for our drop down menu and style it w/ CSS:

Nice and simple!

  • { margin: 0; padding: 0;} .nav { margin: 50px;} .nav li { display: block; float: left; background-color: #ccccee; height: 50px; line-height: 2em; position: relative;} .nav li ul { display: none; position: absolute; top: 50px; left: 0;} .nav li a { display: block; text-decoration: none; padding: 10px;} .nav li a:hover { text-decoration: underline;} .nav li:hover ul {display: block; } .nav li ul li {display: block; float: left; width: 130px;} Still simple!

Even with this alone, the example code will show simple hover-over submenus via CSS, in browsers that support it (all but IE6, I believe). This is where degradation comes into play. We want things to fail gracefully, so each of those top-level menus should be a link in and of itself, rather than slamming all the links into the submenus. This is a good failover.

So now that we’ve got simple menus, let’s add some action!

To animate a single submenu’s intro into the page, we might use code similar to the following:

$(“.nav:first > ul > li:first > ul”).fadeIn(); Which uses an advanced CSS3 selector to grab the very first submenu (a ul element) of the first top-level menu item of the first nav. fadeIn() is a call to a nice pre-defined function of any element(s) returned by jQuery.

That’s all well and good, but what we really need to do is setup a hover effect to fade this menu in and out given the user’s interaction with it.

$(“.nav:first > li:first”).hover(function(){ $(this).children(“ul”).fadeIn(); }, function(){ $(this).children(“ul”).fadeOut(); }); This sets up two anonymous functions to be triggered on mouseOver and on mouseOut. jQuery takes care of all of this. What’s important is understanding the selectors and the event assignment. We won’t go into detail about the selectors here, but definitely read the documentation and play with it. The hover function takes two parameters, each a function. The first will be run on mouseOver, and the second will be run on mouseOut.

So that’s a start. Our submenu is fading in and out. But what if you accidentally mouse off the submenu? The damned thing fades away instantly! To get around this limitation, we’ll use a handy built-in feature of JavaScript, setTimeout. This function takes as its parameters a function literal to be called after a specified number of milliseconds, followed by a timeout value in milliseconds. On mouseOver, we’ll set a class on the element, let’s call it “over”. On mouseOut, we’ll remove the class, and call setTimeout. After a specified amount of time, let’s say 500ms, we’ll check whether the class “over” is present on the top-level nav, and if it isn’t, we’ll fade it out. If it is (indicating the nav item has been hovered over again), we do nothing.

$(“.nav:first > li:first”).hover(function(){ $(this).addClass(“over”); $(this).children(“ul”).fadeIn(); }, function(){ var li = $(this); setTimeout(function() { if(!li.hasClass(“over”)) li.fadeOut(); }, 500); }); This new code sets it up exactly as we’d like it for the first submenu content alone.

What we want to do is make this reusable, as much as possible, and also make it versatile. That’s where jQuery’s plugin system comes in.

jQuery plugins are like any other widget initializer code, only they automatically get to take advantage of jQuery’s fancy schmancy selector system. They should also be written using the function name jQuery rather than $, because jQuery optionally allows itself to be alternately aliased so that it can sit along side other libraries, which might also define a global function $. Both Prototype and MooTools do this, and still jQuery can run in combination with these libraries. If you were to use the $ function, you could likely be unintentionally calling the Prototype or MooTools selector function rather than jQuery’s.

Instructions on authoring jQuery plugins can be found on jQuery’s Web site. It all boils down to:

  1. Respecting the “jQuery” function name over “$”
  2. Defining your function as a property of jQuery.fn, and
  3. Returning the jQuery object that it is called on. (this allows chaining calls, something near and dear to jQuery developers’ hearts)
  4. To be incorporated as an official plugin, the plugin should be defined in a standalone file, named “jquery.[plugin name].js” where “[plugin name]” is your plugin’s name.

Let’s take a look at the next version of our code, the first as a plugin:

jQuery.fn.nav = function() { return this.each(function() { var nav = jQuery(this); nav.find(” > li”).hover(function() { jQuery(this).children(“ul”).fadeIn(); }, function() { var li = jQuery(this); setTimeout(function() { if(li.hasClass(“over”)) li.fadeOut(); }, 500) }); }); }; This is a very suitable, fine piece of code, and it’s even a plugin now. It’s even reusable, because it’s no longer tied to the “.nav” class. You can actually call it multiple times on a page, and using multiple selectors, and all will work just fine, independently:

  • $(“.nav”).nav() works just like our old code, only now it’s a lot shorter.
  • $(“#topNav”).nav() also works, only this is for a single unique element

However, it’s not very versatile beyond the selector bit. For that, we’ll need to introduce the concept of default options and overrides. jQuery provides a simple means of setting up default settings for a plugin that can be overridden in any call to it by passing in an options object. The options object concept is an echo of similar functionality in other programming languages: Ruby (via Hashes), Python (via named arguments) and other languages via similar constructs such as associative arrays. The idea behind it is that no one wants to have to memorize the order of parameters for a function, so don’t make them! Object literals allow you to specify their component properties in any order.

To obtain this functionality in jQuery, simply call jQuery.extend(), passing in an object literal for the default values, and the options object parameter from the plugin’s function. Through this, we can specify faster or slower fade effects, as well as change the actual effects called. This is the final version of our plugin, and the code is quite a bit longer.

jQuery.fn.multinav = function(options) { var settings = jQuery.extend({ effect: “fade”, duration: 250 }, options);   var fade = { over: function(li) { jQuery(li).find(“ul”).fadeIn(settings.duration); }, out: function(li) { jQuery(li).find(“ul”).fadeOut(settings.duration); } };   var slide = { over: function(li) { jQuery(li).find(“ul”).slideDown(settings.duration); }, out: function(li) { jQuery(li).find(“ul”).slideUp(settings.duration); } };   var size = { over: function(li) { jQuery(li).find(“ul”).show(settings.duration); }, out: function(li) { jQuery(li).find(“ul”).hide(settings.duration); } };   var effect = {}; switch(settings.effect) { case “slide”: effect = slide; break; case “size”: effect = size; break; case “fade”: default: effect = fade; break; }   // iterate over the elements obtained from the selector return this.each(function() { var nav = jQuery(this);   nav.find(” > li > ul”).hide();   nav.find(” > li”).hover(function() { jQuery(this).addClass(“over”); effect.over(this); }, function() { var li = jQuery(this).removeClass(“over”); setTimeout(function() { if(!li.hasClass(“over”)) effect.out(li); }, 500); }); }); }; Though this code is longer, it’s still quite legible. We’ve changed the plugin’s name to multinav to reflect it’s new, multi-functional nature The new benefit is that we are able to call on the plugin with alternate effects and timing, simply by passing in an object literal with alternate values from the defaults. Here are a few examples:

  • $(“.nav:first”).multinav(); – this is the default behavior
  • $(“.nav:nth(1)”).multinav({effect: “slide”, duration: 100}); – this selects the second nav-classed element and performs a fast slideIn on hover
  • $(“.nav:last”).multinav({effect: “size”, duration: 1000}); – this selects the last nav-classed element and performs a slow size-in on hover

Using these ideas, you can build any number of plugins which, when handled correctly, can work alongside other libraries, can work under a variety of conditions and present different functionality based on the needs of the job at hand. Calling on a plugin is typically only one line of code. That’s a lot of power in one line. Not only that, but when using the practices of graceful degradation, your page will look and behave appropriately when interactivity is not possible in the browser.

Remember that any widget with interactive elements (form controls, ajax links, tabs, content which moves around or hides/reveals…) should either hide those interactive elements in the CSS and reveal in the JavaScript, or find a way to make use of the elements using the backend. This ensures the page does not contain any confusing, ineffectual controls which look as though they’ll bring about change on the page. In the case of ajax tabs, for instance, you have two choices: Hide the tabs altogether (certainly the easier route), or organize your content and backend technology so that the tabs work as static links to reload the page showing the appropriate (newly-clicked) tab content and hiding the other tabs. This is a bit more trouble, and if you can get away with showing only the most-important content in the first tab, I suggest taking that route.

Have fun writing gracefully-degrading jQuery plugins!

4 Comments

  1. Posted March 15, 2009 at 5:24 pm | Permalink

    So, I finally got around to reading this. Thanks for boiling down the steps required to author a plugin. Will certainly refer to this blog in the future to write some of my own!

  2. Posted March 20, 2009 at 8:54 am | Permalink

    Radness. It felt a little long as I was writing it, since I was covering GDW and jQuery plugins in one post, but hopefully it was clear/concise enough on both. Thanks for reading!

  3. Larry
    Posted March 26, 2009 at 4:08 am | Permalink

    Very easy to follow, to-the-point instructions, thank you!

  4. Posted March 26, 2009 at 4:11 am | Permalink

    Thanks for the read, Larry!

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*