ko.widget

is building block for your web application

Learn more

Overview

Big amount of rich UI functionality is always become difficult. Component Driven Development is aimed to manage code complexity.

ko.widget is JavaScript library which resolve code complexity during building rich UI in Component Driven Development way. It depends on KnockoutJS and jQuery. All examples use RequireJS, ko.widget can be used even without RequireJS.

How To Use

Creating Widget

To make ko.widget working your View and ViewModel should be wrapped in Widget class. See Hello World example:

<div>
    <h1>Hello World Widget</h1>
    <div data-bind="text: title"></div>
</div>
define(["knockout"], function (ko) {

    return function HelloWorldViewModel() {
        this.title = ko.observable("Hello World");
    };

});
define(["ko.widget", "./HelloWorldViewModel", "text!./HelloWorldView.htm"], function (Widget, ViewModel, View) {

    return function HelloWorldWidget() {
        Widget.extend(this, [new ViewModel(), View]);
    };

});

Here is first working widget ready to be used. Note: combine and minimize it using r.js (RequireJS Optimizer tool).

Injecting Widget

HelloWorldWidget ready to be added into DOM. There are two way to do it:

1. Calling appendTo(element) widget's method
require(["jquery", "App/HelloWorldWidget/HelloWorldWidget", "domReady!"], function ($, HelloWorldWidget) {

    var app = new HelloWorldWidget();
    app.appendTo($("body"));

});
2. Using inject binding
require(["knockout", "AppViewModel", "domReady!"], function (ko, AppViewModel) {

    ko.applyBindings(new AppViewModel());

});
define(["knockout", "App/HelloWorldWidget/HelloWorldWidget"], function (ko, HelloWorldWidget) {

    return function AppViewModel() {
        this.helloWorld = ko.observable(new HelloWorldWidget());
    };

});
<html>
<head>
    <!-- Script registration is omitted -->
</head>
<body data-bind="inject: helloWorld">
</body>
</html>

Composition

To simplify code a developer should split UI functionality into small pieces then compose them. ko.widget gives this possibility, each widget can contain nested widgets. Technically composition implemented by using inject binding inside the widget.

Example below shows how can HelloWorldWidget be used inside HelloCompositeWidget.

<div>
    <h1>Hello Composite Example</h1>
    <div data-bind="text: title"></div>
    <div data-bind="inject: helloWorld"></div>
    <h1>Bye Composite Example</h1>
</div>
define(["knockout", "App/HelloWorldWidget/HelloWorldWidget"], function (ko, HelloWorldWidget) {

    return function HelloCompositeViewModel() {
        this.helloWorld = ko.observable(new HelloWorldWidget());
        this.title = ko.observable("Hello Composite");
    };

});
define(["ko.widget", "./HelloCompositeViewModel", "text!./HelloCompositeView.htm"], function (Widget, ViewModel, View) {

    return function HelloCompositeWidget() {
        Widget.extend(this, [new ViewModel(), View]);
    };

});

Isolation

By default widget is strongly isolated. ViewModel and View cannot be reached from outside, View context variable $root point in widget's ViewModel.

Any ViewModel's method can be shared for outside call, use widget's exportMethods for this. Note: init and dispose methods of ViewModel are exported by default.

define(["ko.widget", "./PanelViewModel", "text!./PanelView.htm"], function (Widget, ViewModel, View) {

    return function HelloCompositeWidget() {
        Widget.extend(this, [new ViewModel(), View]);

        this.exportMethods("show", "hide", "visible");
    };

});

Panels and Windows

Panel or Window is usual widget. Only one difference is absolute positioning View. There is may be a problem when widget injected in the element with relative positioning and z-index specified. windowInject binding solves this problem, it appends specified widget to the document body. windowInject and inject bindings are equivalent and fully substitutable.

Single Page Application

Page is again usual widget. Single Page Application consist of widgets only, in term of ko.widget, there is no anything new and special.

require(["knockout", "AppViewModel", "domReady!"], function (ko, AppViewModel) {

    var app = new AppViewModel();
    app.init();
    ko.applyBindings(app);

});
define(["knockout", "simrou", "App/HelloWorldWidget/HelloWorldWidget", "App/HelloCompositeWidget/HelloCompositeWidget"], function (ko, Simrou, HelloWorldWidget, HelloCompositeWidget) {

    return function AppViewModel() {
        var self = this;
        this.page = ko.observable(null);

        this.init = function () {
            var router = new Simrou({
                '/hello-world': function () { self.page(new HelloWorldWidget()) },
                '/hello-composite': function () { self.page(new HelloCompositeWidget()) }
            });
            router.start('/hello-world');
        };
    };

});
<html>
<head>
    <!-- Script registration is omitted -->
</head>
<body>
    <header>Header | <a href="#/hello-world">Page 1</a> | <a href="#/hello-composite">Page 2</a></header>
    <section data-bind="inject: page, injectAnimation: 'fadeIn'"></section>
    <footer>Footer</footer>
</body>
</html>

Simrou used for routing ability. It allows capture query parameters and then they can be pass into page widget through init method call. injectAnimation binding adds animation during page transition.

Unit test cover

Here you can  Run unit tests and see  Source code to find out more use cases.

Fork me on GitHub