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.
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).
HelloWorldWidget ready to be added into DOM. There are two way to do it:
require(["jquery", "App/HelloWorldWidget/HelloWorldWidget", "domReady!"], function ($, HelloWorldWidget) { var app = new HelloWorldWidget(); app.appendTo($("body")); });
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>
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]); }; });
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"); }; });
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.
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.
Here you can Run unit tests and see Source code to find out more use cases.