In the last post, Building Stateful jQuery UI Plugin Using Widget Factory, you were introduced to the working structure of jQuery UI Widgets. You learned that it uses the factory pattern is a way to generate different objects with a common interface. And that it Widget Factory adds features to jQuery plug-in.
jQuery UI Widget Factory is under jQuery UI, but you can use it separately for your own widgets. In this post, you will learn the steps you can take to build your own widget. This posts walks through an implementation of the filterable dropdown from Adam J. Sontag’s and Corey Frang’s post: The jQuery UI Widget Factory WAT?
My motivation in this post is to show what goes where when you are designing your widgets. And provide some direction in the steps you can take when building a widget from scratch.
The Widget Factory builds on the jQuery plug-in API and provides you some extras to make it easier to use, especially when developing a UI control.
Some of the extra conveniences are offered by The Widget Factory:
- 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
The following code creates a sample widget and walks through a relatively complex widget.
Prerequisites
You have already installed jQuery, jQuery UI. This tutorial uses some CSS from jQuery UI, but when you buid your own, you can use whatever CSS you want.
It also assumes you have checked out a beginning tutorial on jQuery UI Widget.
Basic Widget Code
Let’s begin with a shell.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
From http://ajpiano.com/widgetfactory/#slide9 | |
*/ | |
(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 ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Filterable Widget</title> | |
<link href="Content/themes/base/theme.css" rel="stylesheet" /> | |
</head> | |
<body> | |
<h1>Type here</h1> | |
<div id="myFilterable"> | |
</div> | |
<div id="userSelection"></div> | |
<script src="Scripts/jquery-2.1.3.js"></script> | |
<script src="Scripts/jquery-ui-1.11.3.js"></script> | |
<script src="Scripts/filterable/jquery-filterable-0.0.1.js"></script> | |
<script> | |
$("#myFilterable").filterable({ className: "myClass" } ); | |
</script> | |
</body> | |
</html> |
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.
On line 8, $.widget("aj.filterable")
creates a namespace named aj
and the name of the widget, filterable
.
Looking at line 11, the options are the properties you can pass in when you create the widget. To create the widget with the options, you can see $("#myFilterable").filterable({ className: "myClass" } );
on line 18 of the html. You should initialize your parameters here. The consumer of your widget is not required to set the values.
The methods that begin with _ cannot be accessed via the widgets’s public API. So you will not be able to call $( "#foo" ).filterable( "_hover" )
from your consumer. You can access them from within the widget.
The _setOption
method is called each time the consumer changes one of the values in options
. This method is called when the user set any of the options, including when the widget is initialized.
The _destory
method cleans up everything your widget has done.
_create Method
The _create
method sets up your widget. It is called by Widget Factory the first time the plugin is invoked. It uses the values that you set as defaults in options
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_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; | |
}, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!– the result of this widget is –> | |
<div class="ui-widget-header myClass"> | |
<input class="" type="text"/> | |
</div> | |
<div id="myFilterable"></div> |
You will have access to the jQuery object that the widget is involked on. Use this.element
to get the object.
You can also get the options by using this.options
.
Line 7 intializes a variable named filterElems
that will respond to the user hovering. And line 10 builds the html and assigns it to a variable named filterInput
. Line 10 uses It uses jQuery methods to build out the html.
this._on
on line 15 sets up a method to be called when one of this.filterElems
receives a mouseenter or a mouseleave event. The _on
is a method that can only be accessed inside the Widget and is provided by Widget Factory. It binds event handlers to the specified element(s). When your widget is disabled or if an event occurs on an element with the ui-state-disabled
class, the default behavior is that the event handler is not invoked.
Each of the events (shown on line 16 and 17) will call the private _hover
method, that is implemented in the following section. Note that the name of the method to be called is a string.
The result of _create
is to insert the control before
.
Widget Logic (Your Public & Private Methods)
You should store distinct pieces of functionality as separate methods. In this case, build a public method for filter and a private method for _hover.
The filter
method is the public method method, which is made available by Widget Factory to your client, who can invoke it directly.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}, |
On line 8, the filter
method calls the Widget Factory’s _delay
method and sets it to a variable on this
. _delay
invokes function after a specified delay. Keeps your this
context correct. Essentially calls setTimeout()
. It returns the timeout ID. You can clear the timeout later using clearTimeout()
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_hover: function (event) { | |
$(event.target).toggleClass("ui-state-active", e.type === "mouseenter"); | |
this._trigger("hover", event, { | |
hovered: $(e.target) | |
}); | |
}, |
The _hover
method is private, meaning it is available for calling inside your widget. It toggles the ui-state-active class on and off as the mouse enters and leaves. It also provides for a callback to notify the client of the hovering change.
More about Callbacks
Callback are important in your widget. They are as important as user settings.
It is a best practice to provide the consumers of your widget opportunities to respond to changes within the widget. Determine when to expose changes to data in options and allow the consumer to override updates in the default display.
Both filter
and hover
use the Widget Factory’s _trigger
to invoke an event that the plug in user can use. The this._trigger
method is provided by Widget Factory. It has this signature:
this._trigger( "callbackName", [eventObject], [uiObject] )
You pass it:
- callbackName, the name of the event.
- eventObject, an optional even object or null.
_trigger
wraps this object and stores it in event.originalEvent. The user receives an object withevent.type == this.widgetEventPrefix + "eventname"
- uiObject, an optional object containing useful properties the user may need to access.
Then you receive the event in your client.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// in your widget | |
// call the callback event named 'hover' the client code | |
this._trigger( "hover", | |
// send e as the original event back as part of the callback | |
e /* e.type == "mouseenter" */, { | |
// return the uiObject e.Target in the hovered parameter of the event | |
// you could also respond with data that the user might need | |
hovered: $( e.target ) | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// The user can subscribe using an option during initalization | |
$( "#elem" ).filterable({ | |
hover: function( event, ui ) { | |
// your response goes here | |
} | |
}); | |
// Or with traditional event binding/delegation | |
$( "#elem" ).bind( "filterablehover" , function( event, ui ) { | |
// your response goes here | |
}); |
The _trigger
method fires and event, and then the client responds. As shown in the code sample, the user can subscribe using an option during initialization or use jQuery bind.
Callbacks are used in filter, _hover methods. And in the _setOption method of the widget.
_setOption
You can change the this.options
after a plugin has been invoked.
When you change any of the options values, Widget Factory is listening and calls your _setOption
method. If modifiying a particular option requires an immediate state change, use the _setOption method to listen for the change and act on it.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_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) | |
// Switch the new className in for the old | |
.toggleClass(oldValue + " " + value); | |
} | |
// Call the base _setOption method | |
this._super(key, value); | |
// 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 | |
}); | |
}, |
The Widget Factory’s _super method is called on line 16, which invokes the method of the same name from the parent widget, with any specified arguments. Call the parent widget’s _setOption() to update the internal storage of the option.
It is best practice to identify the change based on the key, and then to provide a callback to the client for what has changed. The Widget Factory does not automatically callback to the client when a value is changed. So you will need to provide that (as needed).
_destroy
Widget Factory provides you with a public destroy
method which cleans up all common data, events, and such. Widget Factory then delegates out to your widgets to your _destroy
method for custom, widget-specific, cleanup.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_destroy: function () { | |
// Use the destroy method to reverse everything your plugin has applied | |
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); | |
return this._super(); | |
}, |
As you write your widget, be sure to keep track of the elements you are adding and the classes you are changing so that they can set back to the original state. They make the changes in your destroy method to set them back to the original.
The Client
The client exercises the public methods available.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$("#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")); }); | |
console.log( t ); | |
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(); | |
} | |
} | |
}); | |
setTimeout(function() { cheeses.filterable("option", "className", "cheesePlease"); },3000); |
On 3, it sets className
. hmmm
Widget Factory provides an public create event that is triggered when the widget is created. Line 3, the client is responding by adding a two classes to the register
div and showing it.
On line 4, the client is responding to the “filtered” event _trigger
ed by in the public filter
method of our widget. The widget passes out a variable named visible
which was set in the filter for each of the filterElems. The client access the that variable using ui.visible
to set the pricing information.
On line 10, the client is responding to the widget’s “setOption” event that was triggered from _setOption
It passes out:
- key (renamed option when passed to the client).
- oldValue (renamed original when passed to the client).
- newValue renamed current when passed to the client).
It then does some magic with the register class.
On line 13, the client responds to the “hover” event that was _trigger
ed by in the private _hover
method. The client responds to the mouse is entering the text and shows the price, otherwise it removes the price.
Summary
Widgets can be quite complex. It is important to decide what happens inside the widget and what can happen outside the widget.
The widget factory, part of the jQuery UI Core, provides an object-oriented way to manage the lifecycle of a widget. These lifecycle activities include:
- Creating and destroying a widget
- Changing widget options
- Making “super” calls in subclassed widgets
- Event notifications
Use jQuery UI Widgets when you want to encapsulate and test reusable stateful user interface controls. The jQuery UI Widget Factory creating a consistent API across plugins.
Resources
- The jQuery UI Widget Factory WAT? by Adam J. Sontag and Corey Frang
- Widget Factory from jQuery documentation
- Coding your First jQuery UI Plugin
Sample Code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<!– | |
From http://ajpiano.com/widgetfactory/ | |
Copyright 2014 Bruce D Kyle | |
Licensed under the Apache License, Version 2.0 (the "License"); | |
you may not use this file except in compliance with the License. | |
You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License. | |
Original presentation is copyrighted by Google 2010. | |
–> | |
<title>Filterable Widget</title> | |
<link href="Content/themes/base/core.css" rel="stylesheet" /> | |
<link href="Content/themes/base/theme.css" rel="stylesheet" /> | |
</head> | |
<body> | |
<button type="button" id="activation">Toggle Filterable</button> | |
<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> | |
<script src="Scripts/jquery-2.1.3.js"></script> | |
<script src="Scripts/jquery-ui-1.11.3.js"></script> | |
<script src="Scripts/filterable/jquery-filterable-0.0.3.js"></script> | |
<script> | |
$(function() { | |
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")); }); | |
console.log( t ); | |
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(); | |
} | |
} | |
}); | |
setTimeout(function() { cheeses.filterable("option", "className", "cheesePlease"); },3000); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
From http://ajpiano.com/widgetfactory/ | |
Copyright 2014 Bruce D Kyle | |
Licensed under the Apache License, Version 2.0 (the "License"); | |
you may not use this file except in compliance with the License. | |
You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License.. | |
*/ | |
(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 () { | |
// this.element — a jQuery object of the element the widget was invoked on. | |
// this.options — the merged options hash | |
console.log("_create" + this.options.className); | |
// 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; | |
}, | |
filter: function (event) { | |
// Debounce the keyup event with a timeout, using the specified delay | |
clearTimeout(this.timeout); | |
console.log("_create" + this.options.className); | |
// 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", event.type === "mouseenter"); | |
this._trigger("hover", event, { | |
hovered: $(event.target) | |
}); | |
}, | |
_setOption: function (key, value) { | |
var oldValue = this.options[key]; | |
console.log("_setOption: " + key + ": " + value); | |
// 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) | |
// Switch the new className in for the old | |
.toggleClass(oldValue + " " + value); | |
console.log("toggle class : " + oldValue + " " + value); | |
} | |
// Call the base _setOption method | |
this._super(key, value); | |
// 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 | |
}); | |
}, | |
_destroy: function () { | |
// Use the destroy method to reverse everything your plugin has applied | |
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); | |
return this._super(); | |
} | |
}); | |
})(jQuery); |
Notice
This is a work derived from http://ajpiano.com/widgetfactory which allows for derived works, provided by the following: