knockout.jsecmascript-6knockout-components

Knockout Component Not Binding View Model (ES6)


I'm sure I'm missing something entirely obvious here, but I'm trying out ES6 for the first time, and after five days of getting nowhere, I figured I'd open this up the community.

I have a view model class:

class TestViewModel
{
  constructor(params)
  {
    this.firstName = ko.observable(params.firstName);
    this.message = ko.computed(function() { return 'Hello, ' +     this.firstName() + '!' }, this);
  }
}

export default { viewModel: TestViewModel, template: templateMarkup };

(Ignore the template, it's just a paragraph tag using an import)

Then there's an entry point:

"use strict";
import $ from 'jquery';
import ko from 'knockout';
import comp from '../test-model/test-model';

ko.components.register("test-model", {
  viewModel: comp.viewModel,
  template: comp.template
});

let m = new comp.viewModel({ firstName: "world" });

$("document").ready(function() {
  ko.applyBindings(m);
});

My page has a simple component:

<test-component></test-component>

When I view the page, the element contains my component's template. Instead of display the message "Hello, world!", the page displays "Hello, undefined!". I've debugged the process multiple times, and it always successfully creates an instance of TestViewModel with the proper parameters. But the view model that gets bound to the page is generated after that by the createViewModel function in Knockout. What am I missing in my set up to bind my instance of the model to the component?


Solution

  • You're mixing up components and the root view model. Your constructor will be called twice:

    1. Once because you new it yourself on the let m... line;
    2. Once because your view instantiates the component, telling KO to create an instance of your viewModel;

    Instead you need something like this:

    "use strict";
    
    class TestViewModel
    {
      constructor(params)
      {
        this.firstName = ko.observable(params.firstName);
        this.message = ko.computed(() => 'Hello, ' + this.firstName() + '!');
      }
    }
    
    var templateMarkup = "<p data-bind='text: message'></p>";
    var comp = { viewModel: TestViewModel, template: templateMarkup };
    
    ko.components.register("test-component", {
      viewModel: comp.viewModel,
      template: comp.template
    });
    
    $("document").ready(function() {
      ko.applyBindings({});
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    
    <test-component params="firstName: 'world'"></test-component>

    This seems to work, but I still recommend being careful with this. The ko.components' viewModel entry is called as a constructor function, and I personally don't know the subtle differences between a constructor function and a ES6 class. Based on the docs you could play it safe and use a custom view model factory instead:

    ko.components.register("test-component", {
      viewModel: { createViewModel: (params, componentInfo) => new comp.viewModel(params) },
      template: comp.template
    });