Search code examples
backbone.jsknockout.jsmodelviewmodelknockback.js

Using a KnockBack ViewModel, is there a way to create a computed observable from the underlying Backbone model's methods?


Using a KnockBack ViewModel, is there a way to create a computed observable from the underlying Backbone model's methods?

As an example, in javascript:

var MyModel = Backbone.model.extend({
        validate: function () {
            return this.get('name').length < 0;
        }
    }),
    baseModel = new MyModel({name: 'foo'}),
    kbViewModel = kb.viewModel(baseModel),
    modelContainer = document.querySelector('#myModel');
ko.applyBindings(kbViewModel, modelContainer);

and in the Knockout markup:

<div id="myModel">
    <div data-bind="css:{'invalid': !validate()}">
        <input type="text" data-bind="value: name" />
    </div>
</div>

When I try to run this, I get the error:

Unable to process binding "css: function (){return {'invalid':!validate()} }"
Message: validate is not defined

Am I doing something wrong, or do I need to create the observable in the ViewModel manually?

var MyModel = Backbone.Model.extend({
        validate: function () {
            return this.get('name').length > 0;
        }
    }),
    MyKBViewModel = kb.ViewModel.extend({
        constructor: function (model) {
            kb.ViewModel.prototype.constructor.call(this, model);
            this.validate = ko.pureComputed(function () {
                return this.name().length > 0;
            }, this);
        }
    }),
    baseModel = new MyModel({name: 'foo', class: 'bar'}),
    kbViewModel = new MyKBViewModel(baseModel),
    modelContainer = document.querySelector('#myModel');

ko.applyBindings(kbViewModel, modelContainer);

jsfiddles: without observable, with observable


Solution

  • A little preamble: Knockback puts the model in MVVM, where Knockout is really just VVM. Knockback also gives you some automatic synchronization between them, which is nice. But you still need to keep in mind that the model and the viewmodel are two different pieces of your app. The viewmodel isn't just a Knockout copy of the Bootstrap model. Don't put viewmodel pieces in the model.

    So you need to decide whether validation is a viewmodel behavior or a model behavior. I say viewmodel, because you want to use it in the view. So remove it from the model and define a computed.

    However, if you wanted it to be part of the model, you would want to define an attribute, not just a method, and a model event handler that would update the attribute on change. Knockback will dutifully copy the attribute into the viewmodel, and you can use it there.

    var MyModel = Backbone.Model.extend({
        validate: function() {
          this.set('isValid', this.get('name').length > 0);
        },
        initialize: function() {
          this.validate();
          this.on('change:name', this.validate);
        }
      }),
      baseModel = new MyModel({
        name: 'foo',
        isValid: null
      }),
      kbViewModel = kb.viewModel(baseModel);
    
    kbViewModel.validate = ko.observable();
    
    modelContainer = document.querySelector('#myModel');
    
    ko.applyBindings(kbViewModel, modelContainer);
    .invalid {
      background-color: red;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.2/backbone.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockback/1.0.0/knockback.min.js"></script>
    <div id="myModel">
      <div data-bind="css:{'invalid': !isValid()}">
        <input type="text" data-bind="value: name" />
      </div>
    </div>