Introduction to Stateful Plugins and the Widget Factory

Doug Neiner | May 21, 2010

 

The jQuery UI Widget Factory is a separate component of the jQuery UI Library that provides an easy, object oriented way to create stateful jQuery plugins. Plugins created using the Widget Factory can be simple or very robust as evidenced by the official jQuery UI widgets, each of which are built on top of the Widget Factory. This article will first look briefly at why you might want to use the Widget Factory, then present an in depth walkthrough of creating a stateful plugin using the Factory.

What is a stateful plugin?

A stateful plugin is an advanced type of jQuery plugin that is self-aware — it maintains its own state, and often provides an external interface for outside code to interact with and alter the plugins state. Stateful plugins, or widgets as they are often called, often trigger events and provide callback hooks into important parts of their functionality. A plugin of this type normally focuses on solving a single task, often constrained to a single area of a finished web page. These widgets try to make as few assumptions about their final use as possible, choosing to expose options instead of internal hard coded settings wherever possible.

Widgets like Date Pickers, Tabs and Dialogs are examples of potentially stateful plugins, though a Media Player, Photo Slideshow, or Shopping Cart widget would also make a good candidate as a stateful plugin. On the other hand, a plugin that takes a paragraph and turns @names into Twitter links would not be a promising use case for a stateful plugin since it needs to run only once per target element.

Why use jQuery UI Widget Factory?

The Widget Factory, like many frameworks, is a tool to assist in rapid, maintainable development. The Widget Factory provides a helpful and consistent structure for creating and interacting with these plugins. It is not necessary to use the Widget Factory when building a stateful plugin, but it does remove many of the redundant tasks involved in setting up a standard configuration. The Widget Factory has the additional benefit of providing an API consistent with jQuery UI, which might afford those who use your plugins a smaller learning curve.

Are they jQuery UI Widgets or jQuery Plugins?

It’s important to realize up front that if you use the Widget Factory to create a stateful jQuery plugin, that you are not creating a jQuery UI Widget or extending jQuery UI. The final product is still a jQuery Plugin. The important difference is that jQuery UI Widgets are officially maintained by the jQuery Project and often have more dependencies on jQuery UI than your plugin is likely to have.

Digging in: Building a jQuery Stateful Plugin

A Countdown Timer

Since stateful plugins are often rather advanced jQuery plugins, we will need to use a simplified example for this walkthrough. I decided on a simple countdown timer that can provide triggers at specific times and will allow the way the information is output to be configurable.

As a fictional use case, assume you are building a demo for a Content Management System where every 15 minutes the system gets wiped clean. This countdown timer needs to be used on both the public site and the backend system to inform the user how many minutes are left until the next install. On the backend, you want to warn the user at 5 minutes left that their work will be deleted shortly and again when the timer hits 0, you want to let them know they need to refresh the page if they want to continue using the system. On the front end, since the user presumably isn’t interacting with the site more than just looking around, you want the page to automatically reload when the timer hits 0. Any other warning on the front end would need to be subtle.

The Markup

The plugin will need to get the end time when it is initialized. The best way to do this is to have the HTML contain a date time string that the plugin can parse and use to display the countdown. For this example, I have chosen to use the new HTML5 time element, though we will make sure the plugin works with normal elements as well. Remember to include the HTML5 shim for IE if you plan on styling the `time` element:

<p id="reset">
  The system will be reset
  <time datetime="2010-05-05T7:13Z">every 20 minutes.</time>
</p>

The plugin will only require any element that either contains the date time string or instead has the datatime attribute set.

Create a basic HTML file and put the previous HTML snippet somewhere within the opening and closing body tags. I will be using HTML5 syntax as we go along, so if you prefer HTML4 or XHTML, be sure to add any required attributes yourself (i.e.type="text/javascript"on the script tags, etc.).

Starting the Widget

The very first thing you will want to do is reference jQuery and the jQuery UI Widget factory. Remember, you do not need to include all of jQuery UI. Go ahead and download jquery.ui.widget.js from Github and add links to jQuery and the widget factory to your HTML file.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="js/jquery.ui.widget.js"></script>

Next, create a blank JavaScript file, and link to it in your HTML file. Your JavaScript file should be named using one of the following patterns:

  • jquery.pluginname.js
  • jquery.namespace.pluginname.js

Note about namespaces: Every Widget Factory widget lives inside a specific namespace or scope. The official jQuery UI Widgets occupy the ui namespace, and ui is included in each file name. You must choose your own namespace, and you should not use ui since it is reserved for officially maintained UI widgets. Many people use their initials or some short variation on their name. Since I own and run Pixel Graphic Design Studio, I will use pixel as the namespace for this walkthrough:

<script src="js/jquery.pixel.countdown.js"></script>

Like any self-respecting plugin author, the first bit of code you want to put in our script is a self-executing anonymous function that will provide a closure where the code can safely run. Be sure to pass the jQuery variable into the function so you can use $ internally without fear of it being overwritten by another library:

(function($){

  // Our code will go here

}(jQuery));

Now add the absolute bare minimum to get a widget instance going:

$.widget('pixel.countdown', {});

That’s it! You must use a dot separated formula of namespace.pluginname when using the widget factory. For instance, the UI DatePicker uses ui.datepicker in the same way. Of course right now your widget won't do much, but let's see just how much functionality it already has because of the widget factory. Add this to your HTML file below the script includes, then load up the file in Firefox with Firebug running.

<script>
  jQuery(function($){
    // Initialize our plugin on all time elements
    $("time").countdown();
  });
</script>

Now, run a few commands and see what you get back:

>>> $(":pixel-countdown")
[ time ]

>>> $("time").data('countdown')
Object { element=,  more...}

>>> $("time").countdown("disable")
[time.pixel-countdown-disabled]

>>> $("time").countdown("enable")
[time]

You can see with just a single call to the widget factory, there is already a lot of functionality built for you:

  1. A custom selector :pixel-countdown that can be used to find all of the widgets on a given page
  2. Access to the actual widget object by calling .data('countdown') on the element itself
  3. The ability to enable or disable the widget by calling countdown() and passing a string of "enable" or "disable" (Though the widget doesn’t change state in response to being enabled or disabled currently).

This is a pretty useless plugin right now, but it was important to understand a few of the shortcuts that were already created for you just by using the Widget Factory. Now it is time to add some meat to the plugin.

Widget Initialization

The Widget Factory class provides a few methods you will need to override to really provide functionality to the plugin. We will start by overriding _create() which is called when the widget is instantiated on an element. In this function, the plugin should attempt to grab the date from either the datetime attribute or the contents of the element. For this demo, we are not implementing a full date/time parser according to the HTML5 spec; we will be expecting the string in this format: “YYYY-MM-DDTHH:MMZ”. All times will be in UTC, based on a 24 hour clock.

Inside our widget, you can refer to this.element to get a jQuery wrapped version of the DOM element on which the plugin was instantiated. Remember: If you are attempting to use this.element inside a second closure, it will fail. Be sure to store this in a variable outside the second closure before trying to access it.

Your create method should look like this:

$.widget('pixel.countdown', {
  _create: function(){
    var dateTime = this.element.attr('datetime') || this.element.text(),
        hour,
        min,
        setup    = false;

    dateTime = dateTime.split('T');

    if( dateTime.length === 2 ){
      // Create a new date based on the year, month, day
      this._endTime = new Date(dateTime[0]);

      // Get hours and minutes
      dateTime = dateTime[1].split(':');
      hour = parseInt( dateTime[0], 10);
      min  = parseInt( dateTime[1], 10);

      if( !isNaN(hour) && !isNaN(min) ){
        this._endTime.setUTCHours(hour, min);
        setup = true;
      }
    }

    // In this case we fail silently, and remove the widget
    // We could throw an error instead if wanted.
    if (!setup){
      this.destroy(); // Undo everything we did
      return false;
    }

    this._originalText = this.element.text();
  }
});

Let’s quickly review what we just wrote. First, we used the function name _create. It’s important to note that method names beginning with an underscore are treated as private functions, and all other methods are treated as public. Users will be able to access your public methods a number of ways, the simplest of which looks something like this:

$("time").countdown("functionname", argument1, argument2);

In the create method the plugin takes care of retrieving and storing the date, but doesn’t do anything with it yet. It additionally stores a copy of the original text content so it can be restored later.

Custom Functions

You can have as few or as many custom functions on your plugin as you want or is necessary. It is becoming apparent that the widget needs a “timeLeft” function where it will subtract the current time from the _endTime variable, and return the information. Since we might also want to use this method from outside the widget, make the method public:

timeLeft: function(){
  var left = this._endTime - (new Date()).getTime(),
      min  = left / 1000 / 60,
      sec  = min % 1 * 60;

  min  = parseInt(min, 10);
  sec  = Math.round(sec);

  return left <= 0 ? [0, 0] : [min, sec];
}

You can access this function from within the widget code by using this.timeLeft() and outside the widget with $(selector).countdown("timeLeft"). Now we need a way to take this information, and display it on the page. Let’s add an update function, and set it up to be called every 1 second. We will use the jQuery.proxy method to ensure this remains equal to the widget object inside the update function.

$.widget('pixel.countdown', {
  _create: function(){
    ...

    this._timer = window.setInterval($.proxy(this.update, this), 1000);
    this.update(); // call initially to avoid 1 second delay
  },
  timeLeft: function() { ... },
  update: function(){
    var timeLeft = this.timeLeft(),
        replacementString = "in %m minutes and %s seconds.";
    if(timeLeft){
      this.element.text(
        replacementString.replace('%m', timeLeft[0])
                         .replace('%s', timeLeft[1])
      );
    } else {
      this.element.text('now');
    }
  }
});

At this point, if you reload the test page, you should see the time element transformed to one of two things: “now” or “in x minutes and x seconds”.  If you see “now”, take a moment to update the date in your HTML to point to some point in the future in UTC time.

Quick Review

So far you have a countdown widget that successfully parses the date and time and replaces the contents of the element with the remaining time. However, you cannot currently:

  1. Remove the widget without issues
  2. Customize how the text gets formatted
  3. Trigger any special callbacks when milestones are hit as our use case requires

Using Options

User configurable options are at the heart of every stateful plugin. Remember to offer as few options as are needed to keep your widgets simple but as many as are needed to give a good amount of outside control. This widget needs at least a few customizable options. Lets add two options as a start: interval, and formatter.

You add options by providing an object literal with keys and values to an options property on the widget. We will revise our _create and updatemethods at the same time to reflect the new options.

$.widget('pixel.countdown', {
  options: {
    interval: 1, // Interval in seconds to refresh the widget
    formatter: function(min, sec){
      if(min === 0 && sec === 0){
        return "now.";
      } else {
        var second_label = sec + (sec == 1 ? " second." : " seconds.");
        if( min === 0){
          return "in " + second_label;
        } else {
          return "in " + min + (min == 1 ? " minute" : " minutes") + " and " + second_label;
        }
      }
    }
  },
  _create:  function() {
    ... 

    this._timer = window.setInterval($.proxy(this.update, this), this.options.interval * 1000);
    this.update(); // call it initially to avoid a delay
  },
  timeLeft: function() { ... },
  update:   function() {
    this.element.text(
      this.options.formatter.apply(this.element[0], this.timeLeft())
    );
  }
});

By using apply when calling the formatter function, you are able to pass the DOM element in as the context. You could just have easily passed in the widget object, but since jQuery developers are used to this being equal to the related DOM node (in event callbacks), this makes a nice practice for the widget as well.

You can get an option’s value by using this.options.key inside the widget and $(selector).countdown("option", "key") outside the widget.

Setting options, however, is something you need to implement differently. Plugins created using the Widget Factory should be able to have their options updated at any time and should respond accordingly to the changes. To facilitate this, the Widget Factory provides a _setOption function you should override when implementing your widget. Here is the function filled out for the two options interval and formatter:

_setOption: function(key, value){
  if (this.options[key] === value) {
      return this; // Do nothing, the value didn't change
  }

  switch (key){
    case "interval":
      // Store the new value
      this.options.interval = value;

      // Clear the old timer
      window.clearInterval(this._timer);

      // Set a new timer based on the new interval
      this._timer = window.setInterval(
        $.proxy(this.update, this),
        this.options.interval * 1000
      );
      break;

    case "formatter":
      this.options.formatter = value;
      this.update();
      break;

    default:
      this.options[key] = value;
  }

  return this;
}

Now, options can be set by calling this._setOption(key,value) internally and by calling either $(selector).countdown("option","key", value) or $(selector).countdown("option", { key: value }) from outside our widget. Because we added code to respond to any changes, the widget is always kept in sync.

Event Binding and Custom Events

Since this widget needs no user interaction, we are not binding any events. However, when you are binding events it is important that you always namespace your events using the following formula: eventName + '.' + this.widgetName. This will allow you to unbind all your widget’s events later without disturbing existing event handlers added from outside your widget. In fact, all events are automatically detached using this formula in the destroy method of the Widget class.

Custom events allow your plugin users to be alerted as important situations occur inside your plugin. To illustrate the use of how to trigger custom events when using the Widget Factory, we will add a milestones option and a milestone_reached custom event to the widget.

In your options literal, add the following:

milestones: [
  { label: "one_minute", minute: 1 },
  { label: "five_minutes", minute: 5, second: 30 }
]

Next we will add a private method to prepare the milestones by first sorting them, then removing all milestones that have already expired when the widget is created. What is really important in this example is that we are not altering the options.milestones value. Instead we are altering a new object which we will store in a different variable (next code block). Since we are altering, sorting, and removing the milestones, it is important the original option remains untouched:

_cleanMilestones: function(){
  var timeLeft   = this.timeLeft(),
      vTL        = timeLeft[0] + (timeLeft[1] / 100),
      milestones = $.map(this.options.milestones, function(ms){
        // Create a new literal that contains only the label
        // and the min/sec as a single value
        return { label: ms.label, time: ms.minute + ((ms.second || 0) / 100) };
      }),
      size       = milestones.length;

  // Sort largest to smallest
  milestones.sort(function(a,b){
    if (a.time === b.time) return 0;
    else return a.time > b.time ? -1 : 1;
  });

  // Remove expired milestones
  while(milestones.length && milestones[0].time > vTL){
    milestones.shift();
  }

  return milestones;
}

Add the following lines to your _create method (before the this._timer ... line). The first line calls the clean function and the second line sets the value of the prefix that is automatically added to each custom event you trigger. The Widget Factory will assume you want widgetname as the value if we do not override it (And who wants to bind to countdownmilestone_reached ?):

this.milestones = this._cleanMilestones();
this.widgetEventPrefix = "countdown_";

Finally we need to update our _setOption method and add an additional case statement:

case "milestones":
  this.options.milestones = value;
  this.milestones = this._cleanMilestones();
  break;

Now that you have our milestones all set up and configured, let’s get down to actually triggering the custom event when needed! Refine your update method as follows. At this time we will also add an additional custom event named complete for when the timer runs out:

update: function(){
  var timeLeft = this.timeLeft(),
      vTL = timeLeft[0] + (timeLeft[1] / 100),
      label;

  if(this.milestones.length && this.milestones[0].time > vTL){
    label = this.milestones.shift().label;
    this._trigger('milestone_reached', null , label);
  }

  if(vTL === 0){
    window.clearInterval(this._timer);
    this._trigger('complete');
  }

  this.element.text(
    this.options.formatter.apply(this.element, timeLeft)
  );
}

This method will now trigger an event named countdown_milestone_reached on the DOM element (in this case, the time element) and pass it an extra parameter with the milestone label. But this is not all the Widget Factory does for you! The _trigger method will also look for an option named milestone_reached and if it is a function, it will call it as well, passing in the label. This means the following two code formats are both valid ways to listen for the custom event from outside the widget:

$("time").countdown({
  milestone_reached: function(label){
    alert(label);
  }
});

// or

$("time")
  .countdown()
  .bind("countdown_milestone_reached", function(e, label){
    alert(label);
  });

Cleaning Up

One glaring issue we have been avoiding is how to clean up all the stuff we are doing if the widget is to be removed. Thankfully the Widget Factory makes this easy as well. Its base destroy function does most of the work for us, so you’ll just need to add a few lines to restore the original text content and remove the timer:

destroy: function(){
  window.clearInterval( this._timer);
  this.element.text( this._originalText);

  // Call the parent destroy method
  $.Widget.prototype.destroy.call(this);
}

Now your widget will properly respond when $(selector).countdown("destroy") is called on it or if .remove() is called on the associated DOM element.

Reader Exercise

When we overrode the _setOption function, we never reimplemented the enabled and disabled code. Another core expectation of a Widget Factory widget, is that the API supports enabling and disabling the plugin. Add the necessary code to support this functionality. Hint, look in jquery.ui.widget.js for an idea on how you might accomplish this.

Using Your Widget

Let’s circle back to the original use cases presented at the beginning of this article and see how it would look if implemented with our new widget:

// Public Facing site
$("#repeat time").countdown({
  milestones: [ { label: "show_warning", minute: 1 }],
  milestone_reached: function(){
    // Turn the label red when under 1 minute remaining
    $(this).css('color','red');
  },
  complete: function(){
    // Reload the current page when the timer stops
    window.location.reload(true);
  }
});

// Backend System
$("#repeat time").countdown({
  milestones: [ { label: "show_warning", minute: 5 } ],
  milestone_reached: function(){
    // Show a HTML warning box
    $("#warning").dialog("show");
  },
  complete: function(){
    alert('The CMS has been reset. If you want to ' +
          'save what you are typing, be sure to ' +
          'copy the text before refreshing the page.');
  }
});

Conclusion

In this article you have learned how to build a stateful plugin using the jQuery UI Widget Factory. You know how to set and use options, initialize and destroy the widget as well as trigger custom events. I challenge you to use the Widget Factory in a real project or for the next open source plugin you write. It is really an amazing tool available to jQuery plugin developers and the best way to learn to use it is by… you guessed it… just using it!

 

About the Author

Doug Neiner is the president and lead developer at Pixel Graphic Design Studio, a web development and design studio founded in 2005. With a love for graphic design and a passion for new technology, Doug is an active contributor to the open source community. During the day, Doug works to solve real-world web and business problems for his clients which include Guardian Life and 2nd Cousin Inc. In the open source field, Doug has released various projects including In-Field Labels jQuery Plugin, Downloadify client side file generation, and Starter, a plugin framework generator for jQuery. Recently he has been working closely with the jQuery Team on a number of projects to improve the infrastructure, design and information flow on the jQuery online properties. Additionally, Doug is an editor at Fuel Your Coding, where he also writes about jQuery, WordPress and general development principles.

In addition to spending time in the open source community, Doug enjoys getting outside with his Nikon D50 and attempting to capture the beauty of God's creation, playing Halo online (gamertag: Pixel Gun), and spending time with his awesome wife and two wonderful kids.

Find Doug on: