A Deeper Look at jQuery UI Widgets

View My GitHub Profile

I wanted to get a better understanding of what $.widget was doing "under the hood", so I am analyzing the code in widget.js more closely. The code here is from jQuery UI 1.11.2, commit 17c7f698a2894211bbb5f2d63750f5b3b84bb0ab.

$.widget = function( name, base, prototype ) {
	var fullName, existingConstructor, constructor, basePrototype,
		// proxiedPrototype allows the provided prototype to remain unmodified
		// so that it can be used as a mixin for multiple widgets (#8876)
		proxiedPrototype = {},
		namespace = name.split( "." )[ 0 ];

	name = name.split( "." )[ 1 ];
	fullName = namespace + "-" + name;

	if ( !prototype ) {
		prototype = base;
		base = $.Widget;
	}

	// create selector for plugin
	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
		return !!$.data( elem, fullName );
	};

	$[ namespace ] = $[ namespace ] || {};
	existingConstructor = $[ namespace ][ name ];
	constructor = $[ namespace ][ name ] = function( options, element ) {
		// allow instantiation without "new" keyword
		if ( !this._createWidget ) {
			return new constructor( options, element );
		}

		// allow instantiation without initializing for simple inheritance
		// must use "new" keyword (the code above always passes args)
		if ( arguments.length ) {
			this._createWidget( options, element );
		}
	};
	// extend with the existing constructor to carry over any static properties
	$.extend( constructor, existingConstructor, {
		version: prototype.version,
		// copy the object used to create the prototype in case we need to
		// redefine the widget later
		_proto: $.extend( {}, prototype ),
		// track widgets that inherit from this widget in case this widget is
		// redefined after a widget inherits from it
		_childConstructors: []
	});

	basePrototype = new base();
	// we need to make the options hash a property directly on the new instance
	// otherwise we'll modify the options hash on the prototype that we're
	// inheriting from
	basePrototype.options = $.widget.extend( {}, basePrototype.options );
	$.each( prototype, function( prop, value ) {
		if ( !$.isFunction( value ) ) {
			proxiedPrototype[ prop ] = value;
			return;
		}
		proxiedPrototype[ prop ] = (function() {
			var _super = function() {
					return base.prototype[ prop ].apply( this, arguments );
				},
				_superApply = function( args ) {
					return base.prototype[ prop ].apply( this, args );
				};
			return function() {
				var __super = this._super,
					__superApply = this._superApply,
					returnValue;

				this._super = _super;
				this._superApply = _superApply;

				returnValue = value.apply( this, arguments );

				this._super = __super;
				this._superApply = __superApply;

				return returnValue;
			};
		})();
	});
	constructor.prototype = $.widget.extend( basePrototype, {
		// TODO: remove support for widgetEventPrefix
		// always use the name + a colon as the prefix, e.g., draggable:start
		// don't prefix for widgets that aren't DOM-based
		widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name
	}, proxiedPrototype, {
		constructor: constructor,
		namespace: namespace,
		widgetName: name,
		widgetFullName: fullName
	});

	// If this widget is being redefined then we need to find all widgets that
	// are inheriting from it and redefine all of them so that they inherit from
	// the new version of this widget. We're essentially trying to replace one
	// level in the prototype chain.
	if ( existingConstructor ) {
		$.each( existingConstructor._childConstructors, function( i, child ) {
			var childPrototype = child.prototype;

			// redefine the child widget using the same prototype that was
			// originally used, but inherit from the new version of the base
			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
		});
		// remove the list of existing child constructors from the old constructor
		// so the old child constructors can be garbage collected
		delete existingConstructor._childConstructors;
	} else {
		base._childConstructors.push( constructor );
	}

	$.widget.bridge( name, constructor );

	return constructor;
};

The function is really just line 72: constructor = $[namespace][name] = function(options, element) { this._createWidget(options, element); }; with a prototype created later on line 129. _createWidget is defined in $.Widget, the base class for all widgets. It sets up event an event namespace, attaches the widget object to the target element (as $(element).data(this.widgetFullName, this);. widgetFullName is namespace+'-'+name), and calls this._create and this._init.

With this, you would use var widget = new $[namespace][name]({option: value}, element); and call methods directly: widget.foo(). Turning that into a jQuery plugin is the job of $.widget.bridge which we will look at later.

Lines 79 to 93 and 145 to 158 are a hack to simulate a prototype chain. Changing the base class would create new methods on all subclasses, but there may be other initialization and other data that needs to be modified. The code keeps a list of every widget that inherits from this one (line 157 pushes this constructor onto the list from the base class), and redefines all of those if this is redefined. It's not clear to me under what circumstances all this would be needed.

The real cleverness is in building the prototype of the new constructor. The standard javascript way would be constructor.prototype = new Base; and then add the new methods, as $.extend (constructor.prototype, prototype) (prototype was the argument passed into $.widget initially). But we want to be able to use this.super() in our new methods. So we copy prototype (lines 100-128; basically line 128) but for each function we define a specific _super for this function (line 106), and replace the function with a new one that saves the old this._super as __super (line 113; two underlines instead of one. Sounds like they are begging for hard-to-catch bugs!), sets this._super to the base function just defined, and calls the actual method, then restores this._super. It defines this._superApply similarly.

That's pretty clever, but it ends up creating a lot of overhead even if the method never calls this._super. I was involved in the original discussions of jQuery inheritance model, and my original idea used John Resig's trick for detecting a method that uses this._super. First, determine if the browser will let you examine the source code for functions: var fnTest = /xyz/.test(function(){xyz;}). This creates a function with the text "xyz" in it and returns true if the RegExp can find it. If that works, then you can check every method in prototype with (at line 105)

if (!fnTest || /\b_super\b/.test(value)){
	// note that if fnTest is false then we have to redefine everything
	proxiedPrototype[ prop ] = // fancy proxied function that defines _super;
}else{
	proxiedPrototype[ prop ] = value;
}

But I guess they found that too hacky.

The other clever part of $.widget is lines 66-68. The widget object is attached (by $.widget.bridge) to the element as $(element).data('namespace-name'). So you can select elements that have your widget attached with $(':namespace-name'). Note that it only selects that actual widget, not any widgets derived from it; if you define $.widget('custom.betterdialog', $.ui.dialog, {..}) then $(':ui-dialog') does not select elements that used $().betterdialog().

$.widget.bridge

$.widget.bridge = function( name, object ) {
	var fullName = object.prototype.widgetFullName || name;
	$.fn[ name ] = function( options ) {
		var isMethodCall = typeof options === "string",
			args = widget_slice.call( arguments, 1 ),
			returnValue = this;

		if ( isMethodCall ) {
			this.each(function() {
				var methodValue,
					instance = $.data( this, fullName );
				if ( options === "instance" ) {
					returnValue = instance;
					return false;
				}
				if ( !instance ) {
					return $.error( "cannot call methods on " + name + " prior to initialization; " +
						"attempted to call method '" + options + "'" );
				}
				if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
					return $.error( "no such method '" + options + "' for " + name + " widget instance" );
				}
				methodValue = instance[ options ].apply( instance, args );
				if ( methodValue !== instance && methodValue !== undefined ) {
					returnValue = methodValue && methodValue.jquery ?
						returnValue.pushStack( methodValue.get() ) :
						methodValue;
					return false;
				}
			});
		} else {

			// Allow multiple hashes to be passed on init
			if ( args.length ) {
				options = $.widget.extend.apply( null, [ options ].concat(args) );
			}

			this.each(function() {
				var instance = $.data( this, fullName );
				if ( instance ) {
					instance.option( options || {} );
					if ( instance._init ) {
						instance._init();
					}
				} else {
					$.data( this, fullName, new object( options, this ) );
				}
			});
		}

		return returnValue;
	};
};

bridge provides the "bridge" between the state-handling and inheritance-handling code in $.widget and jQuery itself. It takes a name and constructor function and creates a jQuery plugin with that name (line 193, $.fn[name] = function(){}). The plugin does two different things, depending on how it's called.

If it's called with no arguments or an object (assumed to be the options), then it goes to line 221 and looks for an existing widget attached to the element (line 230). If there is one, the widget is just reset with lines 231-234, instance.option(options); instance._init(). If there is no widget, it is created with line 236 (the new object( options, this ) part) and attached to the element (the $.data( this, fullName ... part).

If the plugin is called with a string, that is taken as the method name and is called. The only cleverness is allowing for $.fn.end(). If the method returns a jQuery object (line 215, if (methodValue.jquery)methodValue.jquery), then use $.fn.pushStack() to correctly set up the linked list.

$.widget.bridge is separated out from $.widget itself so you can use it as a quick plugin-creator without all the heaviness from $.widget. Just pass in a constructor function that takes an options argument and an element, and you have a plugin.

I wrote a quick example (and learned something about how CSS animations don't work along the way). I have to admit, I really don't see a use case for this, where using $.widget.bridge is easier than $.widget straight.

$.widget.extend

$.widget.extend is a custom version of $.extend that always does a deep copy; it's not clear to me why they don't just use $.extend(true,....

$.Widget

See the API documentation for more details.

$.Widget.prototype = {
	widgetName: "widget",
	widgetEventPrefix: "",
	defaultElement: "<div>",
	options: {
		disabled: false,

		// callbacks
		create: null
	},
	_createWidget: function( options, element ) {
		element = $( element || this.defaultElement || this )[ 0 ];
		this.element = $( element );
		this.uuid = widget_uuid++;
		this.eventNamespace = "." + this.widgetName + this.uuid;

		this.bindings = $();
		this.hoverable = $();
		this.focusable = $();

		if ( element !== this ) {
			$.data( element, this.widgetFullName, this );
			this._on( true, this.element, {
				remove: function( event ) {
					if ( event.target === element ) {
						this.destroy();
					}
				}
			});
			this.document = $( element.style ?
				// element within the document
				element.ownerDocument :
				// element is window or document
				element.document || element );
			this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
		}

		this.options = $.widget.extend( {},
			this.options,
			this._getCreateOptions(),
			options );

		this._create();
		this._trigger( "create", null, this._getCreateEventData() );
		this._init();
	},
	_getCreateOptions: $.noop,
	_getCreateEventData: $.noop,
	_create: $.noop,
	_init: $.noop,

	destroy: function() {
		this._destroy();
		// we can probably remove the unbind calls in 2.0
		// all event bindings should go through this._on()
		this.element
			.unbind( this.eventNamespace )
			.removeData( this.widgetFullName );
		this.widget()
			.unbind( this.eventNamespace )
			.removeAttr( "aria-disabled" )
			.removeClass(
				this.widgetFullName + "-disabled " +
				"ui-state-disabled" );

		// clean up events and states
		this.bindings.unbind( this.eventNamespace );
		this.hoverable.removeClass( "ui-state-hover" );
		this.focusable.removeClass( "ui-state-focus" );
	},
	_destroy: $.noop,

	widget: function() {
		return this.element;
	},

	option: function( key, value ) {
		var options = key,
			parts,
			curOption,
			i;

		if ( arguments.length === 0 ) {
			// don't return a reference to the internal hash
			return $.widget.extend( {}, this.options );
		}

		if ( typeof key === "string" ) {
			// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
			options = {};
			parts = key.split( "." );
			key = parts.shift();
			if ( parts.length ) {
				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
				for ( i = 0; i < parts.length - 1; i++ ) {
					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
					curOption = curOption[ parts[ i ] ];
				}
				key = parts.pop();
				if ( arguments.length === 1 ) {
					return curOption[ key ] === undefined ? null : curOption[ key ];
				}
				curOption[ key ] = value;
			} else {
				if ( arguments.length === 1 ) {
					return this.options[ key ] === undefined ? null : this.options[ key ];
				}
				options[ key ] = value;
			}
		}

		this._setOptions( options );

		return this;
	},
	_setOptions: function( options ) {
		var key;

		for ( key in options ) {
			this._setOption( key, options[ key ] );
		}

		return this;
	},
	_setOption: function( key, value ) {
		this.options[ key ] = value;

		if ( key === "disabled" ) {
			this.widget()
				.toggleClass( this.widgetFullName + "-disabled", !!value );

			// If the widget is becoming disabled, then nothing is interactive
			if ( value ) {
				this.hoverable.removeClass( "ui-state-hover" );
				this.focusable.removeClass( "ui-state-focus" );
			}
		}

		return this;
	},

	enable: function() {
		return this._setOptions({ disabled: false });
	},
	disable: function() {
		return this._setOptions({ disabled: true });
	},

	_on: function( suppressDisabledCheck, element, handlers ) {
		var delegateElement,
			instance = this;

		// no suppressDisabledCheck flag, shuffle arguments
		if ( typeof suppressDisabledCheck !== "boolean" ) {
			handlers = element;
			element = suppressDisabledCheck;
			suppressDisabledCheck = false;
		}

		// no element argument, shuffle and use this.element
		if ( !handlers ) {
			handlers = element;
			element = this.element;
			delegateElement = this.widget();
		} else {
			element = delegateElement = $( element );
			this.bindings = this.bindings.add( element );
		}

		$.each( handlers, function( event, handler ) {
			function handlerProxy() {
				// allow widgets to customize the disabled handling
				// - disabled as an array instead of boolean
				// - disabled class as method for disabling individual parts
				if ( !suppressDisabledCheck &&
						( instance.options.disabled === true ||
							$( this ).hasClass( "ui-state-disabled" ) ) ) {
					return;
				}
				return ( typeof handler === "string" ? instance[ handler ] : handler )
					.apply( instance, arguments );
			}

			// copy the guid so direct unbinding works
			if ( typeof handler !== "string" ) {
				handlerProxy.guid = handler.guid =
					handler.guid || handlerProxy.guid || $.guid++;
			}

			var match = event.match( /^([\w:-]*)\s*(.*)$/ ),
				eventName = match[1] + instance.eventNamespace,
				selector = match[2];
			if ( selector ) {
				delegateElement.delegate( selector, eventName, handlerProxy );
			} else {
				element.bind( eventName, handlerProxy );
			}
		});
	},

	_off: function( element, eventName ) {
		eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) +
			this.eventNamespace;
		element.unbind( eventName ).undelegate( eventName );

		// Clear the stack to avoid memory leaks (#10056)
		this.bindings = $( this.bindings.not( element ).get() );
		this.focusable = $( this.focusable.not( element ).get() );
		this.hoverable = $( this.hoverable.not( element ).get() );
	},

	_delay: function( handler, delay ) {
		function handlerProxy() {
			return ( typeof handler === "string" ? instance[ handler ] : handler )
				.apply( instance, arguments );
		}
		var instance = this;
		return setTimeout( handlerProxy, delay || 0 );
	},

	_hoverable: function( element ) {
		this.hoverable = this.hoverable.add( element );
		this._on( element, {
			mouseenter: function( event ) {
				$( event.currentTarget ).addClass( "ui-state-hover" );
			},
			mouseleave: function( event ) {
				$( event.currentTarget ).removeClass( "ui-state-hover" );
			}
		});
	},

	_focusable: function( element ) {
		this.focusable = this.focusable.add( element );
		this._on( element, {
			focusin: function( event ) {
				$( event.currentTarget ).addClass( "ui-state-focus" );
			},
			focusout: function( event ) {
				$( event.currentTarget ).removeClass( "ui-state-focus" );
			}
		});
	},

	_trigger: function( type, event, data ) {
		var prop, orig,
			callback = this.options[ type ];

		data = data || {};
		event = $.Event( event );
		event.type = ( type === this.widgetEventPrefix ?
			type :
			this.widgetEventPrefix + type ).toLowerCase();
		// the original event may come from any element
		// so we need to reset the target on the new event
		event.target = this.element[ 0 ];

		// copy original event properties over to the new event
		orig = event.originalEvent;
		if ( orig ) {
			for ( prop in orig ) {
				if ( !( prop in event ) ) {
					event[ prop ] = orig[ prop ];
				}
			}
		}

		this.element.trigger( event, data );
		return !( $.isFunction( callback ) &&
			callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
			event.isDefaultPrevented() );
	}
};

$.Widget is the base constructor for all UI widgets. It includes a few virtual methods that are no-ops designed to be overridden, like _create, _init and _destroy, and two that I have not discussed yet or had any reason to use: _getCreateOptions that allows the widget to add more options (why one would use that rather than having it part of _create or options I don't know) and _getCreateEventData, the results of which is passed as part of the create event.

Other methods of note are:

_createWidget
Called by $.widget; sets up the options by mixing the passed-in options, the default options (this.options and the result of _getCreateOptions() and sets up this.bindings (see the _on method), this.hoverable (see the _hoverable method) and this.focusable (see the _focusable method). It also sets up an event listener for removal of the underlying element, to make sure that destroy is called.
_on
_off
Event binding routines that take care of setting this and ignoring disabled widgets. They are clearly still being developed, since destroy doesn't actually use them (see line 50), and the methods themselves use bind and delegate rather than the more modern (since jQuery 1.7) on.
_hoverable(element)
Sets element to get the "ui-state-hover" class when the mouse is in it. Also adds element to the hoverables list so the events can be removed when the widget is.
_focusable(element)
Similar to _hoverable(element), but sets element to get the "ui-state-focus" class when it has the focus.