Object JavaScript – Code Walkthrough of a jQuery UI Widget

imageIn 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.

image

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.


/*
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 );


<!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>

view raw

filterable.html

hosted with ❤ by GitHub

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.


_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;
},

view raw

create.js

hosted with ❤ by GitHub


<!– the result of this widget is –>
<div class="ui-widget-header myClass">
<input class="" type="text"/>
</div>
<div id="myFilterable"></div>

view raw

filterable.html

hosted with ❤ by GitHub

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.


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()


_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 with event.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.


// 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 )
});


// 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.


_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.


_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.


$("#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);

view raw

client.js

hosted with ❤ by GitHub

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 _triggered 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 _triggered 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

Sample Code


<!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>


/*
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:

© Copyright 2015 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.