notoriousb1t

MVP Part 1: KnockoutJS

For the last two weeks I have been building a project called Model View Pizza. It is a simple page that customizes a pizza and lets the user see it being built as each selection is made. I plan to build this same application in at least ten of the more popular JavaScript MV* frameworks in twenty weeks.

I decided to start with KnockoutJS because it looked simple to use, and I have completed some very, very light work with it in past projects. KnockoutJS is an MVVM (Model View ViewModel) framework that facilitates separation of the html (view) and the information (model) by using a presenter (ViewModel) to act as a mediator. This should make the separation of responsibilities clearer and make the different components easier to swap out. I am not really clear if this really happens in practice, but it is better to start out using best practices even if the architecture degrades over time.

Getting Started

I prefer to jump right in and experiment. I've tried it the other way, studiously reading through documents to start, but I have never had the right frame of mind until I have tried and failed a few times getting a simple example running. It would be different if I was totally green not knowing how to code at all. Since I am not, I can leverage my existing knowledge by working this way and avoid analysis paralysis.

After muddling for over ten minutes, I took a look at a few KnockoutJS examples and built something simple. Here is an example of how the bindings look in the view:

View

    <div>
        <button data-bind="click: onClick">Click Me</button>
        <span data-bind="text: hasBeenClicked"></span>
    </div>

Each data-bind attribute has an expression for what properties in the viewmodel correspond to this element. For instance, when the button with click: onClick is clicked, the viewmodel should fire the onClick function that it has. I setup my viewmodel as follows and everything worked:

ViewModel

    function ViewModel() {
        this.hasBeenClicked = ko.observable(false);
    }

    ViewModel.prototype = {
        onClick: function() {
            this.hasBeenClicked(true);
        }
    }

    ko.applyBindings(new ViewModel());

Here is a full example of a very simple binding:

Full Example

See the Pen Simplest KnockoutJS Example by Christopher Wallis (@notoriousb1t) on CodePen.

Now that I had a good understanding of the basics, I moved onward and upward to the next step...

Building the thing...

Constructor

The first thing I did was define a ViewModel. I am a big believer in using actual JavaScript constructors (AKA classes) for viewmodels. I iterated over this quite a few times and this is what I ended up with:

    function ViewModel() {
            var self = this;

            // setup initial order
            this.order = {
                crust: ko.observable(initial.crust),
                size: ko.observable(initial.size),
                preset: ko.observable(initial.preset),
                sauce: ko.observable(initial.sauce),
                cheese: ko.observable(initial.cheese),
                toppings: ko.observableArray([]),
                isCustom: ko.pureComputed(function() {
                    return self.order.preset() === initial.preset;
                })
            };

            // setup explicit change event for presets
            this.order.preset.subscribe(function(newValue){
                self.changePreset(self, newValue);
            });
        }

The constructor sets up the order property with the initial values for each option. The isCustom property is used by the view to determine if the toppings checkboxes should be shown. I used pure computed because it does not cause any side effects.

The second part of the constructor subscribes directly to the preset event and calls a function on the prototype named changePreset. Because the observable has no understanding of the overall viewmodel, we have to gain a reference to it by assigning this to self in the correct scope and then pass it as an argument to changePreset. In my first draft, I thought that KnockoutJS was not providing a reference to this, but that turned out to be incorrect.

Prototype

I wrote the prototype for my ViewModel next. I tried to contain all business logic in it.

    ViewModel.prototype = {
            changePreset: function(vm, newValue) {
                // remove all toppings
                this.order.toppings.removeAll();

                for (var presetName in presets) {
                    // skip presets that don't match this
                    if (presetName !== newValue) {
                        continue;
                    }

                    var preset = presets[presetName];

                    // change sauce to preset
                    this.order.sauce(preset.sauce);

                    // add all preset toppings into
                    var toppings = this.order.toppings;
                    toppings.push.apply(toppings, preset.toppings);
                    break;
                }
            },  
            options: options,
            submit: function() {
                console.log(this);
            },
            reload: function() {
                window.location.reload();
            }
        };

In the prototype, the function changePreset removes all toppings, sets any toppings for the preset, and then the correct sauce. Because KnockoutJS observable arrays do not have a push method that takes an array, I used apply to push each item of the array as an argument.

The submit functions logs the current viewmodel to the console so it can be inspected and the reload button is a passthrough to the window reload function. The actually showing of the modal window is handled by Bootstrap at the moment, so no code has been added for that just yet in the viewmodel.

The options for toppings, sauces, etc. are also set in the prototype because they do not change during the life of the application.

Connecting the dots

The last step in getting binding to work was to well... actually bind it. The code to bind my JavaScript class to HTML was fairly simple:

    ko.applyBindings(new ViewModel());

Select Lists

The select lists were pretty easy to wire up. I simply passed the name of the property that should be mapped as the value and the location of the options to options.

    <select data-bind="value: order.preset, options: options.presets"></select>

Nested Templates

The checkbox list was a little tricky to setup. KnockoutJS has a special binding for iterating over a collection, so that part was not particularly difficult. The checkboxes, however, were quite a lot of trouble. The variable $data refers to the current item in the array when using the foreach binding, the checkedValue property tells KnockoutJS what should be used as the value when it is checked, and the checked property tells KnockoutJS where it should add this value when this is checked. It was a bit much to figure out, but I can't think of a better way to do this.

    <dl data-bind="foreach: options.toppings">
        <dd>
            <label>
                <input data-bind="checkedValue: $data, checked: $root.order.toppings"
                     type="checkbox" />
                <span data-bind="text: $data"></span>
            </label>
        </dd>
    </dl>

Templates or Lack Thereof

One thing that disappointed me about KnockoutJS was the lack of templating or tokenized HTML. This is one area where my view would have been clearer. Instead, I had the choice of building a string as a computed property or using a whole lot of spans. Because this could be accomplished in the view, I begrudgingly created a whole lot of spans.

    <div class="modal-body">
          Your <span data-bind="text: order.size()"></span> <span data-bind="text: order.crust()"></span> pizza has 
        <span data-bind="foreach: order.toppings">
            <span data-bind="text: $data"></span>,
        </span>
        <span data-bind="text: order.sauce()"></span>, and <span data-bind="text: order.cheese()"></span>.  Once you are finished
        with that slice... <h4>be sure to checkout the source on GitHub!</h4>
      </div>

Overall Impression

Overall, I had a good impression of the framework. The design philosophy is pretty solid and despite some aspects being less than intuitive, I found the documentation to be pretty helpful in figuring out the things I couldn't outright guess. I was a little disappointed that I didn't see a templating engine, but I suppose it is not strictly necessary to build out this functionatlity.

I am moving on to my next framework, but I will come back around later and add routing to the KnockoutJS application so there is a fair comparison between MVVM frameworks and MVC frameworks.