facebook google plus twitter
Webucator's Free jQuery Tutorial

Lesson: Plugins

Welcome to our free jQuery tutorial. This tutorial is based on Webucator's jQuery Fundamentals Training course.

A jQuery plugin is simply a new method that we use to extend jQuery's prototype object. By extending the prototype object you enable all jQuery objects to inherit any methods that you add. Once the plugin is established, whenever you call $() you're creating a new jQuery object, with your plugin now method included along with all of jQuery's existing methods.

The idea of a plugin is to do something with a collection of elements. You could consider each method that comes with the jQuery core a plugin, like fadeOut or addClass.

You can make your own plugins and use them privately in your code or you can release them into the wild. There are thousands of jQuery plugins available online. The barrier to creating a plugin of your own is so low that you'll want to do it straight away!

Lesson Goals

  • Create a jQuery plugin

How to Create a Basic Plugin

The typical approach for creating a plugin is as follows:

(function($){
	$.fn.myNewPlugin = function() {
		return this.each(function(){
			// do something
		});
	};
}(jQuery));

The outer wrapper is an immediately-invoked function, inside which we create the plugin:

(function($){
	//...
}(jQuery));

This has the effect of creating a "private" scope that allows us to extend jQuery using the dollar symbol without having to risk the possibility that the dollar has been overwritten by another library. Although it is not required that you establish the plugin this way, this is the conventional approach.

The most important aspect of creating a plugin is to assign the method to the base jQuery object, which is what this part accomplishes:

$.fn.myNewPlugin = function() { ... }

$.fn is the base object containing all the jQuery collection methods. Any methods we add to this base object will be available to anything retrieved with $(selector).

The this keyword within the new plugin refers to the jQuery object on which the plugin is being called.

Your typical jQuery object will contain references to any number of DOM elements, which is why jQuery objects are often referred to as collections.

To do something with a collection we need to loop through it, which is most easily achieved using jQuery's each() method:

$.fn.myNewPlugin = function() {
	return this.each(function(){
		// perform operations an individual elements
		// which will be available as this
	});
};

jQuery's each() method, like most other jQuery methods, returns a jQuery object, thus enabling what we've all come to know and love as "chaining", for example: $(...).css().attr(...). We wouldn't want to break this convention, so we return the this object. Within this loop you can do whatever you want with each element.

Here's an example of a small plugin using some of the techniques we've discussed, intended to show a hyperlink's url as part of its displayed text:

(function($){
	$.fn.showLinkLocations0 = function() {
		return this.each(function(){
			$(this).append(' (' + $(this).attr('href') + ')');
		});
	};
}(jQuery));

This code appends text to every element it finds, showing that elements' href attribute in parentheses. The problem with this approach is that it will do that indiscriminately, regardless of whether an element is actually an a tag or not. Since we have no control over what selector is used, this will cause a problem.

(function($){
	$.fn.showLinkLocations1 = function() {
		return this.filter('a').each(function(){
			$(this).append(' (' + $(this).attr('href') + ')');
		}).end();
	};
}(jQuery));

Now we are filtering the elements, and only operating on a tags. Note the call to end() at the end of the chain, which ends the most recent filtering operation in the current chain and returns the set of matched elements to its previous state. filter() will produce a subset of the original set, but whoever uses the function would probably rather have it return the original set.

Testing Our Plugin

OK, great, so we're done. Open up jqy-plugins/Demos/showLinkLocation.html and let's see what different versions of the demo look like...

Version 1 of our Plugin

From the screenshot below, you can see how the plugin works when invoking the showLinkLocations1() function. Note that not every link is affected. We select all elements with css class ".special", and links with this class do get changed. But, links within the <div class="special"> do not. filter finds a subset of the current set of elements, but does not select descendants of the current set. We would probably like a tags that are contained within any selected elements to be changed as well.

Show Link Locations 1

Version 2 of our Plugin

(function($){
	$.fn.showLinkLocations2 = function() {
		return this.find('a').each(function(){
			$(this).append(' (' + $(this).attr('href') + ')');
		}).end();
	};
}(jQuery));

This version uses find instead of filter, and finds all descendant a tags (notice the screenshot below). But, now the problem is reversed -- find will produce only descendant elements within the original selection, and omit the a tags that directly selected by the query.

Show Link Locations 2

Final Version of our Plugin

So, we need to use both approaches, as shown below.

Code Sample:

jqy-plugins/Demos/showLinkLocation.html
<html>
<head>
<style>
.redText { color: red; }
</style>
<script src="../../jqy-libs/jquery.js"></script> 
<script src="../../jqy-libs/fix-console.js"></script> 
</head>
<body>
<div>
	<a class="special" href="http://www.webucator.com/">Webucator</a>
	<a class="special" href="index.html">Home</a>
	<a class="special" href="more.html">More</a>
</div>
<p class="special">This is special, too!</p>
<div class="special">
	<a href="http://www.webucator.com/">Webucator</a>
	<a href="index.html">Home</a>
	<a href="more.html">More</a>
</div>
<div>
	<a href="http://www.webucator.com/">Webucator</a>
	<a href="index.html">Home</a>
	<a href="more.html">More</a>
</div>
<p>This is not special!</p>
<script>
// filter only
(function($){
	$.fn.showLinkLocations1 = function() {
		return this.filter('a').each(function(){
			$(this).append(' (' + $(this).attr('href') + ')');
		}).end();
	};
}(jQuery));

// find only
(function($){
	$.fn.showLinkLocations2 = function() {
		return this.find('a').each(function(){
			$(this).append(' (' + $(this).attr('href') + ')');
		}).end();
	};
}(jQuery));

// both find and filter
(function($){
	$.fn.showLinkLocations = function() {
		return this.find('a').each(function(){
			$(this).append(' (' + $(this).attr('href') + ')');
		}).end().filter('a').each(function(){
			$(this).append(' (' + $(this).attr('href') + ')');
		}).end();
	};
}(jQuery));
    
// Usage example:
$('.special').showLinkLocations().css( { fontSize: '20pt'} );
</script>
</body>
</html>

This plugin first finds all the a descendants of a selection, then backs up and uses filter, and then backs up again to return the original selection (which is confirmed by the font size change in the one p.special element).

Show Link Locations Final Version

Here's another example of a plugin. This one doesn't require us to loop through every element with the each() method. Instead, we're simply going to delegate to other jQuery methods directly:

(function($){
	$.fn.fadeInAndAddClass = function(duration, className) {
		return this.fadeIn(duration, function(){
			$(this).addClass(className);
		});
	};
}(jQuery));

// Usage example:
$('a').fadeInAndAddClass(400, 'finishedFading');

Finding and Evaluating Plugins

Plugins extend the basic jQuery functionality, and one of the most celebrated aspects of the library is its extensive plugin ecosystem. From table sorting to form validation to autocompletion. If there's a need for it, chances are good that someone has written a plugin for it.

The quality of jQuery plugins varies widely. Many plugins are extensively tested and well-maintained, but others are hastily created and then ignored. More than a few fail to follow best practices.

Google is your best initial resource for locating plugins, though the jQuery team is working on an improved plugin repository. Once you've identified some options via a Google search, you may want to consult the jQuery mailing list or the #jquery IRC channel to get input from others.

When looking for a plugin to fill a need, do your homework. Ensure that the plugin is well-documented, and look for the author to provide lots of examples of its use. Be wary of plugins that do far more than you need; they can end up adding substantial overhead to your page. For more tips on spotting a subpar plugin, read Signs of a poorly written jQuery plugin by Remy Sharp.

Once you choose a plugin, you'll need to add it to your page. Download the plugin, unzip it if necessary, place it in your application's directory structure, then include the plugin in your page using a script tag (after you include jQuery).

The Mike Alsup jQuery Plugin Development Pattern

For more on plugin development, read Mike Alsup's essential post, A Plugin Development Pattern (http://www.learningjquery.com/2007/10/a-plugin-development-pattern). In it, he creates a plugin called $.fn.hilight, which provides a centralized method for setting global and instance options for the plugin. Any global options can be overridden by options passed in when an instance of the plugin is created, and, in turn, those options can be overridden by options present in the effected element's data.

The sample plugin, which has been modified slightly from the original, below highlights elements by setting foreground and background colors, and then wrapping the HTML in a <strong> tag.

Code Sample:

jqy-plugins/Demos/Alsup-jQuery.js
(function($) {

	// define plugin and add to $.fn
	$.fn.hilight = function(options) {

		// demonstrates use of private function
		debug(this);

		// build main options before element iteration
		// extending built-in defaults with passed in options
		var opts = $.extend({}, $.fn.hilight.defaults, options);

		// iterate and reformat each matched element
		return this.each(function() {
			$this = $(this);

			// build element specific options from data stored for 
			// this element, extending current options local options
			var o = $.extend({}, opts, $this.data());

			// update element styles
			$this.css({
				backgroundColor: o.background,
				color: o.foreground
			});

			var markup = $this.html();

			// call our format function
			markup = $.fn.hilight.format(markup);
			$this.html(markup);
		});
	};

	// private function for debugging
	function debug($obj) {
		if (window.console && window.console.log)
		window.console.log('hilight selection count: ' + $obj.size());
	};

	// define and expose format function by adding it to $.fn.hilight
	$.fn.hilight.format = function(txt) {
		return '<strong>' + txt + '</strong>';
	};

	// built-in defaults, also added to  $.fn.hilight
	$.fn.hilight.defaults = {
		foreground: 'red',
		background: 'yellow'
	};

})(jQuery);

The line

var opts = $.extend({}, $.fn.hilight.defaults, options);

creates an object starting with the contents of defaults, and overriding with any values in options. We do this before we start processing any elements in the collection.

Then, for each individual element, we extend the options again with the data retrieved from the tag data, if any exists.

var o = $.extend({}, opts, $this.data());

The markup function has been exposed by adding it to $.fn.hilight, meaning that you can replace it. The HTML page that uses this does that before the final application of the plugin.

The debug function has not been exposed, so it is only available within the plugin's code.

Creating a Plugin Using the Alsup Pattern

Duration: 5 to 15 minutes.
  1. The exercise file jqy-plugins/Exercises/js/stripe.js contains a table-striping function Globals.stripe that will only stripe one level of table rows -- it will not descend into child tables. The file table.html contains a table with nested tables, and invokes the striping function separately for the outer and inner tables.
  2. Your job is to turn the stripe function into a plugin, $.fn.stripe, using the Alsup pattern (don't bother with the data support, though).
  3. The odd and even colors should now be options.
  4. Change the call to css after the second striping operation to be chained on after the striping, since our plugin should allow this.
  5. Right now, the plugin stripes all rows, regardless of what type of table section they are in. As a challenge, see if you can modify the plugin to have a default option of striping only the tbody, which can be overridden by the options object passed to the plugin to stripe all rows instead. Hint: the children function can take a query string, which can be used to filter the set of tags it returns.

Solution:

jqy-plugins/Solutions/tables.html
---- C O D E   O M I T T E D ----
<script>
$('.mostTables').stripe({ evenColor:'#ccddff', oddColor:'#aaaaaa' });

$('.innerfruits')
	.stripe({ evenColor:'#ffdd77' })
	.css( 'fontStyle', 'italic' );

$.fn.stripe.defaults = {
	evenColor:'#ffddff',
	oddColor:'#aaffaa'
};
$('table.little').stripe();
</script>
</body>
</html>

This is how we invoke the plugin.

Solution:

jqy-plugins/Solutions/js/stripe.js
(function($){
		
    $.fn.stripe = function(options) {	

		// override defaults with options
		var opts = $.extend({}, $.fn.stripe.defaults, options);

		// start with empty set
		var $tables = $()
			// add any tables that are direct entries in current collection
			.add(this.filter('table').get())
			// add ones that are descendants of entries in current collection
			.add(this.find('table').get());
		// now find any tables that are descendants of the tables we found
		var $omit = $tables.find('table');
		// and omit them from the collection
		$tables = $tables.not($omit);

		var oddColor = opts.oddColor ? opts.oddColor : null;
		var evenColor = opts.evenColor ? opts.evenColor : null;

		$tables.each(function() {
				// this finds only grandchildren (table -> table section -> tr)
				var $sections = $(this).children();
				if (oddColor) $sections.children('tr:odd')
					.css('backgroundColor', oddColor);
				if (evenColor) $sections.children('tr:even')
					.css('backgroundColor', evenColor);
		});
		return this;
	};

	$.fn.stripe.defaults = {
		evenColor: '#ddddff'
	}

}(jQuery));

The function has been stored in $.fn now, instead of our Globals object. Similarly, near the end, we store our default color in $.fn.stripe.

The first parameter has been removed, since that will now be the query upon which our plugin acts. Because of that, within the code, the $(query) has been replaced with this.

Before we start finding tables, we override the defaults with any incoming options.

To properly behave as a jQuery collection method, we return this at the end.

Challenge Solution:

jqy-plugins/Solutions/tables-challenge.html
---- C O D E   O M I T T E D ----
<script>
$('.mostTables')
	.stripe(
		{ evenColor:'#ccddff', oddColor:'#aaaaaa', tbodyOnly: false }
	);

$('.innerfruits')
	.stripe({ evenColor:'#ffdd77' })
	.css( 'fontStyle', 'italic' );

$.fn.stripe.defaults = {
	evenColor:'#ffddff',
	oddColor:'#aaffaa'
};
$('table.little').stripe();
</script>
</body>
</html>

This is how we invoke the plugin.

Challenge Solution:

jqy-plugins/Solutions/js/stripe-challenge.js
(function($){
	$.fn.stripe = function(options) {

		// override defaults with options
		var opts = $.extend({}, $.fn.stripe.defaults, options);

		// start with empty set
		var $tables = $()
			// add any tables that are direct entries in current collection
			.add(this.filter('table').get())
			// add ones that are descendants of entries in current collection
			.add(this.find('table').get());
		// now find any tables that are descendants of the tables we found
		var $omit = $tables.find('table');
		// and omit them from the collection
		$tables = $tables.not($omit);

		var oddColor = opts.oddColor ? opts.oddColor : null;
		var evenColor = opts.evenColor ? opts.evenColor : null;

		$tables.each(function() {
				// this finds only grandchildren (table -> table section -> tr)
				if (opts.tbodyOnly) var $sections = $(this).children('tbody');
				else var $sections = $(this).children();
				if (oddColor) $sections.children('tr:odd')
					.css('backgroundColor', oddColor);
				if (evenColor) $sections.children('tr:even')
					.css('backgroundColor', evenColor);
		});
		return this;
	};

	$.fn.stripe.defaults = {
		evenColor: '#ddddff',
		tbodyOnly: true
	}
}(jQuery));

We have added a tbodyOnly property to the defaults object. The code to find the tables' children has been split with a conditional to either filter with 'tbody' or not.

Make a Table Sortable (Optional)

Duration: 5 to 10 minutes.

For this exercise, your task is to identify, download, and implement a table sorting plugin on the index.html page (located at jqy-plugins/Exercises). When you're done, all columns in the table on the page should be sortable.