An Editor Pattern for Knockout.js Using TypeScript Inheritance

A few months back, Ryan Niemeyer posted about a simple editor pattern for Knockout.js (anyone doing any significant Knockout development should be subscribing to his blog). Historically, I have used the "protected observable" which Ryan outlined in a post in 2011. In short, there are times when you are typing in a value and you need to ability to be able to "accept" or "cancel" user changes. The idea is that in addition the the observables for your object, you have 3 methods: 1) update() – which stores the latest values in a "local cache", 2) revert() – which reverts the entire object back to the previous state, and 3) commit() which updates the "local cache" to the latest state.

The resulting code from Ryan's post looks like this:

var Item = function(data) {
    this.name = ko.observable();
    this.price = ko.observable();
    this.cache = function() {};
 
    this.update(data);
};
 
ko.utils.extend(Item.prototype, {
  update: function(data) {
    this.name(data.name || "new item");
    this.price(data.price || 0);
 
    //save off the latest data for later use
    this.cache.latestData = data;
  },
  revert: function() {
    this.update(this.cache.latestData);
  },
  commit: function() {
    this.cache.latestData = ko.toJS(this);
  }
});

This is an extremely useful pattern that is used frequently in Knockout development. However, I wanted a way to quickly and easily apply this to any object I was working with.

Lately I've been working with TypeScript so my thought was to create a base class in TypeScript that any class could inherit from to give the commit/revert functionality. This actually turned out to be quite easy with TypeScript. The first step is the create a base class called "EditableItem":

/// <reference path="knockout.d.ts" />
 
module EditableExample {
    export class EditableItem {
        private cache: any;
 
        constructor() {
            this.cache = function () { };
        }
 
        revert() {
            this.updateValues(this.cache.latestData);
        }
 
        commit() {
            this.cache.latestData = ko.toJS(this);
        }
 
        updateValues(data) {
            this.cache.latestData = data;
 
            for (var property in data) {
                if (data.hasOwnProperty(property) && this.hasOwnProperty(property)) {
                    this[property](data[property]);
                }
            }
        }
    }
}

The code is not much different from Ryan's example but TypeScript let's me work with class syntax. Notice I'm importing the TypeScript type definition file for Knockout – this can be found in the Definitely Typed repository. The revert and commit methods are practically identical. However, I wanted something more re-usable for assigning the values of the observables so I loop through each property of the data objects to do the assignment automatically. On line #24 above, the assignments happen by sending values into a function rather than assigning with the equals – this is because Knockout observables are all functions so we need to use function syntax. The JavaScript that TypeScript generates is not much different from the original example:

var EditableExample;
(function (EditableExample) {
    var EditableItem = (function () {
        function EditableItem() {
            this.cache = function () {
            };
        }
        EditableItem.prototype.revert = function () {
            this.updateValues(this.cache.latestData);
        };
        EditableItem.prototype.commit = function () {
            this.cache.latestData = ko.toJS(this);
        };
        EditableItem.prototype.updateValues = function (data) {
            this.cache.latestData = data;
            for(var property in data) {
                if(data.hasOwnProperty(property) && this.hasOwnProperty(property)) {
                    this[property](data[property]);
                }
            }
        };
        return EditableItem;
    })();
    EditableExample.EditableItem = EditableItem;
})(EditableExample || (EditableExample = {}));

With this base class in place, it becomes a VERY easy matter to create sub-classes with this automatic functionality:

class Person extends EditableItem{
    constructor(data: any) {
        super();
        this.updateValues(data);
    }
 
    firstName: KnockoutObservableString = ko.observable();
    lastName: KnockoutObservableString = ko.observable();
    age: KnockoutObservableNumber = ko.observable();
}

This enables me to primarily just concentrate on my Person class without worrying about all of the plumbing details in the base class to make commit/revert work. Now we can use the Person object as follows:

var person = new Person({ firstName: "Steve", lastName: "Michelotti", age: 21 });
person.commit(); // commit current state
person.revert(); // reverts to previous state

You can customize this example even further for your application. For example, the constructor takes a data object typed as "any" but you could constrain this further – the underlying updateValues() function can still take any value. Additionally, you could consider passing the initial data object directly to the constructor of the base class – that is, pass it to the super() function. However, there is a major caveat you need to be aware of if you are thinking of going this route. Take a look at the generated JavaScript code for the Person class:

var Person = (function (_super) {
    __extends(Person, _super);
    function Person(data) {
            _super.call(this);
        this.firstName = ko.observable();
        this.lastName = ko.observable();
        this.age = ko.observable();
        this.updateValues(data);
    }
    return Person;
})(EditableItem);

Notice the call to the super() function is the first thing that is called. Then after that is the assignments to the observable fields. After that is the call to the updateValues() function. Therefore, if you want to pass the data directly to the super() function, you'll need to explicitly assign each field as an observable before calling the super() function to avoid the fields being null when the EditableItem's updateValues() function is trying to assign the observable values. The class would look like this:

class Person extends EditableItem {
    constructor(data: any) {
        this.firstName = ko.observable();
        this.lastName = ko.observable();
        this.age = ko.observable();
        super(data);
    }
 
    firstName: KnockoutObservableString;
    lastName: KnockoutObservableString;
    age: KnockoutObservableNumber;
}

Ultimately, I didn't favor this approach because it requires me to put extra lines of code in my constructor as compared to just initializing the observables inline. Either way, TypeScript enables rich scenarios for code re-use and helping better organize your JavaScript into object-oriented code.

Tweet Post Share Update Email RSS