Hi folks
Since releasing v1.1.0, the #1 most requested feature has been an
easier way of getting data in and out of Knockout.
=== How it works in v1.1.0 ===
The approach I've used is receiving data in JSON form from the server
and manually looping over it, converting it into client-side models
with selected properties represented as observables. There are
benefits to my approach - for example you can add extra model
functions and dependentObservables as part of the mapping - but it
seems that most newcomers expect a more automated solution.
For example, given some incoming data,
function personModel(data) {
this.firstName = ko.observable(data.firstName);
this.job = {
title : ko.observable(data.job.title),
isVoluntary : ko.observable(data.job.isVoluntary)
};
this.pets = ko.observableArray(data.pets);
// Can add other dependentObservable properties or model functions
here
}
// Now map the actual data
var viewModel = {
people : ko.utils.arrayMay(incomingPeople, function(personData) {
return new personModel(personData);
})
}
=== Proposed new helpers ===
To automate some of this, I'm considering adding 4 new helper
methods:
* ko.fromJS - receives a plain JavaScript object graph (including
arrays, nested properties, etc) and makes certain parts of it
observable:
- all arrays are mapped to observableArrays
- leaf properties (that is, properties of type string, number, or
boolean) are mapped to observables
- all other structure in the object graph is mapped unchanged (not
made observable)
* ko.fromJSON - receives a JSON string, parses it, and then calls
ko.fromJS on the result
* ko.toJS - takes an arbitrary object graph (e.g., your view model)
and copies it to a new object graph in which all observables (and
observable arrays) are replaced by their current values. In other
words, strips out everything to do with KO and makes plain old
JavaScript objects/arrays. This might be more convenient for sending
to the server.
* ko.toJSON - takes an arbitrary object graph, copies it to a new
object graph stripping out the observables (by calling ko.toJS) and
then stringifies the result into JSON format. This will be easy to
send to the server.
The reason why ko.fromJS only makes *leaf* properties observable (and
not every point in the object graph) is that it's much less confusing
for the developer, and I think is more likely to be what you want. For
example, the following data structure:
{
firstName : "Steve",
job : {
title : "Spaceman",
isVoluntary : true
},
pets : ["Rod", "Jane", "Freddy"]
}
... gets mapped to become equivalent to:
{
firstName : ko.observable("Steve"),
job : {
title : ko.observable("Spaceman"),
isVoluntary : ko.observable(true)
},
pets : ko.observableArray(["Rod", "Jane", "Freddy"])
}
Notice that "job" itself isn't observable (it's unlikely that you'd
want it to be), but the leaf properties on "job" are. Similarly,
"Rod", "Jane" and "Freddy" aren't put inside observable wrappers (most
developers would find that unexpected) - they are just entries on an
observable array.
Now you can bind this to some DOM elements and templates and will be
able to update "firstName", "job.title", "job.isVoluntary", and the
entries in "pets", and the UI gets updated.
Then, when you want to send the data back to the server, you call
ko.toJS(myModel) or ko.toJSON(myModel) and you get back a purely non-
observable data structure suitable for transmission to the server,
without having to manually map it.
=== Possible drawbacks ===
The reason I have been a bit reluctant to implement helpers like this
is that the mapping rules are pretty subjective. In most cases you'll
want leaf properties to be observable, but in some case you might not,
or you might also want intermediate levels in the graph to be
observable. Also, sometimes you'll want to add dependentObservables or
other functions at certain points in the object graph.
If you have any custom requirements at all, then ko.fromJS/ko.fromJSON
isn't going to work for you - you'll have to write custom mapping
logic like before. That's fine, but I'm just slightly concerned that
some developers will think that ko.fromJS/ko.fromJSON is the only
possible way to handle data and will forego the ability to make nice
rich client-side view models with appropriate functions on them.
=== Please try it out and give me your feedback ===
I've implemented the helpers described above in a prototype release of
Knockout, version 1.1.1pre, which you can get from
http://github.com/SteveSanderson/knockout/tree/master/build/output/
I'd appreciate it if you would practically try using these helpers
with your current code to see if it actually meets your needs. That's
the only way to really know if this is the right way forwards or
whether it's actually a dead end. Do they reduce the amount of code
you have to write, or in the end do you always need to write custom
mapping logic anyway?
Final note: Don't start relying on these new helpers for your
production code just yet as they may be removed if they turn out to
confuse us more than they help us :)
Thanks!