/*
* Knockout Validation
* Created By Eric M. Barnard (https://github.com/ericmbarnard)
*
* Source: https://github.com/ericmbarnard/Knockout-Validation
* MIT License: http://www.opensource.org/licenses/MIT
*/
(function () {
if (typeof (ko) === undefined) { throw 'Knockout is required, please ensure it is loaded before loading this validation plug-in'; }
var defaults = {
registerExtenders: true,
messagesOnModified: true,
messageTemplate: null,
insertMessages: true, // automatically inserts validation messages as <span></span>
parseInputAttributes: false, // parses the HTML5 validation attribute from a form element and adds that to the object
writeInputAttributes: false, // adds HTML5 input validation attributes to form elements that ko observable's are bound to
decorateElement: false, // false to keep backward compatibility
errorClass: null, // single class for error message and element
errorElementClass: 'validationElement', // class to decorate error element
errorMessageClass: 'validationMessage', // class to decorate error message
grouping: {
deep: false, //by default grouping is shallow
observable: true //and using observables
}
};
// make a copy so we can use 'reset' later
var configuration = ko.utils.extend({}, defaults);
var html5Attributes = ['required', 'pattern', 'min', 'max', 'step'];
var async = function (expr) {
if (window.setImmediate) { window.setImmediate(expr); }
else { window.setTimeout(expr, 0); }
};
//#region Utilities
var utils = (function () {
var seedId = new Date().getTime();
var domData = {}; //hash of data objects that we reference from dom elements
var domDataKey = '__ko_validation__';
return {
isArray: function (o) {
return o.isArray || Object.prototype.toString.call(o) === '[object Array]';
},
isObject: function (o) {
return o !== null && typeof o === 'object';
},
values: function (o) {
var r = [];
for (var i in o) {
if (o.hasOwnProperty(i)) {
r.push(o[i]);
}
}
return r;
},
getValue: function (o) {
return (typeof o === 'function' ? o() : o);
},
hasAttribute: function (node, attr) {
return node.getAttribute(attr) !== null;
},
isValidatable: function (o) {
return o.rules && o.isValid && o.isModified;
},
insertAfter: function (node, newNode) {
node.parentNode.insertBefore(newNode, node.nextSibling);
},
newId: function () {
return seedId += 1;
},
getConfigOptions: function (element) {
var options = utils.contextFor(element);
return options || configuration;
},
setDomData: function (node, data) {
var key = node[domDataKey];
if (!key) {
node[domDataKey] = key = utils.newId();
}
domData[key] = data;
},
getDomData: function (node) {
var key = node[domDataKey];
if (!key) {
return undefined;
}
return domData[key];
},
contextFor: function (node) {
switch (node.nodeType) {
case 1:
case 8:
var context = utils.getDomData(node);
if (context) return context;
if (node.parentNode) return utils.contextFor(node.parentNode);
break;
}
return undefined;
},
isEmptyVal: function (val) {
if (val === undefined) {
return true;
}
if (val === null) {
return true;
}
if (val === "") {
return true;
}
}
};
} ());
//#endregion
//#region Public API
ko.validation = (function () {
var isInitialized = 0;
return {
utils: utils,
//Call this on startup
//any config can be overridden with the passed in options
init: function (options, force) {
//done run this multiple times if we don't really want to
if (isInitialized > 0 && !force) {
return;
}
//becuase we will be accessing options properties it has to be an object at least
options = options || {};
//if specific error classes are not provided then apply generic errorClass
//it has to be done on option so that options.errorClass can override default
//errorElementClass and errorMessage class but not those provided in options
options.errorElementClass = options.errorElementClass || options.errorClass || configuration.errorElementClass;
options.errorMessageClass = options.errorMessageClass || options.errorClass || configuration.errorMessageClass;
ko.utils.extend(configuration, options);
if (configuration.registerExtenders) {
ko.validation.registerExtenders();
}
isInitialized = 1;
},
// backwards compatability
configure: function (options) { ko.validation.init(options); },
// resets the config back to its original state
reset: function () { configuration = $.extend(configuration, defaults); },
// recursivly walks a viewModel and creates an object that
// provides validation information for the entire viewModel
// obj -> the viewModel to walk
// options -> {
// deep: false, // if true, will walk past the first level of viewModel properties
// observable: false // if true, returns a computed observable indicating if the viewModel is valid
// }
group: function group(obj, options) { // array of observables or viewModel
var options = ko.utils.extend(configuration.grouping, options),
validatables = ko.observableArray([]),
result = null,
//anonymous, immediate function to traverse objects hierarchically
//if !options.deep then it will stop on top level
traverse = function traverse(obj, level) {
var objValues = [],
val = ko.utils.unwrapObservable(obj);
//default level value depends on deep option.
level = (level !== undefined ? level : options.deep ? 1 : -1);
// if object is observable then add it to the list
if (ko.isObservable(obj)) {
//make sure it is validatable object
if (!obj.isValid) obj.extend({ validatable: true });
validatables.push(obj);
}
//get list of values either from array or object but ignore non-objects
if (val) {
if (utils.isArray(val)) {
objValues = val;
} else if (utils.isObject(val)) {
objValues = utils.values(val);
}
}
//process recurisvely if it is deep grouping
if (level !== 0) {
ko.utils.arrayForEach(objValues, function (observable) {
//but not falsy things and not HTML Elements
if (observable && !observable.nodeType) traverse(observable, level + 1);
});
}
};
//if using observables then traverse structure once and add observables
if (options.observable) {
traverse(obj);
result = ko.computed(function () {
var errors = [];
ko.utils.arrayForEach(validatables(), function (observable) {
if (!observable.isValid()) {
errors.push(observable.error);
}
});
return errors;
});
} else { //if not using observables then every call to error() should traverse the structure
result = function () {
var errors = [];
validatables([]); //clear validatables
traverse(obj); // and traverse tree again
ko.utils.arrayForEach(validatables(), function (observable) {
if (!observable.isValid()) {
errors.push(observable.error);
}
});
return errors;
};
}
result.showAllMessages = function (show) { // thanks @heliosPortal
if (show == undefined) //default to true
show = true;
// ensure we have latest changes
result();
ko.utils.arrayForEach(validatables(), function (observable) {
observable.isModified(show);
});
};
obj.errors = result;
obj.isValid = function () {
return obj.errors().length === 0;
};
obj.isAnyMessageShown = function() {
var invalidAndModifiedPresent = false;
// ensure we have latest changes
result();
ko.utils.arrayForEach(validatables(), function (observable) {
if (!observable.isValid() && observable.isModified()) {
invalidAndModifiedPresent = true;
}
});
return invalidAndModifiedPresent;
};
return result;
},
formatMessage: function (message, params) {
return message.replace(/\{0\}/gi, params);
},
// addRule:
// This takes in a ko.observable and a Rule Context - which is just a rule name and params to supply to the validator
// ie: ko.validation.addRule(myObservable, {
// rule: 'required',
// params: true
// });
//
addRule: function (observable, rule) {
observable.extend({ validatable: true });
//push a Rule Context to the observables local array of Rule Contexts
observable.rules.push(rule);
return observable;
},
// addAnonymousRule:
// Anonymous Rules essentially have all the properties of a Rule, but are only specific for a certain property
// and developers typically are wanting to add them on the fly or not register a rule with the 'ko.validation.rules' object
//
// Example:
// var test = ko.observable('something').extend{(
// validation: {
// validator: function(val, someOtherVal){
// return true;
// },
// message: "Something must be really wrong!',
// params: true
// }
// )};
addAnonymousRule: function (observable, ruleObj) {
var ruleName = utils.newId();
if ( ruleObj['message'] === undefined ) {
rulesObj['message'] = 'Error';
}
//Create an anonymous rule to reference
ko.validation.rules[ruleName] = ruleObj;
//add the anonymous rule to the observable
ko.validation.addRule(observable, {
rule: ruleName,
params: ruleObj.params
});
},
addExtender: function (ruleName) {
ko.extenders[ruleName] = function (observable, params) {
//params can come in a few flavors
// 1. Just the params to be passed to the validator
// 2. An object containing the Message to be used and the Params to pass to the validator
// 3. A condition when the validation rule to be applied
//
// Example:
// var test = ko.observable(3).extend({
// max: {
// message: 'This special field has a Max of {0}',
// params: 2,
// onlyIf: function() {
// return specialField.IsVisible();
// }
// }
// )};
//
if (params.message || params.onlyIf) { //if it has a message or condition object, then its an object literal to use
return ko.validation.addRule(observable, {
rule: ruleName,
message: params.message,
params: utils.isEmptyVal(params.params) ? true : params.params,
condition: params.onlyIf
});
} else {
return ko.validation.addRule(observable, {
rule: ruleName,
params: params
});
}
};
},
// loops through all ko.validation.rules and adds them as extenders to
// ko.extenders
registerExtenders: function () { // root extenders optional, use 'validation' extender if would cause conflicts
if (configuration.registerExtenders) {
for (var ruleName in ko.validation.rules) {
if (ko.validation.rules.hasOwnProperty(ruleName)) {
if (!ko.extenders[ruleName]) {
ko.validation.addExtender(ruleName);
}
}
}
}
},
//creates a span next to the @element with the specified error class
insertValidationMessage: function (element) {
var span = document.createElement('SPAN');
span.className = utils.getConfigOptions(element).errorMessageClass;
utils.insertAfter(element, span);
return span;
},
// if html-5 validation attributes have been specified, this parses
// the attributes on @element
parseInputValidationAttributes: function (element, valueAccessor) {
ko.utils.arrayForEach(html5Attributes, function (attr) {
if (utils.hasAttribute(element, attr)) {
ko.validation.addRule(valueAccessor(), {
rule: attr,
params: element.getAttribute(attr) || true
});
}
});
},
// writes html5 validation attributes on the element passed in
writeInputValidationAttributes: function (element, valueAccessor) {
var observable = valueAccessor();
if (!observable || !observable.rules) {
return;
}
var contexts = observable.rules(); // observable array
// loop through the attributes and add the information needed
ko.utils.arrayForEach(html5Attributes, function (attr) {
var params;
var ctx = ko.utils.arrayFirst(contexts, function (ctx) {
return ctx.rule.toLowerCase() === attr.toLowerCase();
});
if (!ctx)
return;
params = ctx.params;
// we have to do some special things for the pattern validation
if (ctx.rule == "pattern") {
if (ctx.params instanceof RegExp) {
params = ctx.params.source; // we need the pure string representation of the RegExpr without the //gi stuff
}
}
// we have a rule matching a validation attribute at this point
// so lets add it to the element along with the params
element.setAttribute(attr, params);
});
contexts = null;
}
};
} ());
//#endregion
//#region Core Validation Rules
//Validation Rules:
// You can view and override messages or rules via:
// ko.validation.rules[ruleName]
//
// To implement a custom Rule, simply use this template:
// ko.validation.rules['<custom rule name>'] = {
// validator: function (val, param) {
// <custom logic>
// return <true or false>;
// },
// message: '<custom validation message>' //optionally you can also use a '{0}' to denote a placeholder that will be replaced with your 'param'
// };
//
// Example:
// ko.validation.rules['mustEqual'] = {
// validator: function( val, mustEqualVal ){
// return val === mustEqualVal;
// },
// message: 'This field must equal {0}'
// };
//
ko.validation.rules = {};
ko.validation.rules['required'] = {
validator: function (val, required) {
var stringTrimRegEx = /^\s+|\s+$/g,
testVal;
if (val === undefined || val === null) {
return !required;
}
testVal = val;
if (typeof (val) == "string") {
testVal = val.replace(stringTrimRegEx, '');
}
return required && (testVal + '').length > 0;
},
message: 'This field is required.'
};
ko.validation.rules['min'] = {
validator: function (val, min) {
return utils.isEmptyVal(val) || val >= min;
},
message: 'Please enter a value greater than or equal to {0}.'
};
ko.validation.rules['max'] = {
validator: function (val, max) {
return utils.isEmptyVal(val) || val <= max;
},
message: 'Please enter a value less than or equal to {0}.'
};
ko.validation.rules['minLength'] = {
validator: function (val, minLength) {
return utils.isEmptyVal(val) || val.length >= minLength;
},
message: 'Please enter at least {0} characters.'
};
ko.validation.rules['maxLength'] = {
validator: function (val, maxLength) {
return utils.isEmptyVal(val) || val.length <= maxLength;
},
message: 'Please enter no more than {0} characters.'
};
ko.validation.rules['pattern'] = {
validator: function (val, regex) {
return utils.isEmptyVal(val) || val.match(regex) != null;
},
message: 'Please check this value.'
};
ko.validation.rules['step'] = {
validator: function (val, step) {
// in order to handle steps of .1 & .01 etc.. Modulus won't work
// if the value is a decimal, so we have to correct for that
return utils.isEmptyVal(val) || (val * 100) % (step * 100) === 0;
},
message: 'The value must increment by {0}'
};
ko.validation.rules['email'] = {
validator: function (val, validate) {
//I think an empty email address is also a valid entry
//if one want's to enforce entry it should be done with 'required: true'
return utils.isEmptyVal(val) || (
validate && /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(val)
);
},
message: 'Please enter a proper email address'
};
ko.validation.rules['date'] = {
validator: function (value, validate) {
return utils.isEmptyVal(value) || (validate && !/Invalid|NaN/.test(new Date(value)));
},
message: 'Please enter a proper date'
};
ko.validation.rules['dateISO'] = {
validator: function (value, validate) {
return utils.isEmptyVal(value) || (validate && /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(value));
},
message: 'Please enter a proper date'
};
ko.validation.rules['number'] = {
validator: function (value, validate) {
return utils.isEmptyVal(value) || (validate && /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(value));
},
message: 'Please enter a number'
};
ko.validation.rules['digit'] = {
validator: function (value, validate) {
return utils.isEmptyVal(value) || (validate && /^\d+$/.test(value));
},
message: 'Please enter a digit'
};
ko.validation.rules['phoneUS'] = {
validator: function (phoneNumber, validate) {
if (typeof (phoneNumber) !== 'string') { return false; }
if (utils.isEmptyVal(phoneNumber)) { return true; } // makes it optional, use 'required' rule if it should be required
phoneNumber = phoneNumber.replace(/\s+/g, "");
return validate && phoneNumber.length > 9 && phoneNumber.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/);
},
message: 'Please specify a valid phone number'
};
ko.validation.rules['equal'] = {
validator: function (val, params) {
var otherValue = params;
return val === utils.getValue(otherValue);
},
message: 'Values must equal'
};
ko.validation.rules['notEqual'] = {
validator: function (val, params) {
var otherValue = params;
return val !== utils.getValue(otherValue);
},
message: 'Please choose another value.'
};
//unique in collection
// options are:
// collection: array or function returning (observable) array
// in which the value has to be unique
// valueAccessor: function that returns value from an object stored in collection
// if it is null the value is compared directly
// external: set to true when object you are validating is automatically updating collection
ko.validation.rules['unique'] = {
validator: function (val, options) {
var c = utils.getValue(options.collection),
external = utils.getValue(options.externalValue),
counter = 0;
if (!val || !c) return true;
ko.utils.arrayFilter(ko.utils.unwrapObservable(c), function (item) {
if (val === (options.valueAccessor ? options.valueAccessor(item) : item)) counter++;
});
// if value is external even 1 same value in collection means the value is not unique
return counter < (external !== undefined && val !== external ? 1 : 2);
},
message: 'Please make sure the value is unique.'
};
//now register all of these!
(function () {
ko.validation.registerExtenders();
} ());
//#endregion
//#region Knockout Binding Handlers
// The core binding handler
// this allows us to setup any value binding that internally always
// performs the same functionality
ko.bindingHandlers['validationCore'] = (function () {
return {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var config = utils.getConfigOptions(element);
// parse html5 input validation attributes, optional feature
if (config.parseInputAttributes) {
async(function () { ko.validation.parseInputValidationAttributes(element, valueAccessor) });
}
// if requested insert message element and apply bindings
if (config.insertMessages && utils.isValidatable(valueAccessor())) {
// insert the <span></span>
var validationMessageElement = ko.validation.insertValidationMessage(element);
// if we're told to use a template, make sure that gets rendered
if (config.messageTemplate) {
ko.renderTemplate(config.messageTemplate, { field: valueAccessor() }, null, validationMessageElement, 'replaceNode');
} else {
ko.applyBindingsToNode(validationMessageElement, { validationMessage: valueAccessor() });
}
}
// write the html5 attributes if indicated by the config
if (config.writeInputAttributes && utils.isValidatable(valueAccessor())) {
ko.validation.writeInputValidationAttributes(element, valueAccessor);
}
// if requested, add binding to decorate element
if (config.decorateElement && utils.isValidatable(valueAccessor())) {
ko.applyBindingsToNode(element, { validationElement: valueAccessor() });
}
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
// hook for future extensibility
}
};
}());
// override for KO's default 'value' binding
(function () {
var init = ko.bindingHandlers['value'].init;
ko.bindingHandlers['value'].init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
init(element, valueAccessor, allBindingsAccessor);
return ko.bindingHandlers['validationCore'].init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
};
} ());
ko.bindingHandlers['validationMessage'] = { // individual error message, if modified or post binding
update: function (element, valueAccessor) {
var obsv = valueAccessor(),
config = utils.getConfigOptions(element),
val = ko.utils.unwrapObservable(obsv),
msg = null,
isModified = false,
isValid = false;
obsv.extend({ validatable: true });
isModified = obsv.isModified();
isValid = obsv.isValid();
// create a handler to correctly return an error message
var errorMsgAccessor = function () {
if (!config.messagesOnModified || isModified) {
return isValid ? null : obsv.error;
} else {
return null;
}
};
//toggle visibility on validation messages when validation hasn't been evaluated, or when the object isValid
var visiblityAccessor = function () {
return isModified ? !isValid : false;
};
ko.bindingHandlers.text.update(element, errorMsgAccessor);
ko.bindingHandlers.visible.update(element, visiblityAccessor);
}
};
ko.bindingHandlers['validationElement'] = {
update: function (element, valueAccessor) {
var obsv = valueAccessor(),
config = utils.getConfigOptions(element),
val = ko.utils.unwrapObservable(obsv),
msg = null,
isModified = false,
isValid = false;
obsv.extend({ validatable: true });
isModified = obsv.isModified();
isValid = obsv.isValid();
// create an evaluator function that will return something like:
// css: { validationElement: true }
var cssSettingsAccessor = function () {
var css = {};
var shouldShow = (isModified ? !isValid : false);
if (!config.decorateElement) { shouldShow = false; }
// css: { validationElement: false }
css[config.errorElementClass] = shouldShow;
return css;
};
//add or remove class on the element;
ko.bindingHandlers.css.update(element, cssSettingsAccessor);
}
};
// ValidationOptions:
// This binding handler allows you to override the initial config by setting any of the options for a specific element or context of elements
//
// Example:
// <div data-bind="validationOptions: { insertMessages: true, messageTemplate: 'customTemplate', errorMessageClass: 'mySpecialClass'}">
// <input type="text" data-bind="value: someValue"/>
// <input type="text" data-bind="value: someValue2"/>
// </div>
ko.bindingHandlers['validationOptions'] = (function () {
return {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var options = ko.utils.unwrapObservable(valueAccessor());
if (options) {
var newConfig = ko.utils.extend({}, configuration);
ko.utils.extend(newConfig, options);
//store the validation options on the node so we can retrieve it later
utils.setDomData(element, newConfig);
}
}
};
} ());
//#endregion
//#region Knockout Extenders
// Validation Extender:
// This is for creating custom validation logic on the fly
// Example:
// var test = ko.observable('something').extend{(
// validation: {
// validator: function(val, someOtherVal){
// return true;
// },
// message: "Something must be really wrong!',
// params: true
// }
// )};
ko.extenders['validation'] = function (observable, rules) { // allow single rule or array
ko.utils.arrayForEach(utils.isArray(rules) ? rules : [rules], function (rule) {
// the 'rule' being passed in here has no name to identify a core Rule,
// so we add it as an anonymous rule
// If the developer is wanting to use a core Rule, but use a different message see the 'addExtender' logic for examples
ko.validation.addAnonymousRule(observable, rule);
});
return observable;
};
//This is the extender that makes a Knockout Observable also 'Validatable'
//examples include:
// 1. var test = ko.observable('something').extend({validatable: true});
// this will ensure that the Observable object is setup properly to respond to rules
//
// 2. test.extend({validatable: false});
// this will remove the validation properties from the Observable object should you need to do that.
ko.extenders['validatable'] = function (observable, enable) {
if (enable && !utils.isValidatable(observable)) {
observable.error = null; // holds the error message, we only need one since we stop processing validators when one is invalid
// observable.rules:
// ObservableArray of Rule Contexts, where a Rule Context is simply the name of a rule and the params to supply to it
//
// Rule Context = { rule: '<rule name>', params: '<passed in params>', message: '<Override of default Message>' }
observable.rules = ko.observableArray(); //holds the rule Contexts to use as part of validation
//in case async validation is occuring
observable.isValidating = ko.observable(false);
//the true holder of whether the observable is valid or not
observable.__valid__ = ko.observable(true);
observable.isModified = ko.observable(false);
// we use a computed here to ensure that anytime a dependency changes, the
// validation logic evaluates
var h_obsValidationTrigger = ko.computed(function () {
var obs = observable(),
ruleContexts = observable.rules();
ko.validation.validateObservable(observable);
return true;
});
// a semi-protected observable
observable.isValid = ko.computed(function () {
return observable.__valid__();
});
//subscribe to changes in the observable
var h_change = observable.subscribe(function () {
observable.isModified(true);
});
observable._disposeValidation = function () {
//first dispose of the subscriptions
observable.isValid.dispose();
observable.rules.removeAll();
observable.isModified._subscriptions['change'] = [];
observable.isValidating._subscriptions['change'] = [];
observable.__valid__._subscriptions['change'] = [];
h_change.dispose();
h_obsValidationTrigger.dispose();
delete observable['rules'];
delete observable['error'];
delete observable['isValid'];
delete observable['isValidating'];
delete observable['__valid__'];
delete observable['isModified'];
};
} else if (enable === false && utils.isValidatable(observable)) {
if (observable._disposeValidation) {
observable._disposeValidation();
}
}
return observable;
};
function validateSync(observable, rule, ctx) {
//Execute the validator and see if its valid
if (!rule.validator(observable(), ctx.params === undefined ? true : ctx.params)) { // default param is true, eg. required = true
//not valid, so format the error message and stick it in the 'error' variable
observable.error = ko.validation.formatMessage(ctx.message || rule.message, ctx.params);
observable.__valid__(false);
return false;
} else {
return true;
}
}
function validateAsync(observable, rule, ctx) {
observable.isValidating(true);
var callBack = function (valObj) {
var isValid = false,
msg = '';
if (!observable.__valid__()) {
// since we're returning early, make sure we turn this off
observable.isValidating(false);
return; //if its already NOT valid, don't add to that
}
//we were handed back a complex object
if (valObj['message']) {
isValid = valObj.isValid;
msg = valObj.message;
} else {
isValid = valObj;
}
if (!isValid) {
//not valid, so format the error message and stick it in the 'error' variable
observable.error = ko.validation.formatMessage(msg || ctx.message || rule.message, ctx.params);
observable.__valid__(isValid);
}
// tell it that we're done
observable.isValidating(false);
};
//fire the validator and hand it the callback
rule.validator(observable(), ctx.params || true, callBack);
}
ko.validation.validateObservable = function (observable) {
var i = 0,
rule, // the rule validator to execute
ctx, // the current Rule Context for the loop
ruleContexts = observable.rules(), //cache for iterator
len = ruleContexts.length; //cache for iterator
for (; i < len; i++) {
//get the Rule Context info to give to the core Rule
ctx = ruleContexts[i];
// checks an 'onlyIf' condition
if (ctx.condition && !ctx.condition())
continue;
//get the core Rule to use for validation
rule = ko.validation.rules[ctx.rule];
if (rule['async'] || ctx['async']) {
//run async validation
validateAsync(observable, rule, ctx);
} else {
//run normal sync validation
if (!validateSync(observable, rule, ctx)) {
return false; //break out of the loop
}
}
}
//finally if we got this far, make the observable valid again!
observable.error = null;
observable.__valid__(true);
return true;
};
//#endregion
//#region Validated Observable
ko.validatedObservable = function (initialValue) {
if (!ko.validation.utils.isObject(initialValue)) { return ko.observable(initialValue).extend({ validatable: true }); }
var obsv = ko.observable(initialValue);
obsv.errors = ko.validation.group(initialValue);
obsv.isValid = ko.computed(function () {
return obsv.errors().length === 0;
});
return obsv;
};
//#endregion
//#region Localization
//quick function to override rule messages
ko.validation.localize = function (msgTranslations) {
var msg, rule;
//loop the properties in the object and assign the msg to the rule
for (rule in msgTranslations) {
if (ko.validation.rules.hasOwnProperty(rule)) {
ko.validation.rules[rule].message = msgTranslations[rule];
}
}
};
//#endregion
//#region ApplyBindings Added Functionality
ko.applyBindingsWithValidation = function (viewModel, rootNode, options) {
var len = arguments.length,
node, config;
if (len > 2) { // all parameters were passed
node = rootNode;
config = options;
} else if (len < 2) {
node = document.body;
} else { //have to figure out if they passed in a root node or options
if (arguments[1].nodeType) { //its a node
node = rootNode;
} else {
config = arguments[1];
}
}
ko.validation.init();
if (config) { ko.validation.utils.setDomData(node, config); }
ko.applyBindings(viewModel, rootNode);
};
//override the original applyBindings so that we can ensure all new rules and what not are correctly registered
var origApplyBindings = ko.applyBindings;
ko.applyBindings = function (viewModel, rootNode) {
ko.validation.init();
origApplyBindings(viewModel, rootNode);
};
//#endregion
})();