renathy
renathy

Reputation: 5355

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.).

Upvotes: 11

Views: 9330

Answers (7)

Maxim
Maxim

Reputation: 13458

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);
    }
}

Upvotes: 2

elsadek
elsadek

Reputation: 1086

I need a solution that allows to enter only digits (including something like backspace etc.).

Check this jquery plugin: http://www.texotela.co.uk/code/jquery/numeric/

it allows decimal separator, you may want to fork it in order to allow other chars like backspace

Upvotes: 1

Maxim Nikonov
Maxim Nikonov

Reputation: 674

You can improve your binding handler to support modification of valueAccessor

Binding:

ko.bindingHandlers.numeric = {
    init: function (element, valueAccessor) {
        var value = 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();
                }
            }
        });

        $(element).change(function () {
            value($(element).val());
        });
    }    
};

In this case HTML will be

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

FIDDLE

Upvotes: 0

Michael Best
Michael Best

Reputation: 16688

This will do what you want:

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

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

Upvotes: 17

David East
David East

Reputation: 32604

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);

Upvotes: 8

Matthew
Matthew

Reputation: 411

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.

Upvotes: 2

Hans Roerdinkholder
Hans Roerdinkholder

Reputation: 3000

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.

Upvotes: 1

Related Questions