9

I have 3 observable variables in view-model, and want to output to formatted value. However, I don't want to write computed method for each of them since they are identical. What is the best way to reuse the code? Thanks.

The code I am going to achieve is:

   this.formattedPrice = ko.computed({
        read: function () {
            return '$' + this.price().toFixed(2);
        },
        write: function (value) {
            // Strip out unwanted characters, parse as float, then write the raw data back to the underlying "price" observable
            value = parseFloat(value.replace(/[^\.\d]/g, ""));
            this.price(isNaN(value) ? 0 : value); // Write to underlying storage
        },
        owner: this
    });

And the failed example is in: Jsfiddle

Thanks,

RajeshKdev
  • 6,029
  • 6
  • 53
  • 76
Gary Zi
  • 95
  • 1
  • 6

1 Answers1

17

Here are a couple ways that you can make this reusable.

If you want to handle this in your view model, then a good choice is to create an extension that will store the formatted computed observable as a "sub-observable" of your original. You can extend observables using extenders or by adding to the shared fn object as described here. I prefer the latter.

So, you could add a function to observables called withCurrencyFormat. It might look like:

ko.observable.fn.withCurrencyFormat = function(precision) {
    var observable = this;
    observable.formatted = ko.computed({
        read: function (key) {
            return '$' + (+observable()).toFixed(precision);
        },
        write: function (value) {
            value = parseFloat(value.replace(/[^\.\d]/g, ""));
            observable(isNaN(value) ? null : value); // Write to underlying storage 
        }        
    }); 

    return observable;
};

Now, you can say:

 self.week1Amount = ko.observable(w1).withCurrencyFormat(2);
 self.week2Amount = ko.observable(w2).withCurrencyFormat(2);
 self.week3Amount = ko.observable(w3).withCurrencyFormat(2);

and bind against it in the UI like:

    <td><input data-bind="value: week1Amount.formatted" /></td>
    <td><input data-bind="value: week2Amount.formatted" /></td>
    <td><input data-bind="value: week3Amount.formatted" /></td>

Sample here: http://jsfiddle.net/rniemeyer/xskJN/

Another choice is to move this into a binding, so you can leave your view model alone. This would use similar code, but in a custom binding handler that might look like:

ko.bindingHandlers.valueAsCurrency = {
    init: function(element, valueAccessor) {
        var observable = valueAccessor(),
            formatted = ko.computed({
                read: function (key) {
                    return '$' + (+observable()).toFixed(2);
                },
                write: function (value) {
                    value = parseFloat(value.replace(/[^\.\d]/g, ""));
                    observable(isNaN(value) ? null : value); // Write to underlying storage 
                },
                disposeWhenNodeIsRemoved: element                
            });

        //apply the actual value binding with our new computed
        ko.applyBindingsToNode(element, { value: formatted });
    }        
};

So, in the binding handler we are create our computed and then using the value binding against it.

Now, your view model would need no changes and you would bind in the UI like:

    <td><input data-bind="valueAsCurrency: week1Amount" /></td>
    <td><input data-bind="valueAsCurrency: week2Amount" /></td>
    <td><input data-bind="valueAsCurrency: week3Amount" /></td>

Sample here: http://jsfiddle.net/rniemeyer/sD6y4/

Tomalak
  • 306,836
  • 62
  • 485
  • 598
RP Niemeyer
  • 113,706
  • 17
  • 286
  • 210
  • I have another question, how to apply this to the sum field, which is read-only. Again, my not working code is http://jsfiddle.net/sD6y4/4/. Since I have sum1, sum2, ..., fields. Is it possible to make a reusable function in this case? Thanks in advance! – Gary Zi Dec 26 '12 at 19:01
  • You could add a `textAsCurrency` binding that just does the `read` part and applies the `text` binding. Might look like: http://jsfiddle.net/rniemeyer/3nrKD/ – RP Niemeyer Dec 26 '12 at 19:11
  • Cool way. I didn't know there are so many ways to solve it. Now I have another question: After updating the model from server via ajax, UI was not refreshed. I guess it is because I am using an array. The example is http://jsfiddle.net/3nrKD/1/. Is it possible to make it work? Thanks a lot. – Gary Zi Dec 26 '12 at 21:05
  • Please ignore my previous comment. I found the issue, and made it works here, http://jsfiddle.net/3nrKD/3/. Thanks. – Gary Zi Dec 26 '12 at 21:12
  • Hi RP. I found an example, like http://jsfiddle.net/3nrKD/5/. So my understanding is that if you update the observable variable, week1Amount, then the UI would be updated. But if you create a new one. It doesn't change. Is my understanding right? Thanks. – Gary Zi Dec 26 '12 at 21:27
  • This is great, however neither method appears to update the UI if the user changes the value such that the underlying value is the same. e.g. change $10.00 to $10 and it won't be updated back to $10.00. Is there a way around that? I can ask as a separate question if you'd prefer – Dan May 14 '15 at 16:26