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.

Advertisements

Editable Fields with Cancelability using Knockout

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


This post is a quick overview of a new feature added to my library of Knockout extensions & helpers which is available on GitHub and partially described here and here.

I quite often find myself wanting to write UI that is read-only until someone clicks on an Edit link, at which point it becomes editable and they can either save or cancel their changes.

A field that becomes editable when a link is clicked

This “edit-in-place” functionality is useful as you don’t need to change much UI and it isn’t too intrusive for quick changes (such as renaming things).

To achieve this behaviour using Knockout you would need the following on the view model:

  • A flag to indicate that the field is in Edit mode
  • A function to enter Edit mode
  • Two functions to exit Edit mode: one that confirms the change and one that rolls back to the original version

To save repeatedly adding the same boilerplate code I have written a simple helper function to encapsulate it: ko.editable.

Usage

To use the editable function, just replace any existing call to ko.observable() with ko.editable():

var ViewModel = function() {
    //equivalent to ko.observable("the title")
    this.field = ko.editable("the title");
};

You can still use the created field exactly as you would any other Knockout observable, but you have the option to bind to 4 new members to gain the editable functionality:

  • isEditing – an observable indicating whether or not the field is in edit mode
  • beginEdit – a function to enter edit mode that also saves the current value so that it can be restored when you cancel
  • cancelEdit – a function to exit edit mode and reset the value to whatever it was when beginEdit was called
  • endEdit – a function to exit edit mode and confirm any changes against the field

You can see a simple implementation here.

<input data-bind="value: field, enable: field.isEditing" />

<a href="#" data-bind="visible: !field.isEditing(), click: field.beginEdit">Rename</a>

<div data-bind="visible: field.isEditing">
    <a href="#" data-bind="click: field.endEdit">Confirm</a> | 
    <a href="#" data-bind="click: field.cancelEdit">Cancel</a>
</div>​

All we are doing is binding the value of an input element to the created field on the view model. We then have 3 links to begin, end and cancel edit mode, as well as a couple of flags to hide and disable elements as applicable.

This is obviously very basic, so I’ve created a slightly more in-depth (and prettier) example here.

Implementation

The implementation itself is relatively simple. We first create a Knockout observable to use as the base field – no point in re-writing anything when we can just re-use it.

ko.editable = Utils.editable = function (initial) {
	var _observable = ko.observable(initial);
	
	return _observable;
};

Next up, we add another observable to act as the isEditing flag…

_observable.isEditing = ko.observable(false);

…and then the 3 methods to enter and exit edit mode…

//start an edit
_observable.beginEdit = function () {
	_observable.isEditing(true);
};

//end (commit) an edit
_observable.endEdit = function () {
	_observable.isEditing(false);
};

//cancel an edit
_observable.cancelEdit = function () {
	_observable.isEditing(false);
};

…and that’s more-or-less it. Everything apart from rolling back the changes on Cancel, that is.

Rolling Back Changes on Cancel

So far we have a field that keeps track of it’s edit state, but we also need it to roll back the value when the user clicks Cancel.

To achieve this we can store the “current” value when the beginEdit function is called, and then re-set that against the base observable inside cancelEdit.

var _rollbackValue;

//...

_observable.beginEdit = function () {
	//store the current value
	_rollbackValue = _observable();
};

//...

_observable.cancelEdit = function () {
	if (!_observable.isEditing()) return;
	
	//re-set the original value
	_observable(_rollbackValue);

	_observable.isEditing(false);
};

Note that we are checking the isEditing flag before doing anything in the cancelEdit function. This is because we only want to be able to roll-back if we are currently editing – once an edit has been either confirmed or cancelled we no longer want to be able to revert to it’s old value.

The full source is available on GitHub (as are the unit tests).

Next

Whilst this is useful, it’s pretty basic and is only applicable to a few scenarios. A sensible progression of this idea is to make entire objects – not just single fields – editable in the same manner.

But that’s a topic for another day…