Skip to content

User Contributed Rules

Nick Evans edited this page Mar 4, 2019 · 47 revisions

Please feel free to contribute your own Custom Rules!

Example Rule

ko.validation.rules['exampleRule'] = {
    validator: function(val, otherVal){
        /* awesome logic */
    },
    message: 'Sorry Chief, {0} this is not Valid'
};

Notice it is also possible to have "anonymous" rules.

ko.observable(15).extend({
  validation: [{
    validator: function(value, params) {
      return parseInt(value, 10) === params;
    },
    message: function(params) {
      return 'Value must be equal to ' + params;
    },
    params: 1
  }]
})

Check that date is valid using momentjs (not minimum date / 0001-01-01)

ko.validation.rules["validateDate"] = {
  validator: function (val, validate) {
    var mindate = moment.utc(new Date("0001-01-01"));

    // Ensure the date is not mindate
    return moment.utc(val).isAfter(mindate);
  },
  message: "Please enter a valid date"
};

Requires one of more (in array)

/*
 * This rules checks the given array of objects/observables and returns 
 * true if at least one of the elements validates agains the the default
 * 'required' rules
 * 
 * Example:
 * 
 *
 * self.mobilePhone.extend({ requiresOneOf: [self.homePhone, self.mobilePhone] });
 * self.homePhone.extend({ requiresOneOf: [self.homePhone, self.mobilePhone] }); 
 *
*/

ko.validation.rules['requiresOneOf'] = {
  getValue: function (o) {
    return (typeof o === 'function' ? o() : o);
  },
  validator: function (val, fields) {
    var self = this;

    var anyOne = ko.utils.arrayFirst(fields, function (field) {
      var stringTrimRegEx = /^\s+|\s+$/g,
                testVal;

      var val = self.getValue(field);

      if (val === undefined || val === null) 
        return !required;
      
      testVal = val;
      if (typeof (val) == "string") {
        testVal = val.replace(stringTrimRegEx, '');
      }

      return ((testVal + '').length > 0);

    });

    return (anyOne != null);
  },
  message: 'One of these fields is required'

};

Change Limit Rule

/**
 * Limits the maximum amount a numeric value can be changed.
 * Parameters: maxChange: <The max valid value change from base value>,
 *             baseValueAccessor: <A function to access the base value>
 * 
 * Example: 'Distance can change a maximum of 10'
 * var initDistance = 5;
 * this.distance.extend({
 *     changeLimit:{
 *         maxChange:10,
 *         baseValueAccessor:function () {
 *             return initDistance;
 *         }
 *     }
 * });
 * 
 */
ko.validation.rules['changeLimit'] = {
    validator: function(val, options) {
        return Math.abs(val - options.baseValueAccessor()) <= options.maxChange;
    },
    message: 'Change limit exeeded'
};

Valid Object

/*
 * Aggregate validation of all the validated properties within an object
 * Parameter: true|false
 * Example:
 *
 * viewModel = {
 *    person: ko.observable({
 *       name: ko.observable().extend({ required: true }),
 *       age: ko.observable().extend({ min: 0, max: 120 })
 *    }.extend({ validObject: true })
 * }   
*/
ko.validation.rules["validObject"] = {
    validator: function (obj, bool) {
        if (!obj || typeof obj !== "object") {
            throw "[validObject] Parameter must be an object";
        }
        return bool === (ko.validation.group(obj)().length === 0);
    },
    message: "Every property of the object must validate to '{0}'"
};

Valid Array

/*
 * Aggregate validation of all the validated elements within an array
 * Parameter: true|false
 * Example
 *
 * viewModel = {
 *    person: ko.observableArray([{
 *       name: ko.observable().extend({ required: true }),
 *       age: ko.observable().extend({ min: 0, max: 120 })
 *    }, {
 *       name: ko.observable().extend({ required: true }),
 *       age: ko.observable().extend({ min:0, max:120 })
 *    }].extend({ validArray: true })
 * }   
*/
ko.validation.rules["validArray"] = {
    validator: function (arr, bool) {
        if (!arr || typeof arr !== "object" || !(arr instanceof Array)) {
            throw "[validArray] Parameter must be an array";
        }
        return bool === (arr.filter(function (element) {
            return ko.validation.group(ko.utils.unwrapObservable(element))().length !== 0;
        }).length === 0);
    },
    message: "Every element in the array must validate to '{0}'"
};

htmlNotEmpty

ko.validation.rules['htmlNotEmpty'] = {
    validator: function (val, otherVal) {
    
        function isBlank(str) {
            return (!str || !str.match(/\S/));
        }

        function isEmpty(str) {
            return (!str || 0 === str.length);
        }
        
        function isHtmlEmpty(str) {
            if (!str.match(/^\s*?\\</)) return false;
            var s = $(str).text();
            return (isEmpty(s) || isBlank(s));
        }
    
        var invalid = isEmpty(val);
        if (!invalid) invalid = isHtmlEmpty(val);

        return !invalid;
    },
    message: 'Invalid.  Please enter a value'
};

Nullable Integer

ko.validation.rules['nullableInt'] = {
    validator: function (val, validate) {
        return val === null || val === "" || (validate && /^-?\d*$/.test(val.toString()));
    },
    message: 'Must be empty or an integer value'
};

Nullable Decimal

ko.validation.rules['nullableDecimal'] = {
    validator: function (val, validate) {
        return val === null || val === "" || (validate && /^-?\d*(?:\.\d*)?$/.test(val.toString()));
    },
    message: 'Must be empty or a decimal value'
};

Conditional Required

/*
 * Determines if a field is required or not based on a function or value
 * Parameter: boolean function, or boolean value
 * Example
 *
 * viewModel = {
 *   var vm = this;
 *   vm.isRequired = ko.observable(false);
 *   vm.requiredField = ko.observable().extend({ conditional_required: vm.isRequired});
 * }   
*/
ko.validation.rules['conditional_required'] = {
    validator: function (val, condition) {
        var required = false;
        if (typeof condition == 'function') {
            required = condition();
        }
        else {
            required = condition;
        }

        if (required) {
            return !(val == undefined || val == null || val.length == 0);
        }
        else {
            return true;
        }
    },
    message: ko.validation.rules.required.message
}

Credit Card

//This rules checks the credit card details 
//The card number (inferred) as well as the card type (via the card type field) are required 
//This checks the length and starting digits of the card per the type
//It also checks the checksum (see http://en.wikipedia.org/wiki/Luhn_algorithm)
//The card type field must return 'vc' for visa, 'mc' for mastercard, 'ae' for amex
//This is based on code from here: http://www.rahulsingla.com/blog/2011/08/javascript-implementing-mod-10-validation-(luhn-formula)-for-credit-card-numbers
//Example:
//
//self.cardNumber.extend({ creditCard: self.cardType });
ko.validation.rules['creditCard'] = {
	getValue: function (o) {
		return (typeof o === 'function' ? o() : o);
	},
	validator: function (val, cardTypeField) {
		var self = this;

		var cctype = self.getValue(cardTypeField);
		if (!cctype) return false;
		cctype = cctype.toLowerCase();

		if (val.length < 15) {
			return (false);
		}
		var match = cctype.match(/[a-zA-Z]{2}/);
		if (!match) {
			return (false);
		}

		var number = val;
		match = number.match(/[^0-9]/);
		if (match) {
			return (false);
		}

		var fnMod10 = function (number) {
			var doubled = [];
			for (var i = number.length - 2; i >= 0; i = i - 2) {
				doubled.push(2 * number[i]);
			}
			var total = 0;
			for (var i = ((number.length % 2) == 0 ? 1 : 0) ; i < number.length; i = i + 2) {
				total += parseInt(number[i]);
			}
			for (var i = 0; i < doubled.length; i++) {
				var num = doubled[i];
				var digit;
				while (num != 0) {
					digit = num % 10;
					num = parseInt(num / 10);
					total += digit;
				}
			}

			if (total % 10 == 0) {
				return (true);
			} else {
				return (false);
			}
		}

		switch (cctype) {
			case 'vc':
			case 'mc':
			case 'ae':
				//Mod 10 check
				if (!fnMod10(number)) {
					return false;
				}
				break;
		}
		switch (cctype) {
			case 'vc':
				if (number[0] != '4' || (number.length != 13 && number.length != 16)) {
					return false;
				}
				break;
			case 'mc':
				if (number[0] != '5' || (number.length != 16)) {
					return false;
				}
				break;

			case 'ae':
				if (number[0] != '3' || (number.length != 15)) {
					return false;
				}
				break;

			default:
				return false;
			}

		return (true);
	},
	message: 'Card number not valid.'
};

Are Same

/*
 * Ensures a field has the same value as another field (E.g. "Confirm Password" same as "Password"
 * Parameter: otherField: the field to compare to
 * Example
 *
 * viewModel = {
 *   var vm = this;
 *   vm.password = ko.observable();
 *   vm.confirmPassword = ko.observable();
 * }   
 * viewModel.confirmPassword.extend( areSame: { params: viewModel.password, message: "Confirm password must match password" });
*/
ko.validation.rules['areSame'] = {
    getValue: function (o) {
        return (typeof o === 'function' ? o() : o);
    },
    validator: function (val, otherField) {
        return val === this.getValue(otherField);
    },
    message: 'The fields must have the same value'
};

Password Complexity

/*
 * Ensures a field matches a regex rule - in this case a password field has some complexity
*/
ko.validation.rules['passwordComplexity'] = {
    validator: function (val) {
        return /(?=^[^\s]{6,128}$)((?=.*?\d)(?=.*?[A-Z])(?=.*?[a-z])|(?=.*?\d)(?=.*?[^\w\d\s])(?=.*?[a-z])|(?=.*?[^\w\d\s])(?=.*?[A-Z])(?=.*?[a-z])|(?=.*?\d)(?=.*?[A-Z])(?=.*?[^\w\d\s]))^.*/.test('' + val + '');
    },
    message: 'Password must be between 6 and 128 characters long and contain three of the following 4 items: upper case letter, lower case letter, a symbol, a number'
};

ensure a property of all items is unique

// ensure a property of all items is unique
ko.validation.rules['arrayItemsPropertyValueUnique'] = {
    validator: function (array, arrayItemPropertyName) {
        if (!array || typeof array !== "object" || !(array instanceof Array)) {
            throw "[arrayItemsPropertyValueUnique] Parameter must be an array";
        }

        //console.log('arrayItemsPropertyValueUnique', array, arrayItemPropertyName);

        var values = [];

        for (var index = 0; index < array.length; index++) {
            var prop = array[index][arrayItemPropertyName];
            var value = prop();
            if (values.indexOf(value) != -1) {
                console.warn("The items in the array do not have a unique value for property '"
                    + arrayItemPropertyName + "'.", array);
                return false;
            } else {
                values.push(value);
            }
        }

        return true;
    },
    message: "The items in the array do not have a unique value for property '{0}'."
};

unique constraint (combination of multiple fields)

// works like the SQL unique constraint - only 1 combination of fields is allowed
// similar to customvalidator above, but takes an array of multiple arguments if required
// rather than being limited to a single property.
// it is strongly recommended you change the error message from default is using more than 1 
// property, as only the first property name will be displayed using the default message.
// 
// Note uses Array.isArray - recomend polyfill at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
//
// usage: self.datagridItems.extend({
//            uniqueConstraint:
//                {
//                    params: ["ItemName", "ItemSubcategoryDescription"],
//                    message: "2 or more rows have the same value for both 'Item' and 'Type'"
//                }
//        });
//
ko.validation.rules['uniqueConstraint'] = {
	validator: function (arr, itemPropertyNames) {
		if (!Array.isArray(arr)) {
			throw new TypeError("[uniqueConstraint] must extend an observableArray");
		}
		if (!Array.isArray(itemPropertyNames)) {
			itemPropertyNames = [itemPropertyNames];
		}
		var vals = [], v, stringJoinHash = '`\r', i = 0,
			mapToValues = function (pName) {
				return ko.unwrap(arr[i][pName]);
			};

		for (; i < arr.length; i++) {
			v = ko.utils.arrayMap(itemPropertyNames, mapToValues).join(stringJoinHash);
			if (ko.utils.arrayIndexOf(vals, v) != -1) {
				return false;
			} else {
				vals.push(v);
			}
		}

		return true;
	},
	message: "2 or more '{0}' items do not have a unique value."
};

Multiple email addresses separated by a semicolon

ko.validation.rules['multiemail'] = {
    validator: function (val, validate) {
        if (!validate) { return true; }

        var isValid = true;
        if (!ko.validation.utils.isEmptyVal(val)) {
            // use the required: true property if you don't want to accept empty values
            var values = val.split(';');
            $(values).each(function (index) {
                isValid = ko.validation.rules['email'].validator($.trim(this), validate);
                return isValid; // short circuit each loop if invalid
            });
        }
        return isValid;
    },
    message: 'Please enter valid email addresses (separate multiple email addresses using a semicolon).'
};

isUnique

/* Validates that all values in an array are unique. 
To initialize the simple validator provide an array to compare against. 
By default this will simply compare do an exactly equal (===) operation against each element in the supplied array.
For a little more control, initialize the validator with an object instead. The object should contain two properties: array and predicate. 
The predicate option enables you to provide a function to define equality. The array option can be observable. 
Note: This is similar to the 'arrayItemsPropertyValueUnique' rule but I find it to be more flexible/functional.

SIMPLE EXAMPLE::
model.thisProp.extend({ isUnique: model.thoseProps });

COMPLEX EXAMPLE::
model.selectedOption.extend({ isUnique: {
    params: {
      array: model.options,
      predicate: function (opt, selectedVal) {
          return ko.utils.unwrapObservable(opt.id) === selectedVal;
      }
    }
}});
*/

ko.validation.rules['isUnique'] = {
    validator: function (newVal, options) {
        if (options.predicate && typeof options.predicate !== "function")
            throw new Error("Invalid option for isUnique validator. The 'predicate' option must be a function.");

        var array = options.array || options;
        var count = 0;
        ko.utils.arrayMap(ko.utils.unwrapObservable(array), function(existingVal) {
            if (equalityDelegate()(existingVal, newVal)) count++;
        });
        return count < 2;
        
        function equalityDelegate() {
            return options.predicate ? options.predicate : function(v1, v2) { return v1 === v2; };
        }
    },
    message: 'This value is a duplicate',
};

localizedDate

/* Validates a date for a given culture, using jQuery UI Datepicker utility functions.
Of course, the jQuery UI Datepicker widget must be loaded, along with the globalization settings
(ex: jquery-ui-i18n.fr.js) for the culture(s) being used.

EXAMPLE::
model.thisProp.extend({ localizedDate: 'fr' });
*/

ko.validation.rules['localizedDate'] = {
    validator: function (value, culture) {
        if (ko.validation.utils.isEmptyVal(value) || !culture) return true;

        var settings = $.datepicker.regional[culture];
        try {
            $.datepicker.parseDate(settings.dateFormat, value, settings);
            return true;
        } catch (e) {
            return false;
        }
    },
    message: 'Please enter a proper date'
};

Valid JSON

ko.validation.rules['json'] = {
    validator: function(value, validate) {
        // http://stackoverflow.com/a/20392392
        if (!validate) { return true; }
        try {
            var o = JSON.parse(value);
            return (o && typeof o === "object" && o !== null);
        }
        catch (e) { }
        return false;
    },
    message: 'The field must be a valid JSON'
};

Exists in

 /*
* Checks that all the values in the current array exists in an array.
* Default compare if predicate is not supplied is ===
* Parameter: array or {array: <array>, predicate: <function>}
* Example
*
* viewModel.people = ['peter']
* viewModel.managers: ko.observableArray(['peter', 'anna'].extend({ existsIn: viewModel.people })
* OR
* viewModel.people = [{name:'peter'}]
* viewModel.managers: ko.observableArray(['peter', 'anna'].extend({ existsIn: {
*    array: viewModel.people, 
*    predicate: function(existingVal, newVal){return existingVal.name == newVal.name; }
* }})
*/
 ko.validation.rules["existsIn"] = {
  validator: function (newVal, options) {
   if (options.predicate && typeof options.predicate !== "function")
    throw new Error("Invalid option for existsIn validator. The 'predicate' option must be a function.");

   if (newVal.length == 0) return true;

   var array = options.array || options;
   var pred = options.predicate ? options.predicate : function (v1, v2) { return v1 === v2; };

   return null != ko.utils.arrayFirst(newVal, function (target) {
    return null != ko.utils.arrayFirst(ko.unwrap(array), function (existingVal) {
     return pred(existingVal, target);
    });
   });
  },
  message: "A value does not exist in target array"
 };

Propagate forced validation and reset

If having a complex data structure, the use of ko.validation.group with the deep:true can often be a major pain. On the other hand, the complex data structure probably also includes arrays and objects that needs to be validated.

Note: This is an extender, not a validation rule.

 //must be in its own extend call, AFTER at least 1 validation has been added
 ko.extenders.syncModified = function(target) {
  target.isModified.subscribe(function (val) {
   var obj = ko.unwrap(target);
   if (obj instanceof Array)
    ko.utils.arrayForEach(obj, function(t) {
     if (t.isModified)
      t.isModified(val);
     else
      ko.utils.objectForEach(t, function(_, t2) {
       t2.isModified && t2.isModified(val);
      });
    });
   else
    ko.utils.objectForEach(obj, function(_, t) {
      t.isModified && t.isModified(val);
    });
  });
  return target;
 };

Example:

function ComplexObject(){
   var self = this;
   ...
   self.somearray = ko.observableArray([someOtherObservableWithValidation]);
   self.somearray
     .extend({ validArray: true }) //first a validation
     .extend({ syncModified: true }); //then sync in own block, so it can depend on modified existing
   ...
   self.errors = ko.validation.group(self, { observable: true, live: true });
   self.isValid = ko.computed(function () { return self.errors().length === 0; });
}
function Save(){
  cmplx.errors.showAllMessages(); //FORCE validation
  if (!cmplx.isValid()) return false;
  actual_save();
  cmplx.errors.showAllMessages(false); //FORCE NEW validation
  return true;
}
var cmplx = new ComplexObject();
Save(cmplx);

Min Age

Note: This is for use with the standard html datepicker due to date format restrictions where val is a date in the format 'yyyy-MM-DD'

    ko.validation.rules['minAge'] = {
        validator: function (val, otherVal) {
            var yyyyMMDD = val.split('-');

            var today = new Date();
            var then = new Date(yyyyMMDD[0], yyyyMMDD[1] - 1, yyyyMMDD[2]);

            var yearDiff = today.getYear() - then.getYear();
            var monthDiff = today.getMonth() - then.getMonth();
            var dayDiff = today.getDate() - then.getDate();

            if (yearDiff < otherVal)
                return false;

            if (yearDiff > otherVal)
                return true;

            if (monthDiff < 0)
                return false;

            if (monthDiff > 0)
                return true;

            if (dayDiff < 0)
                return false;

            return true;
        },
        message: 'You must be at least {0} years of age.'
    };

NPI (National Provider Identifier) Number Validation

/*
 * This rule checks in the given value is a valid NPI (National Provider Identifier) number.
 * Validation taken from http://jsfiddle.net/alexdresko/cLNB6/
 *
 * Example:
 *
 * <input type="text" data-bind="value: myNpiObservable.extend({ isNpiValid: true })" />
 * Live Example: https://jsfiddle.net/ckkbs9wg/
 *
*/
ko.validation.rules['isNpiValid'] = {
  validator: function(npi, bool) {
    var length, counter, tmp, sum;

    if (!npi)
      return false;

    length = npi.length;
    counter = 0;

    if ((length === 15) && (npi.indexOf("80840", 0, 5) === 0))
      sum = 0;
    else if (length === 10)
      sum = 24;
    else
      return false;

    while (length !== 0) {
      tmp = npi.charCodeAt(length - 1) - '0'.charCodeAt(0);

      if ((counter++ % 2) !== 0) {
        if ((tmp <<= 1) > 9) {
          tmp -= 10;
          tmp++;
        }
      }

      sum += tmp;
      length--;
    }

    return ((sum % 10) === 0);

  },
  message: 'Invalid NPI Number'
};

IP Address Validation

/*
 * This rule check if the given value is a valid IP Address
 * Example:
 * <input type="text" data-bind="value: myIpObservable.extend({ ipAddress: true })" />
*/
ko.validation.rules['ipAddress'] = {
    validator: function (val) {
        return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test('' + val + '');
    },
    message: 'Please insert a valid ip address'
};

URL Validation

/*
* This rule check if the given value is a valid URL
* Example:
* <input type="text" data-bind="value: myUrlObservable.extend({ url: true })" />
*/
ko.validation.rules['url'] = {
    validator: function (val) {
        return /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/.test('' + val + '');
    },
    message: 'Please use a valid URL starting with http:// or https://'
};