Stateful jQuery UI Widget Design Pattern

The standard design pattern for building stateful plugins in jQuery is the jQuery UI Widget Factory. This article provides an overview of the Widget Factory pattern, explaining how it manages state, handles component lifecycles, and encapsulates behavior. By standardizing initialization, option handling, event triggering, and teardown logic, this pattern allows developers to build robust, maintainable, and object-oriented user interface components without manually tracking plugin instances.

The jQuery UI Widget Factory

The jQuery UI Widget Factory, accessed via $.widget(), provides an object-oriented base for creating stateful jQuery plugins. When a widget is initialized on an element, the factory creates an instance object, attaches it to the DOM element using jQuery's internal data cache, and bridges method calls from the standard jQuery API to the instance methods.

$.widget("custom.myWidget", {
    // Widget implementation goes here
});

The naming convention requires a namespace and a widget name separated by a dot (e.g., "custom.myWidget"). This creates both a jQuery method ($(selector).myWidget()) and a constructor under the specified namespace ($.custom.myWidget).

Core Architecture and Lifecycle Methods

A stateful widget built with the factory relies on standardized lifecycle hooks:

  • options: An object containing default configuration values. When a widget instance is created, user-provided options are automatically merged with these defaults.
  • _create(): The primary setup method. It is executed once per element upon initial instantiation. All DOM manipulation, event binding, and one-time configuration should occur here.
  • _init(): Runs after _create() and also fires whenever the widget is called again with no arguments or only an options object. It is used for reset or re-initialization logic.
  • _destroy(): Cleans up DOM modifications, unbinds custom event listeners, and removes any injected styles or classes to return the element to its pre-widget state.

Method Visibility and Event Handling

The Widget Factory enforces encapsulation through naming conventions and internal utilities:

  • Public vs. Private Methods: Methods prefixed with an underscore (such as _create or _render) are treated as private and cannot be invoked from the public jQuery API. Public methods (such as enable, disable, or custom public functions) can be invoked using string method calls: $(selector).myWidget("myMethod", arg1).
  • Event Triggering (_trigger): Widgets communicate state changes to parent applications using this._trigger("eventName", eventObject, data). This automatically handles both callback execution defined in the options and standard jQuery DOM event dispatching (prefixed with the widget name, e.g., mywidgeteventname).
  • Managing State (_setOption): Changes made via the option method trigger _setOption(key, value). Overriding this method allows the widget to respond dynamically to configuration updates during runtime.

Implementation Example

Below is the standard boilerplate implementation demonstrating these concepts:

$.widget("custom.progressbar", {
    // Default options
    options: {
        value: 0,
        change: null,
        complete: null
    },

    // Constructor/Initialization
    _create: function() {
        this.element.addClass("custom-progressbar");
        this._update();
    },

    // Handling runtime option changes
    _setOption: function(key, value) {
        if (key === "value") {
            value = this._constrain(value);
        }
        this._super(key, value);
        this._update();
    },

    // Public method
    value: function(val) {
        if (val === undefined) {
            return this.options.value;
        }
        this.option("value", val);
    },

    // Private worker method
    _update: function() {
        var progress = this.options.value + "%";
        this.element.text(progress);
        this._trigger("change");

        if (this.options.value === 100) {
            this._trigger("complete");
        }
    },

    _constrain: function(val) {
        return Math.max(0, Math.min(100, val));
    },

    // Cleanup logic
    _destroy: function() {
        this.element
            .removeClass("custom-progressbar")
            .text("");
    }
});

Using the Widget

Once registered, the widget maintains state per instance:

// Initialization
$("#my-element").progressbar({ value: 20 });

// Reading state via public method
console.log($("#my-element").progressbar("value")); // 20

// Modifying state
$("#my-element").progressbar("value", 50);

// Removing the instance and restoring DOM
$("#my-element").progressbar("destroy");