Home > Uncategorized > Use of Knockout in Nimbah

Use of Knockout in Nimbah

As well as Nimbah being a tool that does something I occasionally need, it’s also a chance to experiment. (Also by writing this up, I’m probably going to spot some things I can simplify.)

While I’ve been using Knockout¬†regularly in my work for a few months, it’s been as a tool that I apply here and there where appropriate. But in Nimbah I decided to go for broke and base the entire thing on Knockout. So in essence its a single view model, the whole document is bound once to the the view model, and when you edit the pipeline you are just editing the view model.

A simplified schematic version of the view model is:

var viewModel = {
    inputText: ko.observable(localStorage.getItem('savedInputText') || ''),
    selected: ko.observable(),
    pipeline: ko.observable(null),
    layout: ko.observable('vertical')
};

ko.computed(function() {
    var root = viewModel.pipeline();
    if (root) {
        root.inputValue(viewModel.inputText());
    }
});

viewModel.outputText = ko.computed(function() {
    var root = viewModel.pipeline();
    return root ? viewModel.stringify(root.outputValue()) : '';
});

In the UI the inputText and outputText properties are each bound to a textarea (the output one being readonly).

But internally, thanks to the ko.computed parts, they are really little more than aliases for two properties on the root pipeline: inputValue and outputValue. The underlying model of Nimbah is based on nodes that all follow this same pattern. A pipeline is a node and operators are nodes, and some operators contain their own pipelines as child nodes, and so on.

The building block is the node function, which builds a raw node – again in simplified schematic form:

var node = function() {
    var model = {};
    model.readOnlyChildren = ko.observableArray();

    model.insertAfter = function(newChild, existingChild) ...
    model.insertBefore = function(newChild, existingChild) ...
    model.remove = function() ...
    model.parent = ko.computed({
        read: function() ...
        write: function(val) ...
    });
    model.firstChild = ...
    model.lastChild = ...
    model.nextSibling = ...
    model.previousSibling = ...

    return model;
};

Each node has (possibly null) references to its parent, firstChild, lastChild, nextSibling and previousSibling, all of which are shielded by ko.computed with read/write operations so that they remain consistent. For example, if you assign node A to be the nextSibling of node B, that’s equivalent to B.parent().insertAfter(B, A), which ensures that B also becomes the previousSibling of A and that B’s parent is now also A’s parent.

There’s an observable array of the node’s children, called readOnlyChildren to try to emphasise that it isn’t meant to be directly modified. I have to expose it because it’s what the UI binds to, but its contents are automatically maintained (via individual insertions and deletions) by the above “proper” node properties.

Why do it this way? Because of the way nodes obtain their input values. If an operator has a previousSibling, it uses that sibling’s outputValue as its own inputValue. If it has a null previousSibling, it’s the first operator in its pipeline, so it should be using the pipeline’s inputValue. And guess what? The pipeline is its parent, so it has no problem getting the inputValue from it. Hence for any operator node, inputValue looks like this:

model.inputValue = ko.computed(function() {
    return model.previousSibling() ? model.previousSibling().outputValue() :
           model.parent() ? model.parent().inputValue() : null;
});

In a pipeline it’s a lot simpler:

model.inputValue = ko.observable(null);

This is because pipelines can act as the root of everything, or be used in whatever way an operator wants to use them. So it’s up to the owner of the pipeline to feed it with the right inputValue.

Of course, the outputValue of a node has to be entirely its own responsibility – the whole point of a node is how it turns a given input into the right output. For a pipeline, it’s just the last child node’s outputValue (or for an empty pipeline it’s just the inputValue):

model.outputValue = ko.computed(function() {
    return model.lastChild() ? model.lastChild().outputValue() : 
           model.inputValue();
});

Each kind of operator has to implement outputValue differently. Here’s split:

model.outputValue = ko.computed(function() {
    var input = model.inputValue();
    if (input && typeof input.split == 'function') {
        return input.split(model.separator());
    }
    return input;
});

So the upshot of all this is that whenever you edit the text in the input textarea, it ripples through all the operators in the pipeline completely automatically, through the miracle of ko.computed. If a node is unplugged from some location and then plugged in somewhere else, everything updates accordingly. It’s just beautiful!

And that’s before we even get on to the joy of how undo/redo just bolts onto the side without any real effort…

Categories: Uncategorized Tags: ,
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: