Search code examples
knockout.jsknockout-validation

Knockout validation valueUpdate depending on attribute error


Is is possible to change the attribute valueUpdate from "onblur" to "keyup" depending on if the attribute has an error attached?

I want to mimick the way validation is done in jQuery Validation, and first do validation on blur, and afterwards do validation on keyup.

Is this possible?

EDIT: Let me just clarify and give an example. I do not mind that binding to the model occurs on "keyup", what I do mind is that the user is shown an error message, before even given the chance to finish typing. Instead, if I take example in validating an email address. If the user types in an invalid email I would like the error to show on blur, and if the user puts focus on the field again to correct the error I would like the error to disappear once the error is corrected. On the other hand, if the user types in a valid email to begin with, and later introduces an error, the error should show immediately.

SECOND EDIT: So I've given it some thought, and I believe that the validation shouldn't interfere with model binding, instead the changes should be made to the displaying of error messages. As stated, I would like the error to appear immediately after the error occurs, but only after a change event happened on the relevant field.

I made this fiddle that almost works, but it should show exactly what I'm trying to accomplish.

http://jsfiddle.net/mntm1bne/3/

<div data-bind="validationOptions: {messageTemplate: 'myCustomTemplate'}">    
    <input data-bind="value: firstName, valueUpdate: 'keyup', event: {change: firstName.enableD}" />
    <br />
    <input data-bind="value: lastName" />

    <div data-bind="if: firstName.isD">
        Firstname is dirty!
    </div>

    <pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
    <div data-bind="text: ko.toJSON($data)"></div>
</div>
<script type="text/html" id="myCustomTemplate">
    <span data-bind="visible: field.isD && !field.isValid(), attr: { title: field.error }">X</span>
</script>

ko.extenders.trackChange = function(target, track) {
  if (track) {
    target.isD = ko.observable(false);
    target.enableD = function() {
        console.log("enable!");
      target.isD(true);
    }
  }
  return target;
};

var ViewModel = function () {
    var self = this;
    self.firstName = ko.observable().extend({ trackChange: true, required: { message: "firstName" }, number: true,
    min: 0,
    max: 100
    });
    self.lastName = ko.observable().extend({ required: { message: "lastName" }});  
}

var viewModel = new ViewModel();

ko.applyBindings(viewModel);

Specifically, the error lies in the first validation message, that is shown on page load.


Solution

  • To do it properly, you'd rewrite the valueUpdate binding so that it takes an observable, and your observable would be based on whether the value has an error.

    That sounds hard, so I went with something a little hackier. I substituted an event binding for valueUpdate. It fires on keydown and if the validated variable is not valid, updates its value from the input (using event.target). The value will always be updated on blur, so I didn't have to handle that.

    var viewModel = {
      num1: ko.observable(50).extend({
        number: true,
        min: 0,
        max: 100
      }),
      maybeEvaluate: function(data, event) {
        setTimeout(function() {
          if (!viewModel.num1.isValid()) {
            viewModel.num1(event.target.value);
          }
        }, 0);
        return true;
      }
    
    };
    
    ko.applyBindings(viewModel);
    .validationMessage {
      color: Red;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js"></script>
    0-100:
    <input data-bind="value: num1, event: {keydown: maybeEvaluate}" />
    <span class="validationMessage" data-bind="validationMessage: num1"></span>
    <br />
    <input />

    Here's another approach, based on protected observables. The original design of protected observables was to give you the option to commit or reset changes. I have an event force a commit on blur, and the protected is modified to auto-commit on receiving a new value if the current value is invalid. (I didn't need the reset routine, so I took that out, so be aware the protectedObservable definition is customized.)

    Update Based on your comment (and also on the fact that I hadn't included the required libs), I've updated the second example so that if the field has ever had an error, it becomes continuously-validating.

    ko.protectedObservable = function(initialValue) {
      //private variables
      var _actualValue = ko.observable(initialValue),
        _tempValue = initialValue,
        hasHadError = false;
    
      //computed observable that we will return
      var result = ko.computed({
        //always return the actual value
        read: function() {
          return _actualValue();
        },
        //stored in a temporary spot until commit
        write: function(newValue) {
          _tempValue = newValue;
          if (!result.isValid()) {
            hasHadError = true;
          }
          if (hasHadError) {
            result.commit();
          }
        }
      }).extend({
        notify: "always"
      });
    
      //if different, commit temp value
      result.commit = function() {
        if (_tempValue !== _actualValue()) {
          _actualValue(_tempValue);
        }
      };
    
      return result;
    };
    
    var viewModel = {
      num1: ko.protectedObservable(50).extend({
        number: true,
        min: 0,
        max: 100
      })
    };
    
    ko.applyBindings(viewModel);
    .validationMessage {
      color: Red;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js"></script>
    0-100:
    <input data-bind="value: num1, valueUpdate: 'input', event: {blur: num1.commit}" />
    <span class="validationMessage" data-bind="validationMessage: num1"></span>
    <br />
    <input />