wired_in
wired_in

Reputation: 2613

Knockout JS: Working with old validation code that changes the DOM

See edit at the bottom.

My company has a huge code base and we want to start using knockout more effectively. However, we have validation code in place already that takes care of all aspects of client-side validation. It uses jQuery to show validation error messages and to sanitize user input.

For example, if I add the class "validate-range" to an input, it will use jQuery change/focusout events to track changes and then if a value is out of the range, it will replace it with the min/max value using $(input).val(). Since this validation code makes changes this way programmatically, my knockout view model won't be updated when these kind of changes are made.

This validation code is used everywhere in the system, and can't be replaced at the moment, so in order to use knockout, I have to make it work along side this code. What i've tried so far is creating a custom value binding that adds an additional change event handler which is used to update the view model whenever the validation code changes an input's value.

This works surprisingly well in all cases except inside a foreach binding (which is the same as using the template/with binding I would imagine). My change event handler isn't being fired on any inputs inside the foreach that use the custom value binding, even though the custom binding is being reapplied to all inputs inside the foreach every time the observable array changes.

I was hoping someone has dealt with this problem before, having to make knockout work with existing javascript code that changes DOM values, and thus doesn't update the view model. Any help is greatly appreciated.

Javascript code for custom binding, creating view model, and old validation code:

// custom value binding for amounts
ko.bindingHandlers.amountValue = {
  init: function (element, valueAccessor) {
    var underlyingObservable = valueAccessor(),

        interceptor = ko.computed({
          read: function () {
            var value = underlyingObservable();
            return formatAmount(value);
          },

          write: function (newValue) {
            var current = underlyingObservable(),
                valueToWrite = parseAmount(newValue);

            if (valueToWrite !== current)
              underlyingObservable(valueToWrite);
            else if (newValue !== current.toString())
              underlyingObservable.valueHasMutated();
          }
        });

    // i apply a change event handler when applying the bindings which calls the write function of the interceptor.
    // the intention is to have the change handler be called anytime the old validation code changes an input box's value via 
    // $(input).val("new value"); In the case of the foreach binding, whenever the observable array changes, and the table rows
    // are re-rendered, this code does get ran when re-applying the bindings, however the change handler doesn't get called when values are changed.
    ko.applyBindingsToNode(element, { value: interceptor, event: { change: function () { interceptor($(element).val()); } } });
  }
};

// view model creation
// auto create ko view model from json sent from server
$(function () {
  viewModel = ko.mapping.fromJS(jsonModel);
  ko.applyBindings(viewModel);
});

// old validation code
$(document).on("focusout", ".validate-range", function () {
  var $element = $(this),
      val = $element.val(),
      min = $element.attr("data-val-range-min"),
      max = $element.attr("data-val-range-max");

  if (val < min)
    // my change handler from custom binding doesn't fire after this to update view model
    $element.val(min);

  if (val > max)
    // my change handler from custom binding doesn't fire after this to update view model
    $element.val(max);

  // more code to show error message
});

HTML code that uses the custom binding inside of a foreach binding:

<table>
  <thead>
    <tr>
      <td>Payment Amount</td>
    </tr>
  </thead>
  <tbody data-bind="foreach: Payments">
    <tr>
      <td><input type="text" class="validate-range" data-val-range-min="0" data-val-range-max="9999999" data-bind="amountValue: Amount" /></td>
    </tr>
  </tbody>
</table>

So in the above example, if I enter "-155" in an amount text box, my custom binding runs and sets the view model Amount to -155. Then the old validation runs and re-sets the value of the textbox to "0" with $(input).val(0). My view model doesn't get updated at this point, and still reflects the -155 value. My change event handler from the custom binding is supposed to be ran to update the view model to 0, but it doesn't.

Edit:

As pointed out in the answer, .val() does not trigger any change events. The change event handler I added didn't do anything. The reason the view model was being updated when the validation code changed a value outside of the foreach binding was because we had logic somewhere else in our javascript code that was manually triggering the change event using the blur event, which in turn triggered my custom binding to run and update the view model. This blur event handler was directly bound to the text boxes, instead of being delegated, so it worked for text boxes that were there when the page is first rendered, but not for the ones dynamically inserted by the foreach binding.

For now, I just changed this logic to delegate the events within the document, so it would include dynamically inserted text boxes, and it seems to be working fine. I'm hoping to come up with a better solution in the future.

Upvotes: 4

Views: 719

Answers (1)

RP Niemeyer
RP Niemeyer

Reputation: 114792

Calling $(element).val("some value"); does not trigger the change event.

You would need to do: $(element).val("some value").change();

Upvotes: 3

Related Questions