jQuery UI How to Use the Widget Factory
We will create a progress bar. As shown in the example below, this can be done by callingjQuery.widget()to accomplish it, which takes two parameters: one is the name of the plugin to create, and the other is an object literal containing the functions that support the plugin. When the plugin is invoked, it will create a new plugin instance, and all functions will be executed in the context of that instance. This is different from standard jQuery plugins in two important ways. First, the context is an object, not a DOM element. Second, the context is always a single object, not a collection.
$.widget( "custom.progressbar", {
_create: function() {
var progress = this.options.value + "%";
this.element
.addClass( "progressbar" )
.text( progress );
}
});
The plugin name must contain a namespace. In this example, we use thecustomnamespace. You can only create a namespace one level deep, socustom.progressbaris a valid plugin name, whilevery.custom.progressbaris not a valid plugin name.
We can see that the Widget Factory provides us with two properties.this.elementis a jQuery object containing one element. If our plugin is called on a jQuery object containing multiple elements, a separate plugin instance will be created for each element, and each instance will have its ownthis.element. The second property,this.options, is a hash of key/value pairs containing all the plugin options. These options can be passed to the plugin as follows:
$( "<div></div>" )
.appendTo( "body" )
.progressbar({ value: 20 });
When we calljQuery.widget(), it extends jQuery by adding functions tojQuery.fn(the system used to create standard plugins). The names of the added functions are based on the name you pass tojQuery.widget()without the namespace - "progressbar". The options passed to the plugin are the values set on the plugin instance. As shown in the example below, we can specify default values for any option. When designing your API, you should be clear about the most common use cases of your plugin so that you can set appropriate defaults and ensure that all options are truly optional.
$.widget( "custom.progressbar", {
// Default options.
options: {
value: 0
},
_create: function() {
var progress = this.options.value + "%";
this.element
.addClass( "progressbar" )
.text( progress );
}
});
Calling Plugin Methods
Now that we can initialize our progress bar, we will perform actions by calling methods on the plugin instance. To define a plugin method, we simply reference a function in the object we pass tojQuery.widget()the object. We can also define "private" methods by prefixing the function name with an underscore.
$.widget( "custom.progressbar", {
options: {
value: 0
},
_create: function() {
var progress = this.options.value + "%";
this.element
.addClass( "progressbar" )
.text( progress );
},
// Create a public method.
value: function( value ) {
// No value passed, act as a getter.
if ( value === undefined ) {
return this.options.value;
}
// Value passed, act as a setter.
this.options.value = this._constrain( value );
var progress = this.options.value + "%";
this.element.text( progress );
},
// Create a private method.
_constrain: function( value ) {
if ( value > 100 ) {
value = 100;
}
if ( value < 0 ) {
value = 0;
}
return value;
}
});
To call a method on the plugin instance, you can pass the method name to the jQuery plugin. If the method you are calling accepts parameters, you simply pass those parameters after the method name.
Note:Methods are executed by passing the method name to the same jQuery function used to initialize the plugin. This is done to prevent jQuery namespace pollution while preserving method chaining. Later in this chapter, we will see other usages that look more natural.
var bar = $( "<div></div>" )
.appendTo( "body" )
.progressbar({ value: 20 });
// Get the current value.
alert( bar.progressbar( "value" ) );
// Update the value.
bar.progressbar( "value", 50 );
// Get the current value again.
alert( bar.progressbar( "value" ) );
Using Options
option()The method is automatically provided to the plugin.option()The method allows you to get and set options after initialization. This method works like jQuery's.css()and.attr()method: you can pass only a name to use it as a getter, pass a name and a value to use it as a setter, or pass a hash of key/value pairs to set multiple values. When used as a getter, the plugin will return the current value of the option corresponding to the passed name. When used as a setter, the plugin's_setOptionmethod will be called for each option that is set. We can specify a_setOptionmethod in our plugin to react to option changes. For actions that should be performed independently when an option changes, we can override_setOptions。
$.widget( "custom.progressbar", {
options: {
value: 0
},
_create: function() {
this.options.value = this._constrain(this.options.value);
this.element.addClass( "progressbar" );
this.refresh();
},
_setOption: function( key, value ) {
if ( key === "value" ) {
value = this._constrain( value );
}
this._super( key, value );
},
_setOptions: function( options ) {
this._super( options );
this.refresh();
},
refresh: function() {
var progress = this.options.value + "%";
this.element.text( progress );
},
_constrain: function( value ) {
if ( value > 100 ) {
value = 100;
}
if ( value < 0 ) {
value = 0;
}
return value;
}
});
Adding Callbacks
The simplest way to extend a plugin is to add callbacks so that users can react when the plugin's state changes. We can see in the example below how to add a callback to the progress bar when the progress reaches 100%._trigger()The method takes three parameters: the callback name, a jQuery event object that triggers the callback, and a data hash related to the event. The callback name is the only required parameter, but the other parameters are very useful for users who want to implement custom functionality in the plugin. For example, if we create a draggable plugin, we can pass the mousemove event when triggering the drag callback, which will allow users to react to the drag based on the x/y coordinates provided by the event object. Note that the event passed to_trigger()must be a jQuery event, not a native browser event.
$.widget( "custom.progressbar", {
options: {
value: 0
},
_create: function() {
this.options.value = this._constrain(this.options.value);
this.element.addClass( "progressbar" );
this.refresh();
},
_setOption: function( key, value ) {
if ( key === "value" ) {
value = this._constrain( value );
}
this._super( key, value );
},
_setOptions: function( options ) {
this._super( options );
this.refresh();
},
refresh: function() {
var progress = this.options.value + "%";
this.element.text( progress );
if ( this.options.value == 100 ) {
this._trigger( "complete", null, { value: 100 } );
}
},
_constrain: function( value ) {
if ( value > 100 ) {
value = 100;
}
if ( value < 0 ) {
value = 0;
}
return value;
}
});
Callback functions are essentially just additional options, so you can get and set them like any other option. Whenever a callback is executed, a corresponding event is triggered. The event type is determined by concatenating the plugin name and the callback name. Both callbacks and events accept the same two parameters: an event object and a data hash related to the event, as shown in the example below.
Your plugin may need to include functionality that prevents users from using it. To achieve this, the best way is to create a cancelable callback. Users can cancel the callback or the related event, just as they would cancel any native event, by callingevent.preventDefault()or returningfalseto achieve this. If the user cancels the callback, the_trigger()method will returnfalse, so you can implement the appropriate functionality within the plugin.
var bar = $( "<div></div>" )
.appendTo( "body" )
.progressbar({
complete: function( event, data ) {
alert( "Callbacks are great!" );
}
})
.bind( "progressbarcomplete", function( event, data ) {
alert( "Events bubble and support many handlers for extreme flexibility." );
alert( "The progress bar value is " + data.value );
});
bar.progressbar( "option", "value", 100 );
Under the Hood
Now that we have seen how to use the Widget Factory to create a plugin, let's look at how it actually works. When you calljQuery.widget(), it will create a constructor function for the plugin and set the object you passed in as the prototype for plugin instances. All functionality automatically added to the plugin comes from a base widget prototype, which is defined asjQuery.Widget.prototype. When creating a plugin instance, usejQuery.datato store it on the original DOM element, with the plugin name as the key.
Because the plugin instance is directly linked to the DOM element, you can access the plugin instance directly without going through plugin methods. This allows you to call methods directly on the plugin instance without passing the method name as a string, and you can also directly access the plugin's properties.
var bar = $( "<div></div>" )
.appendTo( "body" )
.progressbar()
.data( "progressbar" );
// Call a method directly on the plugin instance.
bar.option( "value", 50 );
// Access properties on the plugin instance.
alert( bar.options.value );
You can also create an instance without going through plugin methods, by calling the constructor directly with options and the element:
var bar = $.custom.progressbar( {}, $( "<div></div>" ).appendTo( "body") );
// Same result as before.
alert( bar.options.value );
Extending the Plugin's Prototype
The greatest benefit of a plugin having a constructor and prototype is that it is easy to extend the plugin. By adding or modifying methods on the plugin's prototype, we can modify the behavior of all instances of the plugin. For example, if we want to add a method to the progress bar to reset the progress to 0%, we can add this method to the prototype, and it will be callable on all plugin instances.
$.custom.progressbar.prototype.reset = function() {
this._setOption( "value", 0 );
};
For more details on extending widgets, and on how to create a brand new widget on top of an existing widget, seeExtending Widgets with the Widget Factory。
Cleanup
In some cases, it is useful to allow users to apply a plugin and then unapply it. You can do this through the_destroy()method. In the_destroy()method, you should undo everything the plugin did during initialization and later use._destroy()is called via the.destroy()method. The.destroy()method is automatically called when the element bound to the plugin instance is removed from the DOM, so this can be used for garbage collection. The basic.destroy()method also handles some common cleanup operations, such as removing the instance reference from the widget's DOM element, unbinding all events in the widget namespace from the element, and unbinding all events added with_bind()added events.
$.widget( "custom.progressbar", {
options: {
value: 0
},
_create: function() {
this.options.value = this._constrain(this.options.value);
this.element.addClass( "progressbar" );
this.refresh();
},
_setOption: function( key, value ) {
if ( key === "value" ) {
value = this._constrain( value );
}
this._super( key, value );
},
_setOptions: function( options ) {
this._super( options );
this.refresh();
},
refresh: function() {
var progress = this.options.value + "%";
this.element.text( progress );
if ( this.options.value == 100 ) {
this._trigger( "complete", null, { value: 100 } );
}
},
_constrain: function( value ) {
if ( value > 100 ) {
value = 100;
}
if ( value < 0 ) {
value = 0;
}
return value;
},
_destroy: function() {
this.element
.removeClass( "progressbar" )
.text( "" );
}
});
Closing Remarks
The Widget Factory is just one way to create stateful plugins. There are several other different models you can use, each with its own advantages and disadvantages. The Widget Factory solves many common problems and greatly improves efficiency, while also greatly improving code reuse, making it suitable for jQuery UI and other stateful plugins.
Please note that in this chapter we have used thecustomnamespace. Theuinamespace is reserved for official jQuery UI plugins. When creating your own plugins, you should create your own namespace. This makes it clearer where the plugin comes from and what scope it belongs to.