An Interest In:
Web News this Week
- April 16, 2024
- April 15, 2024
- April 14, 2024
- April 13, 2024
- April 12, 2024
- April 11, 2024
- April 10, 2024
Making Use of jQuery UIs Widget Factory
For a long time, the only way to write custom controls in jQuery was to extend the $.fn
namespace. This works well for simple widgets, however, as you start building more stateful widgets, it quickly becomes cumbersome. To aid in the process of building widgets, the jQuery UI team introduced the Widget Factory, which removes most of the boilerplate that is typically associated with managing a 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
Let’s explore this API, as we build a simple bullet chart widget.
The Bullet Chart Widget
Before we build this widget, let’s understand some of the building blocks of the widget. The Bullet Chart is a concept introduced by Stephen Few as a variation on the bar chart.
The chart consists of a set of bars and markers overlaid on each other to indicate relative performance. There is a quantiative scale to show the actual range of values. By stacking the bars and markers this way, more information can be conveyed without compromising readability. The legend tells the kind of information we are plotting.
The HTML for this chart looks like so:
<!-- Chart Container --><div class="chart bullet-chart"> <!-- Legend --> <div class="legend" style=""> <div class="legend-item"> <span class="legend-symbol marker green"></span> <span class="legend-label">Green Line</span> </div> </div><!-- Chart --> <div class="chart-container" style="width: 86%;"> <!-- Quantitative Scale --> <div class="tick-bar"> <div class="tick" style="left: 0%;"></div> <div class="tick-label" style="left: 0%;">0</div> <div class="tick" style="left: 25%;"></div> <div class="tick-label" style="left: 25%;">25</div> <div class="tick" style="left: 50%;"></div> <div class="tick-label" style="left: 50%;">50</div> <div class="tick" style="left: 75%;"></div> <div class="tick-label" style="left: 75%;">75</div> <div class="tick" style="left: 100%;"></div> <div class="tick-label" style="left: 100%;">100</div> </div> <!-- Bars --> <div class="bar" style="left: 0px; width: 75%;" bar-index="0"></div> <div class="bar blue" style="left: 0px; width: 50%;" bar-index="1"></div> <!-- Markers --> <div class="marker green" style="left: 80%;" marker-index="0"></div> <div class="marker red" style="left: 50%;" marker-index="1"></div> </div></div>
Our widget, which we’ll call jquery.bulletchart
, will dynamically generate this HTML from the data provided. The final widget can be viewed on the demo page, which you can download from GitHub. The call to create the widget should look like so:
$('.chart').bulletchart({ size: 86, bars: [ { title: 'Projected Target', value: 75, css: '' }, { title: 'Actual Target', value: 50, css: 'blue' } ], markers: [ { title: 'Green Line', value: 80, css: 'green' }, { title: 'Minimum Threshold', value: 50, css: 'red' } ], ticks: [0, 25, 50, 75, 100] });
All of the values are in percentages. The size
option can be used when you want to have several bullet charts placed next to each other with relative sizing. The ticks
option is used to put the labels on the scale. The markers and bars are specified as an array of object literals with title
, value
and css
properties.
Building the Widget
Now that we know the structure of the widget, let’s get down to building it. A widget is created by calling $.widget()
with the name of the widget and an object containing its instance methods. The exact API looks like:
jQuery.widget(name[, base], prototype)
For now, we will work with just the name and prototype arguments. For the bulletchart, our basic widget stub looks like the following:
$.widget('nt.bulletchart', { options: {}, _create: function () {}, _destroy: function () {}, _setOption: function (key, value) {} });
It’s recommended that you always namespace your widget names. In this case, we are using 'nt.bulletchart'. All of the jQuery UI widgets are under the 'ui' namespace. Although we are namespacing the widget, the call to create a widget on an element does not include the namespace. Thus, to create a bullet chart, we would just call $('#elem').bulletchart()
.
The instance properties are specified following the name of the widget. By convention, all private methods of the widget should be prefixed with '_'. There are some special properties which are expected by the widget factory. These include the options
, _create
, _destroy
and _setOption
.
options
: These are the default options for the widget_create
: The widget factory calls this method the first time the widget is instantiated. This is used to create the initial DOM and attach any event handlers._init
: Following the call to_create
, the factory calls_init
. This is generally used to reset the widget to initial state. Once a widget is created, calling the plain widget constructor, eg: $.bulletchart(), will also reset the widget. This internally calls_init
._setOption
: Called when you set an option on the widget, with a call such as:$('#elem').bulletchart('option', 'size', 100)
. Later we will see other ways of setting options on the widget.
Creating the initial DOM with _create
Our bulletchart widget comes to life in the _create
method. Here is where we build the basic structure for the chart. The _create
function can be seen below. You will notice that there is not much happening here besides creating the top-level container. The actual work of creating the DOM for bars, markers and ticks happens in the _setOption
method. This may seem somewhat counter-intuitive to start with, but there is a valid reason for that.
_create: function () { this.element.addClass('bullet-chart'); // chart container this._container = $('<div class="chart-container"></div>') .appendTo(this.element); this._setOptions({ 'size': this.options.size, 'ticks': this.options.ticks, 'bars': this.options.bars, 'markers': this.options.markers }); }
Note that the bars, markers and ticks can also be changed by setting options on the widget. If we kept the code for its construction inside _create
, we would be repeating ourselves inside _setOption
. By moving the code to _setOption
and invoking it from _create
removes the duplication and also centralizes the construction.
Additionally, the code above shows you another way of setting options on the widget. With the _setOptions
method (note the plural), you can set mutiple options in one go. Internally, the factory will make individual calls on _setOption
for each of the options.
The _setOption
method
For the bullet chart, the _setOption
method is the workhorse. It handles creation of the markers, bars and ticks and also any changes made to these properties. It works by clearing any existing elements and recreating them based on the new value.
The _setOption
method receives both the option key and a value as arguments. The key is the name of the option, which should correspond to one of the keys in the default options. For example, to change the bars on the widget, you would make the following call:
$('#elem').bulletchart('option', 'bars', [{ title: 'New Marker', value: 50}])
The _setOption
method for the bulletchart looks like so:
_setOption: function (key, value) { var self = this, prev = this.options[key], fnMap = { 'bars': function () { createBars(value, self); }, 'markers': function () { createMarkers(value, self); }, 'ticks': function () { createTickBar(value, self); }, 'size': function () { self.element.find('.chart-container') .css('width', value + '%'); } }; // base this._super(key, value); if (key in fnMap) { fnMap[key](); // Fire event this._triggerOptionChanged(key, prev, value); } }
Here, we create a simple hash of the option-name to the corresponding function. Using this hash, we only work on valid options and silently ignore invalid ones. There are two more things happening here: a call to _super()
and firing the option changed event. We will look at them later in this article.
For each of the options that changes the DOM, we call a specific helper method. The helper methods, createBars
, createMarkers
and createTickBar
are specified outside of the widget instance properties. This is because they are the same for all widgets and need not be created individually for each widget instance.
// Creation functionsfunction createTickBar(ticks, widget) { // Clear existing widget._container.find('.tick-bar').remove(); var tickBar = $('<div class="tick-bar"></div>'); $.each(ticks, function (idx, tick) { var t = $('<div class="tick"></div>') .css('left', tick + '%'); var tl = $('<div class="tick-label"></div>') .css('left', tick + '%') .text(tick); tickBar.append(t); tickBar.append(tl); }); widget._container.append(tickBar); } function createMarkers(markers, widget) { // Clear existing widget._container.find('.marker').remove(); $.each(markers, function (idx, m) { var marker = $('<div class="marker"></div>') .css({ left: m.value + '%' }) .addClass(m.css) .attr('marker-index', idx); widget._container.append(marker); }); } function createBars(bars, widget) { // Clear existing widget._container.find('.bar').remove(); $.each(bars, function (idx, bar) { var bar = $('<div class="bar"></div>') .css({ left: 0, width: '0%' }) .addClass(bar.css) .attr('bar-index', idx) .animate({ width: bar.value + '%' }); widget._container.append(bar); }); }
All of the creation functions operate on percentages. This ensures that the chart reflows nicely when you resize the containing element.
The Default Options
Without any options specified when creating the widget, the defaults will come into play. This is the role of the options
property. For the bulletchart, our default options look like so:
$.widget('nt.bulletchart', { options: { // percentage: 0 - 100 size: 100, // [{ title: 'Sample Bar', value: 75, css: '' }], bars: [], // [{ title: 'Sample Marker', value: 50, css: '' }], markers: [], // ticks -- percent values ticks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100] }, ...}
We start with a size of 100%, no bars and markers and with ticks placed every 10%. With these defaults, our bullet chart should look like:
So far, we have seen how to create the widget using _create
and updating it using _setOption
. There is one other lifecycle method, which will be called when you destroy a widget. This is the _destroy
method. When you call $('#elem').bulletchart('destroy')
, the widget factory internally calls _destroy
on your widget instance. The widget is responsible for removing everything that it introduced into the DOM. This can include classes and other DOM elements that were added in the _create
method. This is also a good place to unbind any event handlers. The _destroy
should be the exact opposite of the _create
method.
For the bullet chart widget, the _destroy
is quite simple:
_destroy: function () { this.element.removeClass('bullet-chart'); this.element.empty(); },
Subclassing, Events and More
Our bulletchart widget is almost feature complete, except for one last feature: legend. The legend is quite essential, since it will give more meaning to the markers and bars. In this section we will add a legend next to the chart.
Rather than adding this feature directly to the bulletchart widget, we will create a subclass, bulletchart2
, that will have the legend support. In the process, we will also look at some of the interesting features of Widget Factory inheritance.