Mapping with inheritance using KnockoutJS

I just came across an instance where i wanted to map objects using KnockoutJS (and the mapping plugin) to a set of objects I have that use inheritance. I tried several methods, but finally came across one I like and though I would share it.

For instance, say you have DTO objects in a database and you’re using KnockoutJS to show those objects on the page. For argument’s sake, say we have the following:

public abstract class Person
  1. {
  2.     public Int32 Id { get; set; }
  3.     public String Name { get; set; }
  4. }
  5. public class Employee : Person
  6. {
  7.     public String Department { get; set; }
  8. }
  9. public class Customer : Person
  10. {
  11.     public String Company { get; set; }
  12. }

You might change these in to JavaScript objects similar to:

function Person(x){
  1.   this.Id = ko.observable(x.Id);
  2.   this.Name = ko.observable(x.Name);
  3. }
  4. function Employee(x){
  5.   this.Department = ko.observable(x.Department);
  6.  
  7.   Person.call(this, x);
  8. }
  9. Employee.prototype = Object.create(Person.prototype);
  10. function Customer(x){
  11.   this.Company = ko.observable(x.Company);
  12.  
  13.   Person.call(this, x);
  14. }
  15. Customer.prototype = Object.create(Person.prototype);

Okay, but now how do we map it? It’s actually pretty easy. Keep in mind that I use the presence/absence of properties to determine the type to use, but you could just as easily use a discriminator column. Anyways, on with it:

function Person(x){
  1.   this.Id = ko.observable();
  2.   this.Name = ko.observable();
  3.  
  4.   // we add this to make it easier to flag them over to the right. If you had
  5.   // a discriminator column, this wouldn't be necessary.
  6.   this.Type = x.Department ? 'Employee' : x.Company ? 'Customer' : 'Person';
  7.    
  8.   ko.mapping.fromJS(x, {
  9.     include: ['Id', 'Name']
  10.   }, this);
  11. }
  12. function Employee(x){
  13.   this.Department = ko.observable(x.Department);
  14.  
  15.   ko.utils.extend(this, new Person(x));
  16.   ko.mapping.fromJS(x, {
  17.     include: ['Department']
  18.   }, this);
  19. }
  20. function Customer(x){
  21.   this.Company = ko.observable(x.Company);
  22.  
  23.   ko.utils.extend(this, new Person(x));
  24.   ko.mapping.fromJS(x, {
  25.     include: ['Company']
  26.   }, this);
  27. }
  28.  
  29. function App(data){
  30.     this.Persons = ko.observableArray();
  31.    
  32.     ko.mapping.fromJS(data,{
  33.         'Persons': {
  34.             create: function(x){
  35.                 // here we look at which properties exist and
  36.                 // use the appropriate object
  37.                 if (x.data.Department){
  38.                     return new Employee(x.data);
  39.                 } else if (x.data.Company){
  40.                     return new Customer(x.data);
  41.                 } else {
  42.                     // default behavior
  43.                     return new Person(x.data);
  44.                 }
  45.             },
  46.             key: function(x){
  47.                 return x.Id;
  48.             }
  49.         }
  50.     }, this);
  51. }
  52. var d = {
  53.     Persons: [
  54.         { Id: 1, Name: 'Joe', Department: 'Software' },
  55.         { Id: 2, Name: 'Bob', Department: 'Software' },
  56.         { Id: 3, Name: 'Sue', Company: 'WYSIWYG Inc' },
  57.         { Id: 4, Name: 'Bill', Department: 'Accoutns Receivable' },
  58.         { Id: 5, Name: 'Susanne', Company: 'Google' },
  59.         // just to show what a defaulted binding looks like
  60.         { Id: 6, Name: 'Ben' }
  61.     ]
  62. };
  63. ko.applyBindings(new App(d));

Basically, we use ko.utils.extend to bring in the inherited members, then ko.mapping.fromJS to map the properties. Note that only the object itself cares about the properties it has to offer (because the base “class” maps its own).

As I mentioned, you can use a discriminator property to find out which object to map to, but you don’t have to. Just keep in mind it gets a little tricky to decipher which object’s which at the UI level without that property.

Happy coding!

Flattr this!

Brad Christie

Software programmer from New Hampshire who enjoys the challenge and fulfillment of accomplishing any size task with ingenuity and persistence. I currently work at Sitecore as a Solutions Architect and an MVC evangelist.