Knockout-jQueryUI

Fork me on GitHub

Extensibility

The library uses classical JavaScript inheritance which makes adding/extending the bindings relatively simple.

Class diagram

The BindingHandler class connects the widget's options/events and the view model's properties/functions.

Properties:

  • widgetName: Widget name
  • widgetEventPrefix: Basically when the widget triggers an event it prefixes the event name with this string. Actually the algorithm of constructing the event name is more complex, you can find it in jQuery UI's Widget._trigger() method. This property is only used by BindingHandler.on()
  • options: Names of the widget's options.
  • events: Names of the widget's events.
  • hasRefresh: Does the widget have a refresh() function?

Methods:

  • init: The binding's "init" callback as described in knockout's documentation.
  • update: The binding's "update" callback as described in knockout's documentation.
  • on: Helper method which subscribes to a widget-triggered event.

Simple widgets

To create a binding for a new widget you should derive a new class from BindingHandler, set the options, events and hasRefresh properties, and optionally override the init() and/or update() methods.

Class diagram for simple widgets

The example below creates a simple widget which changes the color of its element. Then a binding is created for the widget.

HTML

<div data-bind="colorizer: { color: color }, click: toggleColor" style="cursor: pointer;">Lorem ipsum dolor sit amet</div>

JavaScript

// Create the widget.
$.widget( "custom.colorizer", {
    options: {
        color: "black"
    },
    _setOption: function ( key, value ) {
        switch ( key ) {
            case "color":
                this.element.css( "color", value );
                break;
        }
        this._super( key, value );
    }
});

// The binding handler's constructor function.
Colorizer = function () {
    // invoke the base class's constructor with the widget's name
    kojqui.BindingHandler.call(this, 'colorizer');

    // specify the widget's options
    this.options = [ "color" ]
};

// Set the prototype chain. kojqui.utils.createObject() is a simple wrapper around
// Object.create() which also works with pre-ES5 browsers.
Colorizer.prototype = kojqui.utils.createObject(kojqui.BindingHandler.prototype);
Colorizer.prototype.constructor = Colorizer;

// Register the new binding handler with knockout.
kojqui.utils.register(Colorizer);

// It's time to test our new widget and binding handler.
vm1 = {
    color: ko.observable( "black" ),
    toggleColor: function () {
        vm1.color( vm1.color() === "black" ? "red" : "black" );
    }
};

Result

Lorem ipsum dolor sit amet

Derived widgets

Creating a binding for derived widgets is similar to creating bindings for simple widgets, but you should derive from the widget's binding handler instead of BindingHandler.

Class diagram for derived widgets

The lengthy example below extends the dialog widget with a minimize/restore functionality, then creates a binding for it.

HTML

<style>
    .custom-minidialog .ui-dialog-title { width: 70%; }
    .custom-minidialog .custom-minidialog-titlebar-additionalicon { position: absolute; right: .6em; top: 50%; width: 20px; margin: -10px 20px 0 0; padding: 1px; height: 20px; }
</style>
<div title="Dialog Title" data-bind="minidialog: { isOpen: isOpen, minimized: minimized, resizable: false }">I'm a dialog</div>
<input id="isOpen" name="isOpen" type="checkbox" data-bind="checked: isOpen" />
<label for="isOpen">Visible</label>
<input id="minimized" name="minimized" type="checkbox" data-bind="checked: minimized" />
<label for="minimized">Minimized</label>

JavaScript

// creates the dialog-inherited widget
$.widget("custom.minidialog", $.ui.dialog, {
    _create: function () {
        var that = this,
            uiDialogTitlebarMinimize;

        // invoke the base widget's _create() method
        this._super();

        // the unique class helps to define the selectors for the custom css rules
        this.uiDialog.addClass("custom-minidialog");

        // the state is stored in a member variable
        this.minimized = false;

        // adds the minimize/restore anchor to the titlebar
        uiDialogTitlebarMinimize = $("<a href='#'></a>")
            .addClass("custom-minidialog-titlebar-additionalicon ui-corner-all")
            .attr("role", "button")
            .click(function (event) {
                event.preventDefault();
                that._toggleState(event);
            })
            .appendTo(this.uiDialogTitlebar);

        // sets the icon
        (this.uiDialogTitlebarMinimizeText = $("<span>"))
            .addClass("ui-icon ui-icon-minus")
            .text("minimize")
            .appendTo(uiDialogTitlebarMinimize);

        this._hoverable(uiDialogTitlebarMinimize);
        this._focusable(uiDialogTitlebarMinimize);
    },
    _toggleState: function (event) {
        this.minimized ? this.restore(event) : this.minimize(event);
    },
    minimize: function (event) {
        // swaps the icon
        this.uiDialogTitlebarMinimizeText
            .removeClass("ui-icon-minus")
            .addClass("ui-icon-newwin")
            .text("restore");
        // hides the content
        this.element.hide();
        // stores the state in the member variable
        this.minimized = true;
        // raises the minidialogminimize event
        this._trigger("minimize", event);
    },
    restore: function (event) {
        // opposite of the minimize() method
        this.uiDialogTitlebarMinimizeText
            .removeClass("ui-icon-newwin")
            .addClass("ui-icon-minus")
            .text("minimize");
        this.element.show();
        this.minimized = false;
        this._trigger("restore", event);
    }
});

// The binding's constructor function.
var Minidialog = function () {
    // invoke the base class's constructor
    kojqui.Dialog.call(this);

    // override/extend the base class's properties
    this.widgetName = "minidialog";
    this.widgetEventPrefix = "minidialog";
    this.events.push("minimize", "restore");
};

// Set the prototype chain. kojqui.utils.createObject() is a simple wrapper around
// Object.create() which also works with pre-ES5 browsers.
Minidialog.prototype = kojqui.utils.createObject(kojqui.Dialog.prototype);
Minidialog.prototype.constructor = Minidialog;

Minidialog.prototype.init = function (element, valueAccessor) {
    var value = valueAccessor();

    // invokes the base class's init() method
    kojqui.Dialog.prototype.init.apply(this, arguments);

    // connects the viewmodel's 'minimized' observable to the widget
    if (value.minimized) {
        ko.computed({
            read: function () {
                if (ko.utils.unwrapObservable(value.minimized)) {
                    $(element).minidialog('minimize');
                } else {
                    $(element).minidialog('restore');
                }
            },
            disposeWhenNodeIsRemoved: element
        });
    }
    if (ko.isWriteableObservable(value.minimized)) {
        this.on(element, 'minimize', function () {
            value.minimized(true);
        });
        this.on(element, 'restore', function () {
            value.minimized(false);
        });
    }

    return { controlsDescendantBindings: true };
};

// Register the new binding handler with knockout.
kojqui.utils.register(Minidialog);

// It's time to test our new widget and binding handler.
vm2 = {
    minimized: ko.observable(false),
    isOpen: ko.observable(false)
};

Result

I'm a dialog