Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

knockout custom numeric binding

I wanted to use this technique: make an input only-numeric type on knockout

to allow user to enter only numbers.

However, this technique doesn't update observable value on UI.

HTML:

 <span data-bind="text: Interval" ></span>
 <input data-bind="numeric: Interval" />

Binding:

ko.bindingHandlers.numeric = {
    init: function (element, valueAccessor) {
        $(element).on("keydown", function (event) {
            // Allow: backspace, delete, tab, escape, and enter
            if (event.keyCode == 46 || event.keyCode == 8 || event.keyCode == 9 || event.keyCode == 27 || event.keyCode == 13 ||
                // Allow: Ctrl+A
                (event.keyCode == 65 && event.ctrlKey === true) ||
                // Allow: . ,
                (event.keyCode == 188 || event.keyCode == 190 || event.keyCode == 110) ||
                // Allow: home, end, left, right
                (event.keyCode >= 35 && event.keyCode <= 39)) {
                // let it happen, don't do anything
                return;
            }
            else {
                // Ensure that it is a number and stop the keypress
                if (event.shiftKey || (event.keyCode < 48 || event.keyCode > 57) && (event.keyCode < 96 || event.keyCode > 105)) {
                    event.preventDefault();
                }
            }
        });
    }    
};

So, binding doesn't allow to enter characters other than numbers, but when focus is lost on input, corresponding observable is not updating (so span elements is not changing).

NOTE:

I do not need to allow user to enter non numeric characters into input. I know there are other solution like ko numeric extension that converts everything into numerics, but I do not need this. I need a solution that allows to enter only digits (including something like backspace etc.).

like image 554
renathy Avatar asked Jan 23 '14 11:01

renathy


5 Answers

A solid route for numeric only numbers would be to user an extender.

We don't have to track the keypress. It is easier to just subscribe to the observable to intercept the value before it updates. We can then do some regex that allows us to evaluate whether the input is a number or not. If the input is not a number, we will strip out the non-numeric characters. Thus allowing no non-numeric input.

FIDDLE:

HTML

<input type="text" data-bind="value: myNum, valueUpdate: 'afterkeyup'" />

JS

(function(ko) {

    ko.observable.fn.numeric = function () {
        // the observable we are extending
        var target = this;

        // subscribe to the observable so we can
        // intercept the value and do our custom
        // processing. 
        this.subscribe(function() {
           var value = target();
           // this will strip out any non numeric characters
           target(value.replace(/[^0-9]+/g,'')); //[^0-9\.]/g - allows decimals
        }, this);

        return target;
    };

    function ViewModel() {
        this.myNum = ko.observable().numeric();
    };

    ko.applyBindings(new ViewModel());

})(ko);
like image 73
David East Avatar answered Nov 12 '22 04:11

David East


It is my fixed version considering all above but working as real value binding and supporting non-observable objects as source/target.

EDIT:Minified version of knockout does not expose writeValueToProperty function and twoWayBindings. So we should clone writeValueToProperty and use _twoWayBindings. I updated code to support minified version of knockout.

ko.expressionRewriting._twoWayBindings.numericValue = true;
ko.expressionRewriting.writeValueToProperty = function (property, allBindings, key, value, checkIfDifferent) {
    if (!property || !ko.isObservable(property)) {
        var propWriters = allBindings.get('_ko_property_writers');
        if (propWriters && propWriters[key])
            propWriters[key](value);
    } else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
        property(value);
    }
};
ko.bindingHandlers.numericValue = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        $(element).on("keydown", function (event) {
            // Allow: backspace, delete, tab, escape, and enter.
            if (event.keyCode == 46 || event.keyCode == 8 || event.keyCode == 9 || event.keyCode == 27 || event.keyCode == 13 ||
                // Allow: Ctrl+A
                (event.keyCode == 65 && event.ctrlKey === true) ||
                // Allow: . ,
                (event.keyCode == 188 || event.keyCode == 190 || event.keyCode == 110) ||
                // Allow: home, end, left, right.
                (event.keyCode >= 35 && event.keyCode <= 39)) {
                // Let it happen, don't do anything.
                return;
            }
            else {
                if (event.shiftKey || (event.keyCode < 48 || event.keyCode > 57) && (event.keyCode < 96 || event.keyCode > 105)) {
                    event.preventDefault();
                }
            }
        });

        var underlying = valueAccessor();
        var interceptor = ko.dependentObservable({
            read: function () {
                if (ko.isObservable(underlying) == false) {
                    return underlying;
                } else {
                    return underlying();
                }
            },
            write: function (value) {
                if (ko.isObservable(underlying) == false) {
                    if (!isNaN(value)) {
                        var parsed = parseFloat(value);
                        ko.expressionRewriting.writeValueToProperty(underlying, allBindingsAccessor, 'numericValue', !isNaN(parsed) ? parsed : null);
                    }
                } else {
                    if (!isNaN(value)) {
                        var parsed = parseFloat(value);
                        underlying(!isNaN(parsed) ? parsed : null);
                    }
                }
            }
        });
        ko.bindingHandlers.value.init(element, function () { return interceptor; }, allBindingsAccessor, viewModel, bindingContext);
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
    }
}
like image 31
Maxim Avatar answered Nov 12 '22 06:11

Maxim


I would suggest you make a wrapper around http://numeraljs.com/. You would just hook up the settings and on update you would call format on the input.

like image 37
Matthew Avatar answered Nov 12 '22 06:11

Matthew


This will do what you want:

<span data-bind="text: Interval" ></span>
<input data-bind="numeric, value: Interval" />

http://jsfiddle.net/mbest/n4z8Q/

like image 17
Michael Best Avatar answered Nov 12 '22 06:11

Michael Best


Indeed, this does not update your observable. The custom binding is incomplete. It seems to me this is just intended as an example of the idea, not a working solution.

However, in the question you linked, there's actually a better approach somewhere in the comments. It is to use a Knockout extender. See Live Example 1 on http://knockoutjs.com/documentation/extenders.html

There's a few reasons it's better: 1. More robust. For example, you could still paste a string of text from the clipboard in your solution. 2. More user-friendly. Your solution plainly disables a bunch of keys. This is not user friendly at all. The solution proposed by Knockout just ensures the ultimate value is a correct one. 3. Better code separation and maintainability: your HTML can just contain a plain ol' value binding. Once a requirement rises that the value should be numerical, you just extend the observable in your viewmodel. The only change you make is in the JavaScript, as it should be, since it's functionality, not presentation. The change also stands on itself, and it's very clear what the extender does to anyone that might be using the observable in calculations or w/e.

like image 1
Hans Roerdinkholder Avatar answered Nov 12 '22 05:11

Hans Roerdinkholder