New feature: Writable dependent observables

633 views
Skip to first unread message

fla...@gmail.com

unread,
Dec 3, 2010, 1:19:40 AM12/3/10
to KnockoutJS
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.

Ω Alisson

unread,
Dec 3, 2010, 5:08:28 AM12/3/10
to knock...@googlegroups.com
This is great Steven, thanks!

EisenbergEffect

unread,
Dec 3, 2010, 10:18:02 AM12/3/10
to KnockoutJS
Beautiful! I'll be trying it out in my project soon...

On Dec 3, 1:19 am, "st...@codeville.net" <fla...@gmail.com> wrote:
> Hey folks
>
> The latest source code build of 1.1.2pre athttps://github.com/SteveSanderson/knockout/tree/master/build/output/
> Have a look at the simple example athttp://knockoutjs.com/examples/helloWorld.html.

EisenbergEffect

unread,
Dec 3, 2010, 10:34:58 AM12/3/10
to KnockoutJS
Success!!! This worked beautifully for me. Thanks for implementing
this Steve! This is going to open up a lot of important scenarios for
rich MVVM. I look forward to sharing more detail with you about what I
am building soon.

On Dec 3, 1:19 am, "st...@codeville.net" <fla...@gmail.com> wrote:
> Hey folks
>
> The latest source code build of 1.1.2pre athttps://github.com/SteveSanderson/knockout/tree/master/build/output/
> Have a look at the simple example athttp://knockoutjs.com/examples/helloWorld.html.

abp

unread,
Jan 18, 2011, 10:25:42 AM1/18/11
to KnockoutJS
Hi,

this is a great feauture. I'm now using the intercept function
extension.

But having an additional callback "commit" in dependentObservabls,
when using valueUpdate:'afterkeydown' would be nice.
The i could filter while typing and apply a formatting after editing
is done.

I'm now doing this with a custom binding:
ko.bindingHandlers.commitValue = {
init: function(element, valueAccessor, allBindings) {
$(element).blur(function() {
valueAccessor()();
});
}};

valueAccessor is just a callback here, which sets the
dependentObservables value.

On 3 Dez. 2010, 07:19, "st...@codeville.net" <fla...@gmail.com> wrote:
> Hey folks
>
> The latest source code build of 1.1.2pre athttps://github.com/SteveSanderson/knockout/tree/master/build/output/
> Have a look at the simple example athttp://knockoutjs.com/examples/helloWorld.html.

green

unread,
Jan 18, 2011, 5:37:10 PM1/18/11
to knock...@googlegroups.com
I am not sure what you are intent to do. Looks like you want to change a value of an observable while focus leaving the input. But shouldn't this be implemented with "value" binding? Like <input type="text" data-bind="value: firstName"></input>, which will set the value to firstName observable automatically when input value changed, you can even pass valueUpdate: 'afterKeyDown' to "filter while typing'

abp

unread,
Jan 19, 2011, 5:03:43 AM1/19/11
to KnockoutJS
Hi green,

i know i can filter. But i also need to convert after the edit.

For example i declare a binding like this:
<input type="text" data-bind="value: money, valueUpdate:
'afterkeydown',
commitValue: function()
{money(formatCurrency(money()));}">

Then in my View Model:
var SomeModel = function(data)
{
// Map json data
ko.mapping.fromJS(data, {}, this);
// enforces numeric input format like 1,000,000.00 while user is
typing
this.money = this.money.intercept(enforceNumericFormatIntercept);
}

valueUpdate: 'afterkeydown'
Applies input filter via dependentObservable write, created with
intercept in model.
The input filter ensures numeric input with decimal point etc.

commitValue: commitValue: function(){money(formatCurrency(money()));}
Is only applied once after the user leaves the field and finally
"commits" the value
by doing so. Now formatCurrency can expand users input to the desired
format, for example user types:

1000

leaves field, formatCurrency applied, new value:

1,000.00

I hope this clearifies what i meant.

Doing:
Function.prototype.intercept = function(writeCallback, commitCallback)
{
var underlyingObservable = this;
return ko.dependentObservable({
read: underlyingObservable,
write: function(value)
{ writeCallback.call(underlyingObservable,
value) },
commit: function(value)
{commitCallback.call(underlyingObservable,
value)}
});

};

would be nice. :)
But this needs a dependentObservable extension.
Reply all
Reply to author
Forward
0 new messages