Creating Application-specific jQuery plugins

When building an application with jQuery, it’s very likely that you will end up writing some code that would be easiest to use directly in a jQuery chain. When the functions you’re writing solve general problems that don’t have a dependency on your application, it’s usually obvious that you should write the function as a jQuery plugin. However, when you’re writing code that is very specific to your application and you want to use it in a chain, it’s not always clear whether writing the function as a jQuery plugin is a good idea.

I’ve come across three main categories of application-specific plugins since I started using jQuery. The first category contains plugins that are useful throughout most of your application, but useless outside of your application. These plugins usually deal with traversing and manipulating common components based on specific markup in your application. This set of plugins can be used on many elements across many pages and therefore get the most reuse. The second category contains plugins that are tied to a specific element that persists across most, if not all, of your application. These are different from the first category because the plugins are never called on more than one element for any page load. You can think of these as Singleton DOM components, such as help dialogs. The final category consists of plugins that are only used on a single page. In many cases these plugins will be used on multiple elements within the page, but the plugin is never loaded on any other page.

One pattern for creating application-specific plugins is to prefix your method names to prevent name collisions. For example, let’s say we have lots of widgets with common markup structures. If we want to be able to navigate from any given element on the page to the widget the element is contained in, we might create a method called nmkWidget.

Let’s assume the widgets have the following shell:

We can define some helper functions for navigating the DOM:

$.fn.nmkWidget = function() {
	return this.closest( ".nmk-widget" );
};
$.fn.nmkWidgetHeader = function() {
	return this.nmkWidget().children( ".nmk-widget-header" );
};
$.fn.nmkWidgetContent = function() {
	return this.nmkWidget().children( ".nmk-widget-content" );
};

This works well for simple plugins like these that don’t have any dependencies or state management beyond the DOM. But what happens if you have a lot of inter-related functionality that you want to organize into a module? jQuery doesn’t have support for namespaces in jQuery.fn, but there is a simple pattern that can help. We can define a method that acts as a pass-thru from jQuery to any object:

var slice = Array.prototype.slice;
$.fn.exec = function( method ) {
	return method.apply( this, slice.call( arguments, 1 ) );
};

Then we can create objects, nested as deeply as we want, and define methods on them as if they were standard jQuery methods:

var nmk = {
	widget: {
		open: function( duration ) {
			return this.nmkWidget().slideDown( duration );
		},
		close: function( duration ) {
			return this.nmkWidget().slideUp( duration );
		}
	}
};

We can then execute them using .exec() in any jQuery chain:

$( elem ).exec( nmk.widget.close );

We can even pass parameters to these methods:

$( elem ).exec( nmk.widget.close, 1000 );

Now we’ve got reusable, modular, namespaced code that can be used directly in jQuery chains. We can use this to define multiple ways to close our widgets without duplicating any logic and without worrying about where we are in the DOM.

// allow any widget to be closed by a close button located in the header
$( ".nmk-widget-close-button" ).live( "click", function() {
	$( this ).exec( nmk.widget.close );
});

// allow adding a new widget, with undo functionality
$( "#add-widget" ).click(function() {
	// generate a new widget and add to the page
	// returns a jQuery object containing the new widget
	var widget = nmk.widget.create();
	$( "

Your new widget has been added. Click here to undo.

;" ) .click(function() { widget.exec( nmk.widget.close ); }); });