Editable Object Graphs in Knockout

Update: this feature is now available as part of the ko.plus library available on GitHub and NuGet!


A little while back I wrote a post about making editable fields in knockout that allow you to enter an “editing” mode and then either persist or discard any changes made during the edit.

This works by storing a copy of the current value when beginEdit is called, then optionally restoring it on cancelEdit.

This is useful for individual fields but generally you want to make entire view models editable, so how can we extend this to work for more complex scenarios?

Extending to Entire Objects

The first step here is to be able to call beginEdit on an entire object instead of each individual field.  Imagine we have the following object:

var ViewModel = function() {
  this.property1 = ko.editable('initial value');
  this.property2 = ko.editable(123);
};

We should be able to call new ViewModel().beginEdit() and have both properties enter edit mode.  We should also have a new isEditing property on the view model itself to indicate global state.

var ViewModel = function() {
  //invoke 'makeEditable' to make the entire object editable
  ko.editable.makeEditable(this);

  this.property1 = ko.editable('initial value');
  this.property2 = ko.editable(123);
};

var viewModel = new ViewModel();
viewModel.beginEdit();
//viewModel.isEditing() === true
//viewModel.property1.isEditing() === true
//etc

viewModel.cancelEdit();
//viewModel.isEditing() === false
//viewModel.property1.isEditing() === false
//etc

Easy enough to describe, but how do we make it work?

First off, we need to append some wrapper methods and an isEditing flag to the target of the makeEditable function:

ko.editable.makeEditable = function(target) {
  //observable flag to hold global object state
  target.isEditing = ko.observable(false);

  target.beginEdit = function() {
    //...
  };

  target.endEdit = function() {
    //...
  };

  target.cancelEdit = function() {
    //...
  };
};

Next, we need each of those functions to do 2 things:

  1. Iterate through all of the editable properties on target and invoke the same method on each
  2. Update the target.isEditing flag appropriately

As number 1 will be pretty much identical for each of the functions, let’s create a helper function to iterate through the editable properties and avoid repeating ourselves (remember, DRY).

var forEachEditableProperty = function(target, action) {
  for (var prop in target) {
    if (target.hasOwnProperty(prop)) {
      var value = target[prop];

      //is the property editable?
      if (value && value.isEditing) {
        action(value);
      }
    }
  }
};

This function iterates through each property on the target object and, if that property has an isEditing flag (i.e. is editable), it will invoke the specified action.  Using this, our 3 functions in makeEditable become…

target.beginEdit = function() {
  forEachEditableProperty(target, function(prop) {
    prop.beginEdit();
  });
  target.isEditing(true);
};

target.endEdit = function() {
  forEachEditableProperty(target, function(prop) {
    prop.endEdit();
  });
  target.isEditing(false);
};

target.cancelEdit = function() {
  forEachEditableProperty(target, function(prop) {
    prop.cancelEdit();
  });
  target.isEditing(false);
};

That’s all we need to do in order to support the immediate children of our current view model, but what about complex object graphs?

Extending to Complex Object Graphs

Imagine that we have the following view model that contains an editable child property and an array of editable children.

var ChildViewModel = function() {
  /* some editable object */
};

var ViewModel = function() {
  ko.editable.makeEditable(this);
  this.property1 = ko.editable('initial value');

  //editable child property
  this.child = new ChildViewModel();

  //array of editable children
  this.children = ko.observableArray([
    new ChildViewModel(),
    new ChildViewModel()
  ]);
};

Ideally we would want the beginEdit method on the parent view model to call beginEdit on all of the children, both in properties and in arrays.

Our existing implementation will already take care of the this.child property – if it’s editable then it will have an isEditing flag, so will be considered to be editable by forEachEditableProperty – but we will need to add some special handling for the array.

var forEachEditableProperty = function(target, action) {
  for (var prop in target) {
    if (target.hasOwnProperty(prop)) {
      //unwrap the value to support observable arrays and properties
      var value = ko.utils.unwrapObservable(target[prop]);

      //direct editables
      if (value && value.isEditing) {
        action(value);
      }

      //editables in arrays
      if (value && value.length) {
        for (var i = 0; i < value.length; i++) {
          if (value[i] && value[i].isEditing) {
            action(value[i]);
          }
        }
      }
    }
  }
};

We are making 2 changes here:

  1. We are unwrapping the property value using ko.utils.unwrapObservable.  This allows us to support editable objects that are in observable arrays and properties.
  2. We are checking the unwrapped value to see if it has a length property, and are iterating through if we find one. This allows us to support arrays (and observable arrays) containing editable children.

This recurses nicely over child objects that themselves have editable children, so this is in fact all we need to do to support complex object graphs.

You can see a working example here and you can find the source code along with unit tests on GitHub.