In an earlier post, I commented on using combining the hasFocus and css bindings in KnockoutJS to simplify binding and the need to keep state and track events to manage focus and blur and styling of input elements. This was good, but I have found that it can be improved further. In thinking about how to approach serializing my ViewModel graph in order to send information back to the server after users have interacted with the controls to which it is bound, I happened upon something that should have been obvious if I’d have thought it through, but escaped me earlier and that I have found to be useful and elegant now that I have encountered it.
Serialization for server interaction and challenges
KnockoutJS provides a simple and useful method for serializing ViewModel objects to JSON in the toJSON() method. It makes it easy to serialize ViewModel instances by calling ko.toJS, which recurses over what is passed to it and follows all the references and evaluating the observables to generate a plain JavaScript object with primitive properties (at least at the leaf nodes of the object tree), and then converting this to JSON. This is slick and convenient and works as advertised, but there are some gotchas. When calling ko.toJSON(), I was getting properties included in the resulting JSON I didn’t want included. Among these were Knockout computed properties and some circular references. The circular references caused toJSON to fail. It gave nice and helpful error messages logged to the console that helped identify that circular references are not supported, but left me thinking about how to address this. Also, I was having exactly the same problem stated simply and succinctly here – I didn’t want to send the values of computed properties back to my server. The accepted answer given for the question by Ryan Niemeyer contained a suggestion for using sub-observables. These are properties of observables. As Ryan points out, in Javascript, everything is an object. This includes functions. Because functions are objects, they can have properties. Because observables are functions, they are objects and therefore can have properties. Essentially, an observable is a function on which you can define properties. These “sub-observables” are not serialized by toJSON. They also read nicer when you want to have a property tracking whether a property is being edited, as in my other post. With this trick, I get two benefits: the property for defining whether the title is being edited is not serialized to JSON, and instead of having two properties related only by name, I now have a property with sub-properties.
Instead of:
function ViewModel(durationSeconds) {
var self = this;
self.title = ko.observable('');
self.editingTitle = ko.observable(false);
self.headline = ko.observable('');
self.editingHeadline = ko.observable(false);
... }
I arrived at:
function ViewModel(durationSeconds) {
var self = this;
self.title = ko.observable('');
self.title.editing = ko.observable(false);
self.headline = ko.observable('');
self.headline.editing = ko.observable(false);
... }
Binding is still straightforward:
<input type="text" id="title" data-bind="value:title, css: {editing: title.editing}, hasfocus:title.editing, event: { 'keyup': $root.textboxKeyup }">
The “Live Example 2” on the knockoutjs.com documentation page for extenders demonstrates an extender for adding sub-observables for validation. It’s a brilliant idea further showing the utility of this trick. It also appears there is a plugin for including this functionality seamlessly.
Speaking of extenders, the code above can be improved further still. By using an extender to create the editing property on extended observables, it’s not necessary to explicitly add another property (thought it is necessary to explicitly add the extender). Thus, with the same binding, the earlier cited ViewModel can be simplified further as:
function ViewModel(durationSeconds) {
var self = this;
self.title = ko.observable('').extend(trackEditing: false);
self.headline = ko.observable('').extend(trackEditing: false);
... }
The extender that enables this is the following code:
ko.extenders.trackEditing = function(target, startValue) {
target.editing = ko.observable(startValue || false);
return target;
}
With this small extender and the use of sub-observables, my ViewModel is now a lot easier to read.