Blog of Raivo Laanemets

Stories about web development, consulting and personal computers.

Kontainer - View/ViewModel lifecycle manager for KnockoutJS

On 2015-04-15

Last week I have been working on Kontainer library which is a View/ViewModel lifecycle manager for KnockoutJS. It consists of the rewritten code (about 100 lines) that I have extracted from my Single-Page-Application projects.

The purpose of the Kontainer library is to make it easier to use KnockoutJS Views/ViewModels for defining application views without sacrificing performance and to have a well-defined lifecycle for them.

The library has no built-in routing solution but allows to use any front-end router (crossroads, Sammy, etc).

Why not components?

I have previously attempted to use KnockoutJS built-in components for this purpose but there have been issues:

  • By-default async loading causes reflows and UI jerkiness. This was fixed in 3.3.0.
  • No well-defined lifecycle (inserted/bound/rendered?). This is still an open issue. It might get fixed in 3.4.0.
  • Switching views synchronously is hard.

The async loading is useful with AMD but not with browserify+brfs.

Sync view switching

Existing solutions with components for view switching use the component binding with the name as an observable (this is actually more involved as component params has to be bound too):

ko.components.register('main', ...);
var model = { view: ko.observable('main') };
ko.applyBindings(model);

And in HTML:

<body>
  <!-- ko component: { name: view } --><!-- /ko -->
</body>

To switch the view you simply call model.view('something'). However, if there is some data to load with the view then the following happens:

  • Old component is destroyed (sync)
  • New component is loaded (sync/async)
  • Component data is loaded (async)
  • Component data is applied (sync)

Loading the component data over HTTP takes some time and this makes the UI reflow/jerkiness strongly noticeable.

Solution

To solve the problem, only a single batch of sync modifications has to be applied to the DOM. This can done using the Kontainer library:

var show = kontainer.create('body'); // selector or dom element

var template = fs.readFileSync(...); // bundled using brfs
var model = { message: ko.observable('Hello world') };

show(template, model);

And in HTML:

...
<body></body>

Then the view switching is done by using:


var otherTemplate = fs.readFileSync(...); // bundled using brfs

fetch('/some/data').then(function(data) { // loads data async using promises/callbacks
    show(otherTemplate, otherModel.create(data));
});

And the following happens:

  • Component data is loaded (async)
  • Old ViewModel is unbound and disposed (sync)
  • New template is inserted into the page DOM tree (sync)
  • The KO bindings are bound and rendered (sync)

The UI has no extra jerkiness as there is no async operation between the removal of the old view and the insertion of the new. Kontainer also includes support for these callbacks (on the ViewModel) for fine-tuning the template/resulting DOM:

  • inserted(DOM target) - called when the template is inserted into the DOM tree.
  • bound(DOM target) - called when the ViewModel is bound to the DOM tree.

And for reclaiming possible non-gc resources (timers, WebSocket connections, etc):

  • dispose(DOM target) - called when the template and the ViewModel are replaced.

Support

The library supports all browsers starting with IE8. Installation can be done from NPM (@jhermsmeier, great thanks for letting me use the package name on NPM!). CommonJS/AMD/vanilla browser environments are supported. Source code/documentation/issues are on GitHub.