Notes on KnockoutJS Mapping and ViewModel style guide

I wanted to write down some of the better practices I've settled with since using KnockoutJS for nearly two years now.  A lot of these has to do with just avoiding errors that I see later on, as well as working with the peculiarities of the Knockout Mapping library.

These are not definitive, so let me know if you REALLY disagree!

function PersonViewModel(data) {
  var self = this;
  
  self.firstName = ko.observable();
  self.lastName = ko.observable();
    
  ko.mapping.fromJS(data, {}, self);

  self.name = ko.computed(function(){
    return ko.unwrap(self.firstName) + " " + ko.unwrap(self.lastName);
  });
}

Class Definition

  • Define a class, I always name the class CapitalizedCase, as opposed to camelCase for variables.  Makes it clear when I'm about to instantiate a variable
  • I also always name ViewModel separately from just normal classes.  ViewModel are special, they are for data-binding.  To me, PersonViewModel is different from Person.

You can now create a view model like this.

var personVM = new PersonViewModel();

You can also define a class this way

var PersonViewModel = function(data){
  var self = this;
  
  // ...
};
  • But if you do it this way, understand that Javascript Hoisting is now in effect, and the class is not available until this line.
  • Also, remember the trailing semi-colon if you go for this assign variable syntax.

Property Definition & Knockout Mapping

ko.mapping.fromJS can be used to instantiate properties as a constructor.  But there are potential issues.  Consider:

function PersonViewModel(data) {
  var self = this;
  
  //self.firstName = ko.observable();
  //self.lastName = ko.observable();
  //if we don't define these...
    
  ko.mapping.fromJS(data, {}, self);

  self.name = ko.computed(function(){
    return ko.unwrap(self.firstName) + " " + ko.unwrap(self.lastName);
  });
}

var johnVM = new PersonViewModel( { firstName: "John", lastName: "Liu" });
var marcVM = new PersonViewModel( { firstName: "Marc" });
  • In marcVM, self.lastName is not created by ko.mapping and does not exist, and some binding statements will fail
  • So always declare (annoying) all the observable properties that you need.  Don't trust some REST response to always return data that you need.

Knockout Mapping Definition

I go with this style for defining Knockout Mapping definitions.

function PersonViewModel(data) {
  var self = this;
  self.firstName = ko.observable();
  //...
}
PersonViewModel.mapping = {
  create: function(options){
    return new PersonViewModel(options.data);
  },
  key: function(data) {
    return ko.unwrap(data.firstName);
  }
};
  • The reason is that as you'll see in next example, you need the mapping from time to time, might as well keep it in one place that you can access easily.
  • I have also seen people put mapping on ViewModel.prototype.mapping - the benefit I see with this is that within the class, you can refer to it via self.mapping (but I think that lacks clarity).

You can now do this:

var data = {firstName:"John"};
var person = ko.mapping.fromJS(data, PersonViewModel.mapping);

This gets more fun when you have nested Observable Arrays of Objects

function PeopleViewModel(data) {
  var self = this;
  self.teachers = ko.mapping.fromJS([], PersonViewModel.mapping);
  self.students = ko.mapping.fromJS([], PersonViewModel.mapping);
  
  ko.mapping.fromJS(data, {}, self);
}
PeopleViewModel.mapping = {
  create: function(options){
    return new PeopleViewModel(options.data);
  },
  teachers: PersonViewModel.mapping,
  students: PersonViewModel.mapping
  // Reuse the mapping defined above!
};

var data = { 
  teachers: [
    { firstName: "John" },
    { firstName: "Marc" }
  ],
  students: {
    { firstName: "Bob"}
  }
};
var peopleVM = new PeopleViewModel(data);
var index = peopleVM.teachers.mappedIndexOf({firstName:"Marc"});

// index == 1

Summary

Some notes on structuring your ViewModel classes for readability, avoiding binding issues and Knockout Mapping gotchas.

You can try the example in this Plunker