Commits

Matthew Schinckel committed f8d151f

Initial import.

Comments (0)

Files changed (1)

src/knockout.validators.js

+(function(undefined){
+  
+  (function(){
+    // We want to override the value bindingHandler, so we can inject html5 attributes.
+    // We subscribe to the value's html5attrs attribute, so we can update automatically.
+
+    var init = ko.bindingHandlers.value.init;
+    ko.bindingHandlers.value.init = function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+      init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
+      var value = valueAccessor();
+      var $element = $(element);
+      
+      // Subscribe to the list of html5attrs we want to apply to this element.
+      if (value.html5attrs) {
+        var lastAttrs = value.html5attrs();
+        var updateAttrs = function(data) {
+          ko.utils.arrayForEach(data, function(x){
+            element[x.attr] = x.value;
+          });
+          ko.utils.arrayForEach(lastAttrs, function(x){
+            if (!ko.utils.arrayFirst(data, function(y){
+              return y.attr === x.attr;
+            })) {
+              element[x.attr] = undefined;
+            }
+          });
+          lastAttrs = data;
+        };
+        value.html5attrs.subscribe(updateAttrs);
+        updateAttrs(value.html5attrs());
+      }
+      
+      if (value.errors) {
+        var $validationMessageElement = $("<label class='help-inline'></label>");
+        var $targets = $validationMessageElement.add('[for="' + element.id + '"]').add(element);
+        $validationMessageElement.insertAfter($element);
+        // Append/remove the validation message
+        var validationMessage = function(message) {
+          $validationMessageElement.html(message.join("<br>"));
+          // Also apply the error class to relevant elements.
+          // Note that this is not sufficient for Bootstrap: you need to
+          // have a class on 'error' on a parent div.
+          if ($('link[href*=bootstrap][rel=stylesheet]').length) {
+            $targets = $targets.add($targets.parent());
+          }
+          if (message.length) {
+            $targets.addClass('error');
+          } else {
+            $targets.removeClass('error');
+          }
+        };
+        value.errors.subscribe(validationMessage);
+        validationMessage(value.errors());
+      }
+
+    };
+    
+    // We also want to create a bindingHandler for displaying validation classes.
+    ko.bindingHandlers.validationElement = {
+      update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
+        var value = valueAccessor();
+        if (value.hasErrors && value.hasErrors()){
+          $(element).addClass('error');
+        } else {
+          $(element).removeClass('error');
+        }
+      }
+    };
+    
+    // We can also have a bindingHandler that all fields within a viewModel are valid.
+    ko.bindingHandlers.requireValid = {
+      update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
+        var $element = $(element);
+        var errorFields = [];
+        $.each(viewModel, function(attr, value) {
+          if (value.hasErrors && value.hasErrors()) {
+            errorFields.push(attr);
+          }
+        });
+        // If we are attached to a form, make any submit elements disabled.
+        // Otherwise, make this element disabled.
+        if (element.localName === "form") {
+          $element = $element.find('[type=submit]');
+        }
+        $element.attr('disabled', errorFields.length > 0);
+      }
+    };
+    
+  })();
+    
+  ko.subscribable.fn.validate = function(validators) {
+    var validatorList = ko.observableArray([]);
+  
+    var errors = ko.computed(function(){
+      var value = ko.utils.unwrapObservable(this);
+      return ko.utils.arrayFilter(validatorList(), function(condition){
+          return condition.test(value);
+        });
+    }, this);
+    
+    this.hasErrors = ko.computed(function(){ return errors().length > 0; });
+    // Does this object have an error of this type?
+        
+    var errorsObject = ko.computed(function(){
+      var errorData = {};
+      ko.utils.arrayForEach(errors(), function(error){
+        errorData[error.name] = error.message;
+      });
+      return errorData;
+    });
+  
+    this.hasError = function(name) {
+      return errorsObject()[name];
+    };
+    
+    this.errors = ko.computed(function() {
+      var value = ko.utils.unwrapObservable(this);
+      return ko.utils.arrayMap(errors(), function(condition){
+        if (typeof condition.message === "function") {
+          return condition.message(value);
+        }
+        return condition.message;
+      });
+    }, this);
+  
+    var defaultValidators = ko.subscribable.fn.validate.defaultValidators;
+    var validatorNames = ko.observableArray(["required"]);
+    var existingValidator = function(name) {
+      return ko.utils.arrayFirst(validatorList(), function(x) {
+        return x.name === name;
+      });
+    };
+    /*
+    We can apply a list of validators that contains objects of the forms:
+    
+    {
+        required: true,
+        unique: function(obj) { 
+          // function for testing if obj passes validation 
+        },
+        numeric: "Message for failed validation",
+        time: false,
+        
+        date: {
+          test: function(obj) {
+            // function for testing
+          },
+          message: "Message for failed validation",
+          html5attr: {attr: "", value: ""}
+        }
+    }
+    
+    Note that the first three forms will only work if the validator you are
+    attempting to apply is a known validator (ie, is in the list of defaultValidators,
+    or has been applied previously).
+    
+    With these forms, you can essentially update one attribute on the validator,
+    or remove it (by passing false). If you pass in a string, it is assumed that you
+    are changing the message, a function means you are changing the test. You may not
+    set or change the html5attr in this manner.
+    
+    Otherwise, or if you want to apply more than one change, you can use the full
+    form, which requires a test and a message, and allows for an html5 attribute.
+    Using this form, you may also pass in a function to the message, which will be
+    called with the data that failed validation.
+    
+    */
+    function applyValidators(options) {
+      $.each(options, function(name, data){
+        var found = defaultValidators[name] || existingValidator(name) || {};
+        var test = data.test || (typeof data === "function" && data) || found.test;
+        var message = data.message || (typeof data === "string" && data) || found.message;
+        var html5attr = data.html5attr || found.html5attr;
+        
+        validatorList.remove(function(item){ return item.name === name;});
+        if (data) {
+          validatorList.push({
+            name: name,
+            test: test,
+            message: message,
+            html5attr: html5attr
+          });
+        }
+      });
+    }
+    
+    applyValidators(validators);
+    
+    this.validators = ko.computed(function(){
+      return ko.utils.arrayMap(validatorList(), function(v){
+        return v.name;
+      });
+    });
+    
+    this.html5attrs = ko.computed(function(){
+      return ko.utils.arrayFilter(
+        ko.utils.arrayMap(validatorList(), function(v){
+          return v.html5attr;
+        }), function(h) {
+          return !!h;
+        });
+    });
+    
+    // Allow us to dynamically change the validators on a field.
+    // This can also be used to dynamically change a validation message.
+    this.validate = applyValidators;
+    
+    return this;
+  };
+
+  ko.subscribable.fn.validate.defaultValidators = {
+    required: {
+      message: "This value is required",
+      test: function(value){ return !value && value !== 0;},
+      html5attr: {attr:'required', value:true}
+    }
+  };
+  
+})();