Search code examples
knockout.jsknockout-validation

firing custom extender of one observable when another computed observable is changed


I am trying to create my own validation without using the knockout validation library. Am trying to create a common Validate extender that can do all type of validations i want it to do. Am doing this by passing type of validation and the required flag in an object to the extender. The problem am having is that the validate method only fires when the Password field is changed and not when the PasswordVisible property is changed. This is causing problem when the Password is already empty and when the PasswordVisible property is changed, the attempt to empty Password is not considered as change and hence not firing the extender.

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <script type="text/javascript" src="knockout-3.4.0.js"></script>

    Name:<input type="text" data-bind="value:Name" /><br />
    Already A User: <input type="checkbox" data-bind="checked:AlreadyUser" /><br />
    New Password:<input type="password" data-bind="value:Password,visible:PasswordVisible" /><br />
    <input type="button" value="Submit" onclick="validateModel();" />

    <script type="text/javascript" >
        var pageModel;

        ko.extenders.Validate = function (target, validateOptions) {
            target.HasErrors = ko.observable(false);
            var required = validateOptions.required();
            var validationType = validateOptions.validationType;
            function validate(newValue) {
                alert('validating');
                if (required) {
                    switch (validationType) {
                        case "Text":
                            target.HasErrors(newValue == "" ? false : true);
                            break;
                        default:
                            target.HasErrors(false);
                            break;
                    }
                }
            }

            validate(target());
            target.subscribe(validate);
            return target;
        };

        //The model itself
        var ViewModel = function () {            
            var self = this;
            self.Name = ko.observable('');
            self.AlreadyUser = ko.observable(false);
            //computed variable that sets the visibility of the password field. I have to clear the password when am making it invisible
            self.PasswordVisible = ko.computed(function () { return !this.AlreadyUser(); }, this).extend({ notify: 'always' });
            //this field is only required when visible
            self.Password = ko.observable('').extend({ Validate: { required: function () { return self.PasswordVisible() }, validationType: "Text" } });
            self.PasswordVisible.subscribe(function (newVal) { self.Password(''); });
            self.HasErrors = ko.computed(function () { return self.Password.HasErrors(); },self);
        };



        //The method calls on click of button
        function validateModel() {
            alert(pageModel.HasErrors());
            }

        //create new instance of model and bind to the page
        window.onload = function () {          
            pageModel = new ViewModel();
            ko.applyBindings(pageModel);
        };

    </script>
</body>
</html>

How to fire the validate when the PasswordVisible is changed as well.


Solution

  • You could make HasErrors a ko.computed to automatically create subscriptions to any observable used. It might trigger some unneeded re-evaluations though...

    ko.extenders.Validate = function(target, validateOptions) {
      target.HasErrors = ko.computed(function() {
        // Create subscription to newValue
        var newValue = target();
    
        // Create subscriptions to any obs. used in required
        var required = validateOptions.required();
        var validationType = validateOptions.validationType;
    
        if (ko.unwrap(required)) {
          switch (validationType) {
            case "Text":
              return newValue == "";
          }
        };
    
    
        return false;
      }, null, {
        deferEvaluation: true
      });
    
      return target;
    };
    

    Note that you also do not need to wrap the PasswordVisible observable in a function to execute it; you can use ko.unwrap instead.

    Here's my approach in your code. You might want to take another look at the multiple validations once you hide the password when there's a value inside (the clear via self.Password('') triggers another validation).

    var pageModel;
    var i = 0;
    ko.extenders.Validate = function(target, validateOptions) {
      target.HasErrors = ko.computed(function() {
        console.log("validating " + ++i);
    
        // Create subscription to newValue
        var newValue = target();
    
        // Create subscriptions to any obs. used in required
        var required = validateOptions.required();
        var validationType = validateOptions.validationType;
    
        if (ko.unwrap(required)) {
          switch (validationType) {
            case "Text":
              return newValue == "";
          }
        };
    
    
        return false;
      }, null, {
        deferEvaluation: true
      });
    
      return target;
    };
    
    //The model itself
    var ViewModel = function() {
      var self = this;
      self.Name = ko.observable('');
      self.AlreadyUser = ko.observable(false);
      //computed variable that sets the visibility of the password field. I have to clear the password when am making it invisible
      self.PasswordVisible = ko.computed(function() {
        return !this.AlreadyUser();
      }, this).extend({
        notify: 'always'
      });
      //this field is only required when visible
      self.Password = ko.observable('').extend({
        Validate: {
          required: function() {
            return self.PasswordVisible()
          },
          validationType: "Text"
        }
      });
      self.PasswordVisible.subscribe(function(newVal) {
        self.Password('');
      });
      self.HasErrors = ko.computed(function() {
        return self.Password.HasErrors();
      }, self);
    };
    
    
    
    //The method calls on click of button
    function validateModel() {
      console.log(pageModel.HasErrors());
    }
    
    //create new instance of model and bind to the page
    window.onload = function() {
      pageModel = new ViewModel();
      ko.applyBindings(pageModel);
    };
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
    
    Name:
    <input type="text" data-bind="value:Name" />
    <br />Already A User:
    <input type="checkbox" data-bind="checked:AlreadyUser" />
    <br />New Password:
    <input type="password" data-bind="value:Password,visible:PasswordVisible" />
    <br />
    <input type="button" value="Submit" onclick="validateModel();" />