1
votes

I'm using knockout and the mapping plugin to automatically create my view model. I have a bunch of amounts in my view model that I bind to textboxes. When a user changes an amount in a textbox, all I want is to make sure what they entered is a number, and that it's greater than 0, and if it isn't I want to replace what they entered with 0. This seems like it should be SO simple... with a custom binding or subscribe function.

Everything I'm reading about knockout validation talks about extenders and read/write computed observables, or adding yet another plugin (such as jquery validation). They all seem like overkill for the situation, and the extenders/computed observables have to be explicitly declared for each observable you want to validate. I have a lot of amounts that are automatically created using the mapping plugin, so this doesn't seem reasonable.

Any help would be greatly appreciated!

2

2 Answers

7
votes

For your specific scenario, one way to handle this is by creating a custom binding that is able to intercept the value and do your validation. This can be done by creating a writeable computed in the custom binding to bind against. The advantage is that you don't have to worry about the mapping plugin customizing your object creation.

It might look something like:

ko.bindingHandlers.positiveNumericValue = {
    init : function(element, valueAccessor, allBindingsAccessor) {
        var underlyingObservable = valueAccessor();
        var interceptor = ko.computed({
            read: underlyingObservable,
            write: function(newValue) {
                var current = underlyingObservable(),
                    valueToWrite = isNaN(newValue) ? 0 : parseFloat(+newValue);

                if (valueToWrite < 0) {
                   valueToWrite = 0;   
                }

                //only write if it changed
                if (valueToWrite !== current) {
                    underlyingObservable(valueToWrite);
                } else {
                    //if the rounded value is the same as it was, but a different value was written, force a notification so the current field is updated to the rounded value
                    if (newValue !== current) {
                        underlyingObservable.valueHasMutated();
                    }
                }   
            } 
        });
        ko.bindingHandlers.value.init(element, function() { return interceptor }, allBindingsAccessor);
    },  
    update : ko.bindingHandlers.value.update
};

Here is a sample: http://jsfiddle.net/rniemeyer/2TnSM/

Another way would be to extend observables with an option to create the writeable computed.

For your scenario, it might look like:

ko.observable.fn.forcePositive = function() {
    var underlyingObservable = this;
    if (!this.forcePositiveInterceptor) {
         this.forcePositiveInterceptor = ko.computed({
            read: this,
            write: function(newValue) {
                var current = underlyingObservable(),
                    valueToWrite = isNaN(newValue) ? 0 : parseFloat(+newValue);

                if (valueToWrite < 0) {
                   valueToWrite = 0;   
                }

                //only write if it changed
                if (valueToWrite !== current) {
                    underlyingObservable(valueToWrite);
                } else {
                    //if the rounded value is the same as it was, but a different value was written, force a notification so the current field is updated to the rounded value
                    if (newValue !== current) {
                        underlyingObservable.valueHasMutated();
                    }
                }   
            } 
        });
    }            

    return this.forcePositiveInterceptor;        
};

You would then bind against it like:

<input type="text" name="age" data-bind="value: age.forcePositive()" />

The way that I implemented it here, you would need to call is as a function forcePositive() so the writeable is initialized. This is so that you can just use the mapping plugin without any customization and just do this on any observable that you want to use this functionality with.

Sample: http://jsfiddle.net/rniemeyer/Dy4MH/

I think that either choice would work for your scenario. I probably favor the second choice, so that you could add more of these while using the normal bindings.

1
votes

A quick and small one

http://jsfiddle.net/gxfup/1/

edit:

I dont like to let the mapping plugin define my viewmodels, I use a prototype view model (Like in the example above, and then let the mapper plugin map on that).

ViewModel = function (data) {
    this.number = ko.observable().extend({ number: true });
    return ko.mapping.fromJS(data, {}, this);
};