Seperate KO View formating and ViewModel state

A normal scenario with Knockout is that you want date and number observables to be formatted to the current culture of the client, this is easily implemented in a Knockout extender, something like.

ko.extenders.numeric = function(observable, options) {
    var textValue = null;
    return ko.computed({
        write: function(value) {
            textValue = value;
            var parsed = Number.parseFloat(value);
            if(!isNaN(parsed)) {
                observable(parsed);
            } else {
                observable(null);
            }
        },
        read: function() {
            var value = observable();
            if(value == null) {
                return textValue;
            }

            return value.toLocaleString(); //This  can be replaced with for example the Globalize plugin 
        }
    });
};

Use it like

this.amount = ko.observable().extend({ numeric: true });

The problem with this approach is that the ViewModel will no longer work against numbers it will work against formatted strings, so this.amount() will return a string instead of a number. This is very bad practice in a View/ViewModel seperation stand point.

In knockout 3.x the team introduced something called preprocessors, this means that you can alter the binding for a bindingHandler just before it will trigger.
More info here http://knockoutjs.com/documentation/binding-preprocessing.html

We can use this to add some logic to the value and text binding, in this case we want to check the observable for a underlying toView computed observable. The binding in clear text would look like this data-bind=”value: myValue.toView || myValue”. This will make sure that KO uses the toView observable if present. To make this work with a preprocessor we do

ko.bindingHandlers.value.preprocess = function(value) {
    return "(" + value + ").toView || " + value;
};

The function is passed the expression of the user defined binding attribute, it can contain text like a observable name “myValue”, but it could also contain inline expressions like “myValue() + 1”. By wrapping the value variable in parenthesis we make sure the code work with inline expressions (It will work in the meaning it wont crash but it cant add a toView computed toa expression so formatting will be disabled).

Now that the value binding supports toView observables we need to implement that in the extender.

ko.extenders.numeric = function(observable, options) {
    var textValue = null;
    observable.toView = ko.computed({
        write: function(value) {
            textValue = value;
            var parsed = Number.parseFloat(value);
            if(!isNaN(parsed)) {
                observable(parsed);
            } else {
                observable(null);
            }
        },
        read: function() {
            var value = observable();
            if(value == null) {
                return textValue;
            }

            return value.toLocaleString(); //This  can be replaced with for example the Globalize plugin 
        }
    });

    return observable;
};

Only difference is that we add the computed as a member on the original observable instead of returning the new computed.
Full code.

var toViewBindingHandlers = {
    value: true,
    text: true
};
var toViewPreprocessor = function(value) {
    return "(" + value + ").toView || " + value;
};

for(var bindingHandler in toViewBindingHandlers) {
    ko.bindingHandlers[bindingHandler].preprocess = toViewPreprocessor;
}

ko.extenders.numeric = function(observable, options) {
    var textValue = null;
    observable.toView = ko.computed({
        write: function(value) {
            textValue = value;
            var parsed = Number.parseFloat(value);
            if(!isNaN(parsed)) {
                observable(parsed);
            } else {
                observable(null);
            }
        },
        read: function() {
            var value = observable();
            if(value == null) {
                return textValue;
            }

            return value.toLocaleString(); //This  can be replaced with for example the Globalize plugin 
        }
    });

    return observable;
};

ko.applyBindings({ val: ko.observable().extend({ numeric: true }) });

Fiddle: http://jsfiddle.net/894f0s0o/

Advertisements

5 comments

  1. Hi Anders!

    Thank you for this post, I’m currently using this in conjunction with DataTables.net and Numeral.JS and it’s really useful.

    Our testers found an error when formating a value of type Integer.
    When they entered for example 100,23 it would format the value to 100 but if they changed the value again to 100,23 it would not format anything.

    This should be a problem whenever the value is not considered to have been changed by KO.

    I solved it by having observable.notifySubscribers(observable()) inside the write like this:

            observable.toView = ko.computed({
                write: function (value) {
                    textValue = value;
                    var isNumeric = $.isNumeric(value.toString().replace(",", ".").replace(" ", ""));
                    if (isNumeric) {
                        observable(value);
                    } else {
                        observable(textValue);
                    }
                    observable.notifySubscribers(observable());   //This line 
                },
    
    

    But maybe you have a different/better approach for solving this issue or perhaps there is an error with my approach?

    Thanks!

    /Rami

    1. Hi Rami, glad you liked this post!
      Yes, I noticed this behavior in my code too, you can test it by writing a none numeric value like AA, notice that the tex value will be updated. Now write AAA and the textvalue wont be updated, in my case this is because in both cases the underlying observable will get the value null and thus not notify any listeners.

      This can be fixed by adding observable.notifySubscribers after the observable is set to null like.

                  if(!isNaN(parsed)) {
                      observable(parsed);
                  } else {
                      observable(null);
                      observable.notifySubscribers():
                  }
      

      This will for some cases notify listeners twice (When going from a none null value to a null value). This can be fixed by some additional logic, look here
      http://jsfiddle.net/894f0s0o/2/

      Also as a side note, you are missing the whole point with this exercise, your code seems to be setting the text value to the underlying observable? The whole purpose is to separate the View (formated string) and the ViewModel (number).

      Hope it helps, thanks, Anders

  2. Hi Anders

    Thanks for the quick response and the explanation.
    This works perfectly, I’l keep my notifySubscribers outside the if(!NaN) condition but will remove the newValue from it.

    I have tweaked this exercise to fit my needs a little, we needed to seperate the View(formated string) and ViewModel(unformated string) that is why I’m setting the textValue instead of null.

    Thanks!

    /Rami

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s