I've built a very data/number heavy app in Knockout. I'm currently getting the error:
Uncaught Error: Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.
This is happening when my custom bindingHandler (which formats the numbers into 'large' form, ie. 123,345,678,987) tries to write back to the original input which displays the value of a computed function.
Value displayed in an input element:
self.value = ko.computed(function(){ return self.chosenAge().population; // 'fetched' from an array. });
Binding Handler:
ko.bindingHandlers.largeNumber = { init: function(element, valueAccessor) { numberInit(element); var value = valueAccessor(); var interceptor = ko.computed({ read: function() { // inject number formatting return numeral(ko.unwrap(value)).format('0,0'); }, write: function(newValue) { // remove formatting when writing a new value value(numeral().unformat(newValue)); } }); // display new value in target element if(element.tagName.toLowerCase() == 'input' ) { ko.applyBindingsToNode(element, { value: interceptor }); } else { ko.applyBindingsToNode(element, { text: interceptor }); } } };
You need to specify a 'write' option in your ko.computed function. Please see the documentation on computed observables. Your binding handler has nothing to do with your value failing to update. Your computed should look something like this:
self.value = ko.computed(function(){ read: function () { return self.chosenAge().population; // 'fetched' from an array. }, write: function (value) { //update your self.chosenAge().population value here }, owner: self });
Hope this helps.
Personally, I would capture the implementation of your formatting in an extender
.
Define an extension that wraps the data observable with a computed
formatter:
ko.extenders.largeNumber = function (target, option) { var formatter = ko.computed( { read: function(){ var formatted = target().toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); return formatted; }, write: function(nv){ var numeric = nv.replace(/,/g,""); target(numeric); console.log("View model now contains " + target()) } }) return formatter; };
Then create an observable that uses the extension: (I've wrapped it in a constructor)
function createExtendedObs(initialValue){ return ko.observable(initialValue).extend({ largeNumber: { viewModel: self } });
In addition, I would make my observable be the population
property within the data object that represents a chosenAge
{ name: "Bronze", population: createExtendedObs(10000) }
This is not a requirement. If you want the population to be stored in another observable, I setup the extender to have access to the viewModel through the options
parameter. Thus, you would access the other observable that way. If the "other" observable were to be a computed, then you would need to specify the write
function (as depicted in the other answer) which is the root of the issue you are having. Another option would be to subscribe
to chosenAge
and set the "other" population observable from within the subscription.
Using the data property directly, I am able to bind to it as follows,
<div data-bind='with: chosenAge'> <input data-bind='value: population'></input> </div>
Check out my fiddle for the full setup.
Edit
Just reviewed a comment about wanting to enter the value "with" the formatting. The field will be formatted each time the computed receives a new value. By default, this will be on focus loss. If you want to modify it as the input is provided then you would need to provide the valueUpdate
binding parameter. Since the field is being manipulated dynamically this will cause caret positioning issues. You would need to manage the caret position accordingly.
<input data-bind='value: population, valueUpdate: "afterkeydown"'></input>