Simple Implementation of a Protected ObservableArray in KnockoutJS

Recently I worked on an exercise building the UI for a table grid-based app,  where you can click on a row to reveal an edit view that presents the record in more detail.  For the bindings, I utilized knockout.js (note this article assumes some basic understanding of how two-way bindings are implemented using knockout).

table grid
The Table Grid
edit record view
The Edit Record View

As part of the exercise I was aiming for usability. And in line with that, it struck me that if you happen to be in the edit view, edit a number of attributes, and then realize you made mistakes, that you should be able to revert back to the original data. Problem is in knockout.js, the bindings done with Observables and ObservableArrays update your viewmodel immediately, leaving you with no way to revert short of another request to the server.

Google to the rescue.  I found this exemplary Knock Me Out blog post by Ryan Niemeyer on how to handle committing or resetting edits on Observables.  I’m hardly one to be above borrowing code from those more talented than myself, so I incorporated it and found his protectedObservable to work fantastic … at least for attributes that would normally be observables.

But I also had a number of observableArrays, and these are where the rollback functionality is especially needed, as relying on a form reset wouldn’t help us with deleted array elements.  Some more googling and I found this jsFiddle, also by Ryan, and I thought I was golden.  Turned out my joy was premature as it didn’t work for me, though this was through no one’s fault but my own.  If I had read his comments more closely I would have seen it was designed to work only with observableArrays of primitive types.  My observableArrays contained objects which themselves contained observables and observable arrays (which, in turn, contained objects).  So I had, at hand, what I thought was a more complex issue.

But the solution turned out to be quite simple!

Basically, I just made a shallow copy of the initial array that we would keep around for the case we want to rollback, and attached “reset” and “commit” functions to an observableArray to create our “protectedObservableArray,” keeping the same interface that Ryan implemented.  Reset basically works by clearing our observableArray, then iterating through our copy of the array, calling .reset() on any elements that have such a function, and pushing those elements onto the observableArray.  Commit is similar in that you iterate through the elements in the observableArray and call .commit() on the element if the function exists, and then resetting our rollback array to a shallow copy of the array underlying our observableArray.  Easy stuff!

//allow commit/reset functionality on an observableArray
ko.protectedObservableArray = function protectedObservableArray(initialArray) {
  var m_rollbackValue = initialArray.slice(),
      result = ko.observableArray(initialArray);

  //commit result values
  result.commit = function () {
    var x,
        len,
        el;

    for (x = 0, len = result().length; x < len; x += 1) {
      el = result()[x];
      if (el._destroy) {
        el._destroyCommitted = true;
      } else if (typeof el.commit === 'function') {
        el.commit();
      }
    }
    m_rollbackValue = result().slice();
  };

  //reset to rollback values
  result.reset = function () {
    var x,
        len,
        el;

    //clear out underlying array in observable
    result().length = 0;

    for (x = 0, len = m_rollbackValue.length; x < len; x += 1) {
      el = m_rollbackValue[x];
      if (typeof el.reset === 'function') {
        el.reset();
      }
      if (el._destroy && !el_destroyCommitted) {
        el._destroy = false;
      }
      result.push(el);
    }
  };

  return result;
};

But there’s always a “but.”  In this case, this protectedObservableArray works fine if all its members are either protectedObservables or protectedObservableArrays. But what if we want our arrays to contain more complex objects whose members, in turn, could be protectedObservables, protectedObservableArrays, or additional objects?  These objects do not have any reset/commit functionality, and in turn, these objects’ members will not have their reset/commit functionality triggered.  Allowing our arrays to contain members that are not primitives was the whole point of coming up with this protectedObservableArray implementation, in the first place.

One approach would be to iterate through all the members of any object, and if said object has an own property “commit” (or “reset”) that is of type “function”, then call the function.  The issue with this approach is we’re going to end up having to handle nested objects, not to mention having to handle objects we don’t want to check (such as functions), and this code is going to become ugly pretty fast.  (But feel free to take it on, if you so desire).

I took what I think is a more elegant, and easier, approach that involved making available the ability to extend our objects with reset/commit functionality.  .reset() would iterate though the members of the object and call their .reset() functions (if they have one), and like-wise for .commit().  Any nesting of objects becomes irrelevant as our “protected” objects share the reset/commit interface the other protected members have.

The code, encapsulated in a constructor function I added to the ko namespace, ended up like this:


ko.ProtectedObjectExtend = function ProtectedObjectExtend() {
  if (!(this instanceof ko.ProtectedObjectExtend)) {
    return new ko.ProtectedObjectExtend();
  }

  this.commit = function () {
    var prop;
    for (prop in this) {
      if (this.hasOwnProperty(prop) && typeof this[prop].commit === 'function') {
        this[prop].commit();
      }
    }
  };

  this.reset = function () {
    var prop;
    for (prop in this) {
      if (this.hasOwnProperty(prop) && typeof this[prop].reset === 'function') {
        this[prop].reset();
      }
    }
  };
};

Now, for any object type we want to have this functionality we can either set or extend its prototype with an instance of ko.ProtectedObjectExtend, or in the case of an object instance we can extend that instance:

//set prototype of object type
MyClass.prototype = new ko.ProtectedObjectExtend();

//extend prototype of object type
MyClass.prototype = $.extend(MyClass.prototype, new ko.ProtectedObjectExtend());

//extend object instance
myObject = $.extend(myObject, new ko.ProtectedObjectExtend());

The only issue with this approach is our objects cannot have members called reset or commit, else only one version of the member will survive the extend call.  So design your objects with that in mind.

And that will do it. If you go back to the demo I built–http://phhht.com/health–you can see it in action on the edit view. Try clicking “Cancel” to undo any changes, and clicking “Save” to commit changes. (Re-click “edit” to return to the edit view and see if it reset or committed as expected).

I’ll be checking this code into my GitHub in the coming days, but in the meanwhile I would appreciate any feedback or questions.

Advertisements