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
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,...
.
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
$.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
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)
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)
_hoverable(element)
, but sets element
to get the "ui-state-focus"
class when it has the focus.