0

This is a question to try and resolve a UI caching issue with Knockout mentioned as an aside in this answer.

Take the below binding (based on the previous answer):

ko.bindingHandlers.amountValue = {
  init: function (element, valueAccessor, allBindingsAccessor) {
    var underlyingObservable = valueAccessor();

    var interceptor = ko.computed({
      read: function () {
        return "$" + underlyingObservable();
      },

      write: function (newValue) {
        var current = underlyingObservable(),
            valueToWrite = Math.round(parseFloat(newValue.replace("$", "")) * 100) / 100;

        if (valueToWrite === current) {
          // Clear the observable's value to force a UI refresh
          underlyingObservable(null);
        }

        underlyingObservable(valueToWrite);
      }
    });

    ko.applyBindingsToNode(element, { value: interceptor });
  }
};

The write function strips the dollar sign, converts it from a string to a number, and rounds the amount to 2 decimal places, storing the result in the observable.

However, if the user is entering a value which, after parsing/conversion, results in the same value as that currently stored in the underlying observable then there is a problem. Essentially, Knockout decides that as the value has not changed, therefore the UI need not change. The UI and the underlying observable are out of sync. THIS IS BAD.

Example: current underlying Amount 1 value is 22.45. User enters "$22.45111". On update the Amount 1 value is set as 22.45. However the on-screen field value is left as "$22.45111". i.e. It is out of sync with the underlying Amount 1 value.

I have a workaround for this issue which is also implemented in the above binding:

        if (valueToWrite === current) {
          // Clear the observable's value to force a UI refresh
          underlyingObservable(null);
        }

This checks if the valueToWrite to the observable is the same as that currently stored in the observable and, if it is, clears that value out (by setting the observable to null). Then it subsequently sets the observable back to that old / new value which in turn refreshes the UI.

This works but it feels wrong. Surely there must be a better way? I'm assuming that I've overlooked something but I don't know what.

I don't believe that valueHasMutated / valueWillMutate is the answer. Is there a forceUIRefresh method hiding out in Knockout that I haven't spotted?

This is illustrated in this jsfiddle

Community
  • 1
  • 1
John Reilly
  • 5,791
  • 5
  • 38
  • 63

1 Answers1

1

There has been a change between Knockout 2.x and 3.x that affects the way computed notify its dependencies. You need to use the notify extender as the result of the read function has not changed.

Your "if" condition in the else clause of the write function is also not required.

var interceptor = ko.computed({
  read: function () {
    // this function does get called, but it's return value is not used as the value of the textbox.
    // the raw value from the underlyingObservable, or the actual value the user entered is used instead, no   
    // dollar sign added. It seems like this read function is completely useless, and isn't used at all
    return "$" + underlyingObservable();
  },

  write: function (newValue) {
    var current = underlyingObservable(),
        valueToWrite = Math.round(parseFloat(newValue.replace("$", "")) * 100) / 100;

    if (valueToWrite !== current) {
      // for some reason, if a user enters 20.00000 for example, the value written to the observable
      // is 20, but the original value they entered (20.00000) is still shown in the text box.
      underlyingObservable(valueToWrite);
    } else {
      underlyingObservable.valueHasMutated();
    }
  }
}).extend( {notify: 'always'} );
Robert Slaney
  • 3,712
  • 1
  • 21
  • 25