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:
- Iterate through all of the editable properties on
target
and invoke the same method on each - 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:
- We are unwrapping the property value using
ko.utils.unwrapObservable
. This allows us to support editable objects that are in observable arrays and properties. - 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.