Under certain circumstances a ko.computed
will not update even though the ko.observable
that it is bound to changes. I would like to know why, what I am doing wrong and what I should be doing instead.
Example
Consider this simple dozen-to-pieces-converter (JSFiddle here).
HTML
<label>Dozen:</label><br>
<input type="number" data-bind="value: dozen">
<span class="error" data-bind="text: error"></span><br>
<label>Pieces:</label><br>
<input type="number" data-bind="value: pieces"><br>
JavaScript
function ViewModel() {
var self = this;
self.error = ko.observable('');
self.pieces = ko.observable('');
self.dozen = ko.computed({
read: function() {
var p = parseInt(self.pieces(), 10);
if (!p) return '';
if (p % 12 === 0) return p / 12;
else return '';
},
write: function(value) {
if (/\D/.test(value)) {
self.error('Only whole numbers');
} else {
self.error('');
if (value) self.pieces(value * 12);
}
}
});
}
ko.applyBindings(new ViewModel());
What it does
It will display two inputs. One lets you enter dozens (for example 2) and the other will display how many pieces that is (24).
You can also input a number of pieces (for example 36) and the the converter will show how many dozens that is (3).
If you enter something in the pieces
box that isn't divisible by 12, the dozens
input is cleared indicating that the number is not an even dozen. Likewise, we don't allow the user to enter fractional dozens.
How it works
The dozens
input is bound to a KnockoutJS computed observable. It does not hold it's own value (maybe it does under the hood, but this is beyond my knowledge) but is computed from the pieces
property, which is a regular observable. To get the dozens, the pieces is divided by 12 and if the result is a whole number, that is returned, otherwise we return an empty string.
The problem
Start the calculator and try typing 1.5
in the dozens
input. The calculator will inform you that decimals are not allowed. The pieces
field will be left untouched, as it should. However, the dozens
input is not cleared to reflect the value of the pieces
field.
Then enter a new value in the pieces
input, for example 18
. This should clear the dozens
input (as it is a computed
depending only on the pieces
property of the view model, and this value has changed), but it does not.
The value 1.5
keeps being displayed in the dozens
field even though the read()
function of the computed
will never return a decimal like that. The observable
that the computed
is bound to has been updated, but the computed
does not compute.
Only when you enter a number divisible by 12 in the pieces
field, will the dozens
field update.
My questions
Why does the dozens
input keep displaying 1.5
even though the return value from the read()
function is an empty string? Can I force a new read()
inside or after the write()
or otherwise force an update of the UI?
I'm assuming that the reason the dozens
input doesn't update even after changing the value of the pieces
input, is that the result from the read()
function will be the empty string twice in a row and that KnockoutJS caches this value internally and sees that it hasn't changed. Again, how do I force the input to update?
The input keeps displaying 1.5 because the UI and the model are out of sync. The model hasn't received any changes because the value is and always was an empty string. When 1.5 is entered the write function aborts without writing any values to the model so the values are still empty as far as the model is concerned. Then when 18 is entered for Pieces again the function returns an empty string which as you guessed isn't registered as change.
You can force the computed function to update the UI again with this little gem: computed.notifySubscribers()
write: function(value) {
if (/\D/.test(value)) {
self.error('Only whole numbers');
self.dozen.notifySubscribers(); //<--- here
} else {
self.error('');
if (value) self.pieces(value * 12);
}
}