Hey folks
The latest source code build of 1.1.2pre at
https://github.com/SteveSanderson/knockout/tree/master/build/output/
contains a very interesting new feature that opens up a lot of
possibilities. Previously, dependentObservables could only be read,
not written to, because their value was computed from other underlying
observables. But now you can optionally specify a "write" callback for
your dependentObservables to supply your own logic for writing a new
value back to the underlying observables.
The way to do this is to construct your dependentObservable as
follows:
someModelProperty : ko.dependentObservable({
read: function() {
// This is the normal evaluator function - the place where you
put the logic
// to return the current value of your dependentObservable
(usually as a
// function of other model properties)
},
write: function(value) {
// Here, put your logic to update the underlying model
properties to
// match the incoming "value"
}
})
What scenarios does this enable?
=========================
The benefit of this is that you can now use dependentObservable as a
kind of "value converter" (as well as the things you previously used
it for). Let's say you have a form like this:
Price excluding tax: <textbox> (bound to an observable)
Price including tax: <textbox> (bound to a dependentObservable that
adds 15% to above value)
You can model this as follows:
var viewModel = {
price: ko.observable(100)
};
viewModel.priceIncludingTax = ko.dependentObservable({
read: function() { return viewModel.price * 1.15 },
write: function(value) { viewModel.price(value / 1.15) }
});
Now the user can edit either text box, and the other one will be
updated. If they edit "price", then "priceIncludingTax" will be
updated by adding 15% to "price". If they edit "priceIncludingTax",
then "price" will be updated by subtracting 15% from
"priceIncludingTax".
I used the phrase "value converter" because priceIncludingTax converts
the underlying "price" value to another format (i.e., by adding 15%)
for display and editing.
Another scenario: Parsing data entry
==========================
Have a look at the simple example at
http://knockoutjs.com/examples/helloWorld.html.
It would now be possible to define "fullName" as a writable
dependentObservable so you could bind it to a *textbox* (not just a
span) so the user could edit the full name, and their entry would be
parsed and used to update the underlying "firstName" and "lastName"
values:
viewModel.fullName = ko.dependentObservable({
read: function () {
// Compute full name from the underlying observables
return viewModel.firstName() + " " + viewModel.lastName();
},
write: function(value) {
// Parse "value" and update the underlying observables
var firstSpacePos = value.indexOf(" ");
if (firstSpacePos < 0) return; // Reject entry if it doesn't include
a space
viewModel.firstName(value.substr(0, firstSpacePos));
viewModel.lastName(value.substr(firstSpacePos + 1));
}
});
This scenario also demonstrates that a single writable dependent
observable can read/write multiple underlying observables.
Another scenario: Restricting data entry
=============================
Several people have recently asked for a way to restrict what values
can be written to an observable. This is now pretty easy, because you
can have a dependentObservable whose "read" function just returns the
value of your underlying observable, and whose "write" function only
writes an incoming value to the observable if it matches your
criteria. Then you bind your UI to that dependentObservable, not to
the underlying observable. For example,
viewModel.lastName = ko.observable("some initial value");
viewModel.lastNameEditingValue = ko.dependentObservable({
read: viewModel.lastName,
write: function(value) {
// For example, only accept values with length less than 20
if (value.length < 20)
viewModel.lastName(value);
}
})
And now bind a textbox to lastNameEditingValue, and it will only
update lastName when the user enters strings of length less than 20.
In case you think the syntax is a bit cumbersome, you could create
your own extension called "intercept" like this:
Function.prototype.intercept = function(callback) {
var underlyingObservable = this;
return ko.dependentObservable({
read: underlyingObservable,
write: function(value) { callback.call(underlyingObservable,
value) }
});
};
... and then you could just have the following in your view model:
viewModel.lastName = ko.observable("some initial
value").intercept(function(value) {
// Only accepts strings of length < 20
if (value.length < 20)
this(value);
})
In this case, viewModel.lastName is now the writable dependent
observable, and its underlying storage is an anonymous hidden
observable.
Comments / Questions
=================
Many thanks to Luc who originally suggested this feature. If anyone
has any feedback, post here. All being well, this feature will be
included in the 1.1.2 release and there'll be full docs published for
it at that time.