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
dozensinput keep displaying1.5even though the return value from theread()function is an empty string? Can I force a newread()inside or after thewrite()or otherwise force an update of the UI?I'm assuming that the reason the
dozensinput doesn't update even after changing the value of thepiecesinput, is that the result from theread()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()