The jQuery UI Widget Factory
WAT?

adam j. sontag

corey frang

Thanks for coming!

The jQuery UI Widget Factory is an HTML5 presentation designed to familiarise developers with basic approaches to debugging jQuery and JavaScript code. It also introduces many of the common pitfalls most people encounter at some point on their jQuery journey. It is always a work in progress!

Use the left and right arrow keys or your mouse wheel to navigate.

ajpiano.com/widgetfactory

Easy Plugins Are Easy

It's easy to extend the jQuery prototype (jQuery.fn) to make a “plugin” that operates on sets of elements.

(function( $ ) {
	$.fn.redman = function() {
		return this.each( function() {
			$( this ).addClass( "wu-tang" )
				.css( "color", "red" );
		});
	};
})( jQuery );

State of Confusion

Stateful plugins are not as straightforward, but really useful.

  • Reverse the plugin's effect?
  • Allow plugin users to react to events triggered by the plugin?
  • Change the plugin's effect after it has been applied?
  • Remember: only one function in the $.fn namespace!
  • Fine! But how can I even invoke "sub-methods?"

But that jQuery UI thing has lots of plugins that let users do all that stuff! How?

You down with OOP?

Objects are a good way to organise code and maintain state

( function( $ ) {
	function Redman( elem ) {
		this.init( elem );
	}
	Redman.prototype.init = function( elem ) {
		this.element = $( elem ).addClass( "wu-tang" ).css( "color", "red" );
	};
	Redman.prototype.destroy = function() {
		this.element.removeClass( "wu-tang" ).removeAttr( "style" ).removeData( "redman" );
	};

	$.fn.redman = function() {
		return this.each( function() {
			// Instantiate a new Redman, storing the instance in the element's data cache
			$.data( this, "redman", new Redman( this ) );
		});
	};
})( jQuery );

$( function() {
	var r = $( "#foo" ).redman().data( "redman" );
	setTimeout( function() { r.destroy(); }, 2000 );
});

Works, but is not idiomatic, nor does it abstract common plugin tasks

The Manufacturing Business

A factory is for making things.

The factory pattern is a way to generate different objects with a common interface.

The jQuery UI Widget Factory is simply a function ($.widget) that takes a string name and an object as arguments and creates a jQuery plugin and a "Class" to encapsulate its functionality.

		$.widget( "aj.filterable" , { /* snip */ });

Powers all the jQuery UI widgets, but it can stand alone

SWAG

The widget factory provides a number of plugin conveniences

  • Creates your namespace, if necessary (jQuery.aj)
  • Encapsulated class (jQuery.aj.filterable.prototype)
  • Extends jQuery prototype (jQuery.fn.filterable)
  • Merges defaults with user-supplied options
  • Stores plugin instance in .data()
  • Methods accessible via string - plugin( "foo" ) - or directly - .foo()
  • Prevents against multiple instantiation
  • Evented structure for handling setup, teardown, option changes
  • Easily expose callbacks: ._trigger( "event" )
  • Sane default scoping (What is this?)
  • Free pseudoselector! $( ":aj-filterable" )
  • Inheritance! Widgets can extend from other widgets
    $.widget( "aj.electricAccordion", $.ui.accordion, { /* snip */ });
    

Let's Rotate These Tires

Building a simple filtering widget in almost no time flat

How do I use this thing?

( function( $ ) {
	// The jQuery.aj namespace will automatically be created if it doesn't exist
	$.widget( "aj.filterable", {
		// These options will be used as defaults
		options: { className : "" },
		_create: function() {
			// The _create method is where you set up the widget
		},
		// Keep various pieces of logic in separate methods
		filter: function() {
			// Methods without an underscore are "public"
		},
		_hover: function() {
			// Methods with an underscore are "private"
		},
		_setOption: function( key, value ) {
			// Use the _setOption method to respond to changes to options
			switch( key ) {
				case "length":
					break;
			}
			// and call the parent function too!
			return this._superApply( arguments );
		}
		_destroy: function() {
			// Use the destroy method to reverse everything your plugin has applied
			return this._super();
		},
	});
})( jQuery );

I Can See Your Privates

Methods prefixed with an underscore are, by convention only, private.

  • These methods cannot be accessed via a plugin's public API:
  • $( "#foo" ).filterable( "_hover" ) will not work
  • They are, however, accessible directly from the widget instance:
  • $( "#foo" ).data( "aj-filterable" )._hover() will
    • (1.9) $( "#foo" ).data( "filterable" ) widget name.
    • (1.10) $( "#foo" ).data( "aj-filterable" ) namespace-widget name
    • (1.11) $( "#foo" ).filterable( "instance" ) no more confusion
  • They are also accessible on the widget's prototype
  • $.aj.filterable.prototype._hover = function() { /* snip */ };

Clothing Optional

Provide an object at the options key to provide defaults.

// Parameters that must be initialised,
// but are optional for the plugin user
options: {
	delay: 250,
	className: ""
}

This is akin to a familiar step from basic jQuery plugin authoring

$.fn.foo = function( opts ) {
	var defaults = { bar: "baz" },
		options = $.extend( {}, defaults, opts );
};

In The Beginning...

Use the _create method to set up your widget. It is called the first time that the plugin is invoked on an element.

_create: function() {
	// this.element -- a jQuery object of the element the widget was invoked on.
	// this.options --  the merged options hash

	// Cache references to collections the widget needs to access regularly
	this.filterElems = this.element.children()
		.addClass( "ui-widget-content " + this.options.className );
	this.filterInput = $( "<input type='text'>" )
		.insertBefore( this.element )
		.wrap( "<div class='ui-widget-header " + this.options.className + "'>" );

	// bind events on elements: 
	this._on( this.filterElems, {
		mouseenter: "_hover",
		mouseleave: "_hover"
	});

	// toggles ui-state-focus for you:
	this._focusable( this.filterInput );
	// _hoverable works for ui-state-hover, but we will do something slighty different in our hover
	this._on( this.filterInput, {
		"keyup": "filter"
	});
	this.timeout = false;
}

_trigger( "finger" )

The _trigger method fires an event the plugin user can...use.

// Signature
this._trigger( "callbackName", [eventObject], [uiObject] )
// Example
this._trigger( "hover", e /* e.type == "mouseenter" */, { hovered: $( e.target )});
// The user can subscribe using an option during initalization
$( "#elem" ).filterable({ hover: function( event, ui ) { } });
// Or with traditional event binding/delegation
$( "#elem" ).bind( "filterablehover" , function( event, ui ) { } );
callbackName
The name of the event you want to dispatch
eventObject (Optional)
An event object (or null). _trigger wraps this object and stores it in event.originalEvent
The user receives an object with event.type == this.widgetEventPrefix + "eventname"
uiObject (Optional)
An object containing useful properties the user may need to access.
Protip: Use a method like ._ui to generate objects with a consistent schema.

Getting Organised

Store distinct pieces of functionality as separate methods.
If the user may need to invoke a function programmatically, expose it!

filter: function( event ) {
	// Debounce the keyup event with a timeout, using the specified delay
	clearTimeout( this.timeout );
	// like setTimeout, only better!
	this.timeout = this._delay( function() {
		var re = new RegExp( this.filterInput.val(), "i" ),
			visible = this.filterElems.filter( function() {
				var $t = $( this ), matches = re.test( $t.text() );
				// Leverage the CSS Framework to handle visual state changes
				$t.toggleClass( "ui-helper-hidden", !matches );
				return matches;
			});
		// Trigger a callback so the user can respond to filtering being complete
		// Supply  an object of useful parameters with the second argument to _trigger
		this._trigger( "filtered", event, {
			visible: visible
		});
	}, this.options.delay );
},
_hover: function( event ) {
	$( event.target ).toggleClass( "ui-state-active", e.type === "mouseenter" );
	this._trigger( "hover", event, {
		hovered: $( e.target )
	});
},

Ch-ch-ch-ch-changes

Plugin users can change options after a plugin has been invoked using $("elem").filterable("option","className","newName");.

If modifiying a particular option requires an immediate state change, use the _setOption method to listen for the change and act on it.

_setOption: function( key, value ) {
	var oldValue = this.options[ key ];
	// Check for a particular option being set
	if ( key === "className" ) {
		// Gather all the elements we applied the className to
		this.filterInput.parent().add( this.filterElems )
		// Wwitch the new className in for the old
		.toggleClass( oldValue + " " + value );
	}
	// Call the base _setOption method
	this._superApply( argruments );

	// The widget factory doesn't fire an callback for options changes by default
	// In order to allow the user to respond, fire our own callback
	this._trigger( "setOption", null, {
		option: key,
		original: oldValue,
		current: value
	});
},

Control-Z

Use the destroy method to revert an element back to its state from before plugin invocation.

_destroy: function() {
	// Remove any new elements that you created
	this.filterInput.parent().remove();

	// Remove any classes, including CSS framework classes, that you applied
	this.filterElems.removeClass( "ui-widget-content ui-helper-hidden ui-state-active " + this.options.className );

	this._super();
}

A Cheesy Example

<ul id="cheeses">
	<li data-price="17.99">Gruyere</li>
	<li data-price="16.99">Comte</li>
	<li data-price="4.99">Provolone</li>
	<li data-price="8.99">Cheddar</li>
	<li data-price="18.99">Parmigiano Reggiano</li>
	<li data-price=".99">Government</li>
</ul>
<div id="register">
	One pound of each would cost $<span id="total"></span>
</div>
  • Gruyere
  • Comte
  • Provolone
  • Cheddar
  • Parmigiano Reggiano
  • Government
One pound of each would cost $

State + Events = Extensibility

A good widget provides a solid base with callbacks for customisation.

var total = $("#total"), cheeses = $("#cheeses"), register = $("#register"), price = $("<span>"),
	activation = $("#activation").button({icons:{primary:"ui-icon-search"}});
activation.click(function() {
	if ( cheeses.is(":aj-filterable") ) {
		return cheeses.filterable("destroy");
	}
	cheeses.filterable({
		className: "cheese",
		create: function() { register.addClass("ui-widget-header cheese").show(); },
		filtered: function(e, ui) {
			var t = 0;
			ui.visible.each(function() { t = t + parseFloat( $(this).data("price") ); });
			total.text( t.toFixed( 2 ) );
		},
		setOption: function(e, ui) {
			ui.option === "className" && register.toggleClass([ui.original, ui.current].join(" "));
		},
		hover: function(e, ui) {
			if (e.originalEvent.type === "mouseenter") {
				price.text(" - " + ui.hovered.data("price") + " per lb").appendTo(ui.hovered);
			} else {
				price.detach();
			}
		}
	}).on("filterabledestroy", function(e,ui) {
		register.removeClass("ui-widget-header "+ui.options.className).hide();
	}).filterable("filter");
	setTimeout(function() { cheeses.filterable("option", "className", "cheesePlease"); },3000);
});

It's .widget() all the way down

  • In 1.9, a widget can "inherit" from itself
  • This is the ideal way to extend widgets
// An incredibly contrived example
$.widget( "ui.dialog", $.ui.dialog, {
	close: function() {
		if ( confirm( "Is it closing time?" ) ) {
			this._super();
		}
	}
});

I've got a bridge to sell you

  • $.widget.bridge is the mechanism that actually creates the plugin
  • Set/change which widget prototype exists on jQuery.fn
// Create a brand new dialog widget from scratch
$.widget( "my.awesomeDialog", {/* snip */});

// Use the bridge to assign it to jQuery.fn.dialog
$.widget.bridge( "dialog", $.my.awesomeDialog );

U and I can be friends

But not roommates

  • The ui namespace is reserved for official jQuery UI plugins
  • Just because you're using the widget factory doesn't mean you have to use ui
  • We'd kinda prefer if you didn't :)

No problem, enjoy

  • adam j. sontag
  • ajpiano at ajpiano dot com
  • @ajpiano
  • I'm ajpiano on freenode IRC (#jquery) and everywhere
  • my blog
  • yayQuery

Revisionist History

  • corey frang
  • gnarf37 at gmail dot com
  • @gnarf37
  • I'm gnarf on freenode IRC (#jquery) and everywhere
  • my blog