jQuery UI Widgets

Project Silk

DropDown image DropDownHover image Collapse image Expand image CollapseAll image ExpandAll image Copy image CopyHover image

Introduction

When building rich client-side web applications, some of the visual elements on the page will naturally take on roles, responsibilities, and state. As more of these elements are added to the page, complexity will increase, so it's important for the design to support a maintainable codebase. Maintainable solutions have at least two important characteristics: they have unit tests, and they have an intentional design that plays to the strengths of the platform, language, and key parts of the environment.

The web browser is the platform. JavaScript represents the language and various JavaScript libraries represent key parts of the solution. Among other benefits, libraries like jQuery and jQuery UI are used to:

  • Address typical challenges like browser compatibility
  • Provide consistency for AJAX interactions, animations, and events
  • Assist in creating a maintainable codebase through modularity

According to the official jQuery UI project, "[it] provides abstractions for low-level interaction and animation, advanced effects and high-level, themeable widgets, built on top of the jQuery JavaScript Library, that you can use to build highly interactive web applications." A central concept in the visual parts of jQuery UI is the widget. Widgets are objects attached to page elements that supply services for managing lifetime, state, inheritance, theming, and communication with other widgets or JavaScript objects.

Even though they have a number of additional features on top of typical jQuery plugins, it's important to know a widget is a jQuery plugin. This may not be obvious because they are defined differently, but widgets are used the same way you use official jQuery methods and most custom plugins. Sometimes a plugin is sufficient and other times a widget is more appropriate. When you need to apply behavior or state to individual elements and need to communicate between elements, widgets provide a number of capabilities you would otherwise have to write yourself. This chapter illustrates the use of these capabilities.

In this chapter you will learn:

  • How to define and apply widgets
  • How to manage the lifetime of widgets
  • How to define default options that permit overrides and change notifications
  • How to use options for decoupling behavior and facilitating event subscriptions
  • How to use private methods to improve the readability of the code
  • How to define and use public methods, properties, and events
  • How to inherit from a base widget

The technologies discussed in this chapter are jQuery Plugins and the jQuery UI Widget Factory. The code examples used in this chapter come from the Widget QuickStart included with Project Silk. For more information, see Appendix B: Widget QuickStart.

Widget Fundamentals

If you know how to use jQuery, you know how to use a widget. However, before you can use a widget, it has to be defined. Once it has been defined it can be applied to elements. Widgets are defined using the widget factory. When the widget factory is invoked, it creates a widget method on the jQuery prototype, $.fn – the same place plugins and other jQuery functions are located. The widget method represents the primary interface for applying the widget to elements and using the widget after it's applied.

Defining a Widget

The dependencies for a widget can be fulfilled with script references to the CDN locations for jQuery and jQuery UI. Widgets often reside in their own .js file and are wrapped in a self-executing function. This wrapper creates a JavaScript closure which prevents new variables from being globally scoped. A single solution should prevent any more than one global object according to well-accepted JavaScript practices. The $ and undefined arguments reestablish their default expectations inside the closure in case another script previously defined them as something else.

JavaScript Copy Code
// Contained in jquery.qs.tagger.js
(function($, undefined) {
  $.widget('qs.tagger', {
    // definition of the widget goes here
  });
}(jQuery));

The call to $.widget invokes the widget factory which makes the widget available to use. The first argument, qs.tagger, is the widget's namespace and name separated by a period. The name is used as the name of the widget method placed on the jQuery prototype. The second argument, called the widget prototype, is an object literal that defines the specifics of the widget. The widget prototype is stored directly on the jQuery object under the namespace provided: $.qs.tagger.

Using a Widget

Once a widget has been defined, it's ready to be applied to elements. To apply the widget to the matched elements, invoke the widget method just like you would other jQuery methods. The following code shows how to apply the tagger widget to all span elements with a data-tag attribute.

JavaScript Copy Code
// Contained in startup.widget.js
$('span[data-tag]').tagger();

Because the widget method is used as the primary interface to the widget, it's not only called when initially applying the widget to the element, it's also used for calling methods and reading and writing options and properties on the widget. When widgets are applied to elements, an instance of the widget prototype is created and stored inside each element. This is how the widget factory knows if a widget has already been attached to an element so it can take the correct action in subsequent calls.

Managing Lifetime

There are three phases of a widget's lifetime that you can control: creation, initialization, and destruction.

Creation

The first time the widget is applied to an element, the factory calls the widget's _create function. Method names preceded with an underscore have private scope by convention, which means they only expect to be invoked from inside the widget. The following code shows the _create method in the infobox widget.

JavaScript Copy Code
// Contained in jquery.qs.infobox.js
_create: function () {
    var that = this,
        name = that.name;
    that.infoboxElement = $('<div class="qs-infobox" />');
    that.infoboxElement.appendTo('body')
    .bind('mouseenter.' + name, function () {
        mouseOverBox = true;
    })
    .bind('mouseleave.' + name, function () {
        mouseOverBox = false;
        that.hideTagLinks();
    });
},
  • The _create method is the most appropriate place to perform a number of common tasks including the following:
  • Adding classes to various elements in the widget is the recommended way to apply styling, layout theming and more to the widget.
  • Storing references can increase performance when a particular set of elements are used from a number of methods. Simply create object-level variables for them once and all other methods can use them. This is an accepted jQuery performance best practice.
  • Creating elements is common for widgets that have requirements like animations, effects, styling, accessibility, and cross-browser compatibility. As an example, consider the div.qs-infobox element created by the infobox widget.
  • Applying other widgets is recommended during creation when you need them available as soon as possible. Even if your widgets don't require each other, consider using the official jQuery UI widgets from inside yours to add useful behaviors and interactions.

Initialization

  • While the _create method is only called when the widget is first applied to the element, the _init method is called each time the widget method is called with no arguments or with options. When the widget is applied to the element the first time, _init is called after _create. When the widget method is called after the widget has been attached, only _init will be called. The _init method is the recommended place for setting up more complex initialization and is a good way to give the widget a way to reset. Although, it's not uncommon for widgets to not implement an _init method.

Destruction

  • The widget's destroy method is used to detach a widget from an element. The goal of the destroy method is to leave the element exactly like it was before the widget was attached. Therefore, it's not surprising the common tasks are to remove any added classes, detach any added elements and destroy any initialized widgets. Here is the destroy method for the tagger widget.
JavaScript Copy Code
// Contained in jquery.qs.tagger.js
destroy: function () {
    this.element.removeClass('qs-tagged');

    // if using jQuery UI 1.8.x
    $.Widget.prototype.destroy.call(this);
    // if using jQuery UI 1.9.x
    //this._destroy();
}

The last part calls the widget's base implementation of destroy and is a recommended practice. The base destroy will remove the instance of the widget from the element and unbind all namespaced event bindings, which are covered later in the chapter.

Defining Options

Options give widgets the ability to have state that is public, readable, writable, and callable. Options are automatically merged with the widget's default options during creation and the widget factory supports change notifications when option values change. In principle, you should be able to save the options on a widget, remove the widget from memory, recreate the widget with the saved options and have the same widget you started with.

Options are defined in the options property of the widget prototype as shown below in the infobox widget.

JavaScript Copy Code
// Contained in jquery.qs.infobox.js
$.widget('qs.infobox', {
    options: {
        dataUrl: ''
        maxItems: 10,
    },
    ...

To override default options during the creation of the widget, pass them in as an object literal to the widget method as shown below in the startup code.

JavaScript Copy Code
// Contained in startup.widget.js
var infobox = $('body').infobox({
    dataUrl: 'http://feeds.delicious.com/v2/json/popular/'
});

This can be done as many times on an element as needed. The options will always be merged with the options already in the widget.

To read the options from inside the widget, use the options property directly as shown on the last line below.

JavaScript Copy Code
// Contained in jquery.qs.infobox.js
displayTagLinks: function (event, tagName) {
    var i,
        that = this,
        options = that.options,
        url = options.dataUrl + tagName + '?count=' + options.maxItems,
        ...

Reading the values directly off of options is acceptable when reading values from inside the widget, but you should not use this approach when changing the value of options. Instead, use the option method (without an 's').

JavaScript Copy Code
// Code illustration: not in QuickStart
var max = this.option('maxItems');
this.option('maxItems', max + 4);

The option method is called with one argument when reading the option's value, two arguments when setting a value and a single object hash when setting more than one option. The option method should always be used to change the value of options so change notifications will work as expected. Changing the option directly on the options property bypasses the notification mechanism.

When Options Change

If the widget needs to react to an option's value being changed, it should use the _setOption method. This method is called by the widget factory just after the value has been set on the options property. The Widget QuickStart doesn't have a need for _setOption, but if the number of links in the infobox widget were configurable by the user, as an example, the widget might need to adjust the size of the box when maxItems changes.

JavaScript Copy Code
// Code illustration: not in QuickStart
_setOption: function (name, value) {
    if(name === 'maxItems') { 
        this._resizeBoxForMaxItemsOf(value); 
    }
    $.Widget.prototype._setOption.apply(this, arguments);
},

If maxItems is the name of the option being provided, the _resizeBoxForMaxItemsOf method will be called. The last line is calling the base widget's _setOption method. This will set the value of the option and will aid in supporting a disabled state.

Note:
All widgets support the notion of being disabled whether they choose to implement it or not. The Boolean value is stored at this.options.disabled or $(selector).widget('option', 'disabled') if you're asking from the outside. In return for honoring this option (whatever that would mean for the UI and behavior of your widget) the widget factory will default it to false and manage some CSS classes related to theming and accessibility.

The _setOption method is not called for the options passed in during the creation of the widget. When a widget has changed some of its options, inside _create for example, and wants _setOption to be called on each option, a convenient approach is to use the _setOptions method (with an 's') as in the following example.

JavaScript Copy Code
// calls this._setOption on all options
this._setOptions(this.options);

If options are passed to the widget method after it has been created, _setOption will be called on each passed option just before _init is called.

Functions as Options

Defining functions as options is a powerful way to decouple the widget from functionality better located elsewhere.

Note:
The widgets in Mileage Stats use this approach for publishing and subscribing to global events by using their publish and subscribe options and getting data from the dataManager using their sendRequest option. To learn more about the pub/sub engine, see the Communication chapter and the Data chapter for more on the dataManager.

For example, rather than forcing the tagger widget to know how to invoke the public methods on the infobox widget, they can be kept free of any knowledge of each other by passing in the functions from the startup script since it already knows about both widgets. To set this up, the tagger widget defines activated and deactivated options.

JavaScript Copy Code
// Contained in jquery.qs.tagger.js
$.widget('qs.tagger', {
    options: {
        activated: null,
        deactivated: null
    },

Just like normal options, these can either define defaults or not. The startup script will provide these options when it applies the tagger widget to the span elements.

JavaScript Copy Code
// Contained in jquery.qs.tagger.js
$('span[data-tag]').tagger({
    activate: function (event, data) {
        // call displayTagLinks() on infobox here
    },
    deactivate: function () {
        // call hideTagLinks() on infobox here
    }
});

In the above code examples, the options are being set and read from inside the widget's implementation or passed in during creation or initialization. These options can also be read and written to from outside the widget through a public interface. Later in the chapter you'll see how function-based options are used as callbacks for events.

The Widget Method

Well-designed objects have public interfaces that are intentional, intuitive, and focused. Widgets go one step further and provide a single method that represents the entire public interface of the widget. The action the widget performs when you call this method depends on the number and type of arguments provided in the call. In addition to creating and initializing the widget as shown earlier, the widget method is also used to do the following:

  • Invoke public methods
  • Read and write public properties
  • Read and write options

Public Methods

Public methods are defined on the widget prototype as you can see here in the infobox widget. The public methods are hideTagLinks and displayTagLinks.

JavaScript Copy Code
// Contained in jquery.qs.infobox.js
$.widget('qs.infobox', {
    hideTagLinks: function() {
        ...
    },
    displayTagLinks: function(event, tagName) {
        ...
    }

Widgets must be created before their methods can be called. So the following calls to the infobox widget assume the widget method has already been called once to apply the widget to the body element. To call hideTagLinks from outside the widget, use a jQuery selector to match the element and pass the name of the method to the widget method as its only argument.

JavaScript Copy Code
// Code illustration: not in QuickStart
$('body').infobox('hideTagLinks');

When you have to pass arguments into the call, like displayTagLinks, simply add the arguments after the method name.

JavaScript Copy Code
// Code illustration: not in QuickStart
$('body').infobox('displayTagLinks', event, data.name);

The option method covered earlier in Defining Options (not to be confused with the options property) is an example of a public method. When one argument is passed to it, the method will return the value of that option. When two arguments are passed, it will set the option specified in the first argument to the value of the second argument. When calling this method from outside the widget, pass the method name, option, as the first argument, the name of the option as the second, and the value as the third argument as shown here.

JavaScript Copy Code
// Code illustration: not in QuickStart
var max = $('body').infobox('option', 'maxItems', 12);

As you can see above, public methods can also return values by putting the statement on the right hand side of a variable declaration. Returning a value from methods on infobox is reasonable because it is only attached to a single element. But be aware if you call a method on a wrapped set that contains more than one element, the method will only be called on and return from the first element.

In the examples so far, each time the widget method is invoked, it is being called on the instance returned by the jQuery function, $(selector), which requires accessing the DOM. The next section recommends a couple of alternatives.

Reusing an Instance

Each time the jQuery function uses a selector to invoke the widget method it must search the DOM. This has a negative impact on performance and is unnecessary because widget methods return a jQuery object, which includes the wrapped set of matched elements.

JavaScript Copy Code
// Code illustration: not in QuickStart
var ib = $('body').infobox();  // queries the DOM
ib.infobox('displayTagLinks'); // does not query the DOM

Rather than use a selector with the jQuery method each time you need to call a method on a widget, create a variable when the widget is initially attached to the elements. This will access the DOM, but it should be the only time you need to. In subsequent calls, like the second line in the snippet above, you can call the widget method on the variable you created and it won't access the DOM.

Using the Pseudo Selector

In a situation where neither the selector nor the instance is available, there is still a way to obtain all instances of a particular widget. As long as you know the name of the widget you can use a pseudo selector to get all instances that have been applied to elements.

JavaScript Copy Code
// contained in an older, more tightly coupled version of startup.js
$('body').infobox();

// contained in an older, more tightly coupled version of jquery.qs.tagger.js
var ibInstance = $(':qs-infobox');
ibInstance.infobox('displayTagLinks',      // method name
                   $(this).text(),         // tag
                   event.pageY + offsetY,  // top
                   event.pageX + offsetX); // left

The pseudo selector begins with a colon, followed by the widget's namespace and name separated by a hyphen. This selector has the potential to increase coupling between widgets so be aware of this if you intend to use it.

Private Members

Private methods and properties have private scope, which means you can only invoke these members from inside the widget. Using private members is a good idea because they improve the readability of the code.

Methods

Private methods are methods that start with an underscore. They are expected to be accessed directly using this. Private methods are common and recommended.

Private methods are only private by convention. This means if a widget isn't called according to the convention for calling public methods, described later, then its private methods can still be accessed. The convention is easy and consistent, and the underscore makes it easy to distinguish between the public and private interface.

Properties

Unlike methods, properties on the widget prototype are not made private by prepending an underscore – they are private by default. Only methods are made private with underscores. Properties don't need underscores because they cannot be accessed through the widget method.

JavaScript Copy Code
// Code illustration: not in QuickStart
$.widget('qs.infobox', {
    dataUrl: '',   // should only be accessed using this.dataUrl
    _maxItems: 10  // unnecessary, properties are already private
});

Because each element contains its own instance of the widget, the dataUrl property can be different for each element.

Clearly dataUrl is best exposed as an option, but if this was not a configurable option, you would likely want to define it so only one copy of the value was available to all instances of the widget. Let's call these static members.

Static Members

To define a variable that's available to all instances of the widget but nowhere else, place them inside the self-executing function wrapper, but above the call to the widget factory as shown in the tagger widget.

JavaScript Copy Code
// Contained in jquery.qs.tagger.js
(function ($) {

    var timer,
        hideAfter = 1000; // ms

    $.widget('qs.tagger', {
        ...

Because the timer variable is defined outside of the widget prototype, only a single timer will be created and shared across all instances of the tagger widget. Functions that don't rely on the instance of the widget can also be defined here.

If you need access to static members from outside the widget, they can be added to the widget after the widget's definition. A fictitious change can be made to the infobox widget to illustrate this. Inside the displayTagLinks method in the infobox widget, a function variable called displayResult is defined.

JavaScript Copy Code
// Contained in jquery.qs.infobox.js
displayResult = function () {
    // don't need to pass in elem, html, top, or left
    // since they are in scope
    elem
    .html(html);
    .css({top: top, left: left});
    .show();
};

It is defined in displayTagLinks because it's the only method that uses it. If the infobox widget needs to make AJAX calls from other methods, the displayResult function might need to be moved so it is available to all methods that need it. Defining it outside the scope of the widget is a way to make this happen.

JavaScript Copy Code
// Code illustration: not in QuickStart
$.widget('qs.infobox', {
    ...
}); 
$.extend($.qs.infobox, {
    displayResult: function(elem, html, top, left) {
        elem
        .html(html);
        .css({top: top, left: left})
        .show();
    }
});

The $.extend method is used to merge the object passed as the second argument into the object passed as the first argument. Therefore, the displayResult method is merged into the prototype of the widget, $.qs.infobox. With displayResult defined here, the infobox widget can use it from anywhere as shown here.

JavaScript Copy Code
// Code illustration: not in QuickStart
// assume elem, html, top, and left variables were already defined
$.qs.infobox.displayResult(elem, html, top, left);

Events

Events are an effective way to communicate between widgets without forcing them to be tightly coupled. jQuery supports and extends the DOM event model and provides the ability to raise and handle custom events that are not defined in the DOM.

Binding Handlers

Event handlers bind to widget events the same way they bind to other events.

JavaScript Copy Code
// Code illustration: not in QuickStart
$('span[data-tag]').bind('taggeractivated', function(event, data) {
    // handle the event
});

Notice how the name of the event being bound to has had the name of the widget prepended. This is the default behavior for event names. If you would prefer a different name so your code is more readable, this behavior can be changed.

Event Naming

The widgetEventPrefix property defines what will be prepended to the names of the events the widget raises. By default the value is the name of the widget and is set by the widget factory. If you want to use something other than the widget name, simply define this property and provide an alternative value.

JavaScript Copy Code
// Contained in jquery.qs.tagger.js
$.widget('qs.tagger', {

    widgetEventPrefix: 'tag',

    options: {
        activated: null,
        deactivated: null
    },

When widgetEventPrefix has a value, it will be used instead of the widget name.

Raising the Event

The widget naming convention described above is only applicable to the event handler. Inside the widget, the original event name is used to raise the event. The following code sample shows one way the tagger widget might raise the activated event when the mouse enters the element.

JavaScript Copy Code
// Code illustration: not in QuickStart
_create: function () {
    var that = this,
        tag = that.infoboxElement.text();

    that.infoboxElement
        .bind('mouseenter', function (event) {
            that._trigger('activated', event, {name: tag});
        });
},

When trigger is called, the event will be raised and any bindings will be invoked. The problem with binding directly from inside a widget is that it creates more coupling than is needed for event handlers. If the widget is following well-accepted widget design practices, the widget will have callbacks defined in its options.

Relationship to Options

When options are defined as functions and their names correspond to an event name without the prefix, they are referred to as callbacks. The _trigger method on the base widget will automatically invoke the callback whose name matches the event being raised.

JavaScript Copy Code
// Contained in jquery.qs.tagger.js
widgetEventPrefix: 'tag',

options: {
    activated: null,
    deactivated: null
},

_create: function () {
    var that = this,
        name = this.name(),
        tag = this.element.text();

    this.element
        .bind('mouseenter.' + name, function (event) {
            that._trigger('activated', event, {name: tag});
        });
},

The JavaScript that creates the tagger widget can now define the handler for the activated and deactivated events when it creates the widgets.

JavaScript Copy Code
$('span[data-tag]').tagger({
    activated: function (event, data) {
        infobox.infobox('displayTagLinks', event, data.name);
    },
    deactivated: function () {
        infobox.infobox('hideTagLinks');
    }
});

This allows the two widgets to interact without explicitly knowing about each other. Using this approach causes the script that invokes the widgets to act as a connective tissue that describes a lot about the solution in a succinct readable format.

Inheritance

Sometimes when building a widget, another widget already has a lot of what the new widget needs. The widget factory's inheritance support is designed for this case. For illustration purposes, consider the following widget.

JavaScript Copy Code
// Code illustration: not in QuickStart
(function ($) {
    $.widget('a.container', {
        ...
        resize: function() {
            // resize width and height
        },
        ...
    });
}(jQuery));

If this widget was built elsewhere and you want to change its resizing behavior to animate, a reasonable approach would be to inherit from a.container and override its resize method. Inheritance is accomplished by passing three arguments into the widget factory. The first argument is the namespace and name of the widget, the second is the base widget being inherited from, and the third argument is the object prototype of the derived widget.

JavaScript Copy Code
// Code illustration: not in QuickStart
(function ($) {
    $.widget('an.animatedContainer', $.a.container.prototype, {
        ...
        resize: function() {
            // override with animations
        },
    });
}(jQuery));

The only difference between the signature above and the signature usually used for defining widgets is addition of the second parameter.

Inheritance is a useful tool when you are using a widget that almost does what you want it to do. In version 1.9 of jQuery UI, widgets will be able to inherit from themselves, which makes it even easier to extend the functionality of widgets.

Summary

Using the jQuery UI widget factory is a great way to add modularity to client-side web applications. Their lifetimes can be managed with _create, _init, and destroy. Options should have intelligent defaults but can be overridden at any time by passing them to the widget method or calling the option method directly. Functions are a powerful way to decouple functionality and using them for callbacks makes raising and handling events straight-forward. Widgets can have public methods and properties and uses a prepended underscore for private methods. Define functions and variables outside of the widget prototype but inside the self-executing function wrapper when it's appropriate for all instances of the widget to use a single function or variable. Widgets can also be inherited when base functionality can be shared across different widgets. See the next chapter for more places to learn about jQuery plugins and jQuery UI.

Further Reading

Appendix B: Widget QuickStart

Widget Factory documentation on the jQuery UI wiki:
http://wiki.jqueryui.com/w/page/12138135/Widget-factory

jQuery Documentation for Plugins/Authoring:
http://docs.jquery.com/Plugins/Authoring

jQuery UI Developer Guidelines:
http://jqueryui.com/docs/Developer_Guide

jQuery UI source code:
https://github.com/jquery/jquery-ui

Resources

Content Delivery Network (CDN) Addresses:
http://www.asp.net/ajaxlibrary/cdn.ashx