Search code examples
javascriptjqueryknockout.jscustom-bindingko-custom-binding

show underlying value of input on focus knockout custom binding


I have created (please excuse the mess - just hacking it together at the moment!) the following binding handler in knockout

The desired functionality is that if a field is numeric then it will be formatted to 'X' decimal places (default 2) when displaying but the underlying value is whatever is entered.

All works fine, but I want to add the functionality that when an input is focused then it shows the actual value and I just have no idea how to do this.

i.e.

  1. user enters 1.1121 into an input
  2. when they leave the input it formats to 1.11
  3. if they go back into the input (focus) then it displays 1.1121 for editing

It is point 3 that I have no idea how to achieve as at the moment it shows 1.11 and then over-writes on blur??

Can anyone point me in the right direction - basically where do I access the underlying value on focus and replace the displayed text with the underlying value??

for brevity I have removed some other 'decoration' code that wraps the inputs as they are not relelvant.

Thanks in advance.

    ko.bindingHandlers.specialInput = {
    init: function (element, valueAccessor, allBindingsAccessor) {

        var value = valueAccessor();

        var decimals = allBindingsAccessor().decimals || 2;
        var formatString = "";

        var interceptor = ko.computed({
            read: function () {
                if(isNumeric(ko.unwrap(value))){

                    //to do if time - replace this with a function that will accept any number of decimals
                    if(decimals == 0){
            formatString = "0,0";
        }
                    if(decimals == 1){
            formatString = "0,0.0";
        }
                    if(decimals == 2){
            formatString = "0,0.00";
        }
                    if(decimals == 3){
            formatString = "0,0.000";
        }
                    if(decimals == 4){
            formatString = "0,0.0000";
        }
                    if(decimals == 5){
            formatString = "0,0.00000";
        }


                return numeral(ko.unwrap(value)).format(formatString);
                }else{
                    return ko.unwrap(value);
                }
            },
            write: function (newValue) {
                if ($.trim(newValue) == '')
                    value("");
                else
                   if(isNumeric(newValue)){ 
                    value(numeral().unformat(newValue));
                value.valueHasMutated();
                   }else{
                    value(newValue);
                    value.valueHasMutated();
                   }
            }
        }).extend({notify: 'always'});




        if (element.tagName.toLowerCase() == 'input') {
            ko.applyBindingsToNode(element, {
                value: interceptor
            });
        } else {
            ko.applyBindingsToNode(element, {
                text: interceptor
            });
        }


    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {

        var value = ko.unwrap(valueAccessor());

        return ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);

    }
};

function isNumeric(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
}

Solution

  • I wouldn't implement this with an interceptor computed.

    I'd rather do the following:

    In the init register event handlers for focus and blur in the init:

    • on the focus handler, you have to show the underlying value in the input
    • on the blur handler, you have to store the number in the textbox, and show the rounded value in the input

    In the update you have to store the real value, and show it in the textbox. Most probably the textbox won't have the focus when you update the value, so you should be safe showing the rounded value. However, if you think that in some cases it can be focused, you can do something like this to test it to decide how to show the value: Using jQuery to test if an input has focus

    pseudocode

    ko.bindingHandlers.yourBindingName = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      var $element = $(element);
    
      $element.on('focus', function() {
        // Show raw value:
        $element.val(/* raw value */);
      });
    
      $element.on('blur', function() {
        // Update the observable with the value in the input
        ko.unwrap(valueAccessor())( /* get raw value from input */);
        // Show the rounded value
        $element.val(/* rounded value */);
      });
    
      // Update will be called after init when the binding is first applied,
      // so you don't have to worry about what it's initially shown
    },
    
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // When the value changes, show it in the input
        $.element.val(/* if focused show raw, if not, show roundede*/);
    }
    

    };

    If you are sure that you are only going to use it with input and writable observables, this code is safe. If you have doubts about it, you should add many checks like cheking the kind of element to use jQuery .val() or .text(), check if the binding expression is a writeable observable to update its value, and so on.

    NOTE: thereis something which is overseen many times: disposing of objects that we no longer need when the element is destroyed (that can happen for example with 'if', 'whit' or 'template'). In this case I think you don't need to do that, but, please, see this: Custom disposal logic if you think you need to destroy something.