///
(function () {
if (typeof (ko) === undefined) { throw 'Knockout is required, please ensure it is loaded before loading this validation plug-in'; }
var configuration = {
registerExtenders: true,
messagesOnModified: true,
messageTemplate: null,
insertMessages: true,
parseInputAttributes: false,
errorMessageClass: 'validationMessage'
};
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();
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;
},
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);
},
extend: function (o, p, q) {
if (!p) {
return o;
}
for (var i in p) {
if (utils.isObject(p[i])) {
if (!o[i]) { o[i] = {}; }
utils.extend(o[i], p[i]);
} else {
o[i] = p[i];
}
}
if (q) {
utils.extend(o, q);
}
return o;
},
newId: function () {
return seedId += 1;
}
};
} ());
//#endregion
ko.validation = {
//Call this on startup
//any config can be overridden with the passed in options
init: function (options) {
utils.extend(configuration, options);
if (configuration.registerExtenders) {
ko.validation.registerExtenders();
}
ko.validation.registerValueBindingHandler();
},
//backwards compatability
configure: function (options) { ko.validation.init(options); },
group: function (obj) { // array of observables or viewModel
var objValues = utils.isArray(obj) ? obj : utils.values(obj);
var observables = ko.utils.arrayFilter(objValues, function (item) {
if (ko.isObservable(item)) {
item.extend({ validatable: true });
return true;
}
return false;
});
var result = ko.dependentObservable(function () {
var errors = [];
ko.utils.arrayForEach(observables, function (observable) {
if (!observable.isValid()) {
errors.push(observable.error);
}
});
return errors;
});
result.showAllMessages = function () {
ko.utils.arrayForEach(observables, function (observable) {
observable.isModified(true);
});
};
return result;
},
formatMessage: function (message, params) {
return message.replace('{0}', 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();
//Create an anonymous rule to reference
ko.validation.rules[ruleName] = {
validator: ruleObj.validator,
message: ruleObj.message || 'Error'
};
//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
//
// Example:
// var test = ko.observable(3).extend({
// max: {
// message: 'This special field has a Max of {0}',
// params: 2
// }
// )};
//
if (params.message) { //if it has a message object, then its an object literal to use
return ko.validation.addRule(observable, {
rule: ruleName,
message: params.message,
params: params.params || true
});
} else {
return ko.validation.addRule(observable, {
rule: ruleName,
params: params
});
}
};
},
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);
}
}
}
}
},
insertValidationMessage: function (element) {
var span = document.createElement('SPAN');
span.className = configuration.errorMessageClass;
utils.insertAfter(element, span);
return span;
},
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
});
}
});
},
registerValueBindingHandler: function () { // parse html5 input validation attributes where value binder, optional feature
var init = ko.bindingHandlers.value.init;
ko.bindingHandlers.value.init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
init(element, valueAccessor, allBindingsAccessor);
//if the bindingContext contains a $validation object, they must be using a validationOptions binding
var config = utils.extend({}, configuration, bindingContext.$data.$validation);
if (config.parseInputAttributes) {
async(function () { ko.validation.parseInputValidationAttributes(element, valueAccessor) });
}
if (config.insertMessages && utils.isValidatable(valueAccessor())) {
var validationMessageElement = ko.validation.insertValidationMessage(element);
if (config.messageTemplate) {
ko.renderTemplate(config.messageTemplate, { field: valueAccessor() }, null, validationMessageElement, 'replaceNode');
} else {
ko.applyBindingsToNode(validationMessageElement, { validationMessage: valueAccessor() });
}
}
};
}
};
ko.validation.utils = utils;
//#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[''] = {
// validator: function (val, param) {
//
// return ;
// },
// 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) {
return required && val && (val + '').length > 0;
},
message: 'This field is required.'
};
ko.validation.rules['min'] = {
validator: function (val, min) {
return !val || val >= min;
},
message: 'Please enter a value greater than or equal to {0}.'
};
ko.validation.rules['max'] = {
validator: function (val, max) {
return !val || val <= max;
},
message: 'Please enter a value less than or equal to {0}.'
};
ko.validation.rules['minLength'] = {
validator: function (val, minLength) {
return val && val.length >= minLength;
},
message: 'Please enter at least {0} characters.'
};
ko.validation.rules['maxLength'] = {
validator: function (val, maxLength) {
return !val || val.length <= maxLength;
},
message: 'Please enter no more than {0} characters.'
};
ko.validation.rules['pattern'] = {
validator: function (val, regex) {
return !val || val.match(regex) != null;
},
message: 'Please check this value.'
};
ko.validation.rules['step'] = {
validator: function (val, step) {
return val % step === 0;
},
message: 'The value must increment by {0}'
};
//#endregion
//#region Knockout Binding Handlers
ko.bindingHandlers['validationMessage'] = { // individual error message, if modified or post binding
update: function (element, valueAccessor) {
var obsv = valueAccessor();
obsv.extend({ validatable: true });
var errorMsgAccessor = function () {
if (!configuration.messagesOnModified || obsv.isModified()) {
return obsv.isValid() ? null : obsv.error;
} else {
return null;
}
};
ko.bindingHandlers.text.update(element, errorMsgAccessor);
}
};
// 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:
//
//
//
//
ko.bindingHandlers['validationOptions'] = {
makeValueAccessor: function (valueAccessor, bindingContext) {
return function () {
var validationAddIn = { $validation: valueAccessor() };
return utils.extend({}, validationAddIn, bindingContext.$data);
};
},
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
//We don't want to change the context of the 'WITH' binding... just simply pull the options out of the binding string
// so we just pass the same context down, and store the validation options on the $data item.
var newValueAccessor = ko.bindingHandlers.validationOptions.makeValueAccessor(valueAccessor, bindingContext);
return ko.bindingHandlers['with'].init(element, newValueAccessor, allBindingsAccessor, viewModel, bindingContext);
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var newValueAccessor = ko.bindingHandlers.validationOptions.makeValueAccessor(valueAccessor, bindingContext);
return ko.bindingHandlers['with'].update(element, newValueAccessor, allBindingsAccessor, viewModel, bindingContext);
}
};
//#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: '', params: '', message: '' }
observable.rules = ko.observableArray(); //holds the rule Contexts to use as part of validation
observable.isValid = ko.dependentObservable(function () {
var i = 0,
r, // the rule validator to execute
ctx, // the current Rule Context for the loop
rules = observable.rules(), //cache for iterator
len = rules.length; //cache for iterator
for (; i < len; i++) {
//get the Rule Context info to give to the core Rule
ctx = rules[i];
//get the core Rule to use for validation
r = ko.validation.rules[ctx.rule];
//Execute the validator and see if its valid
if (!r.validator(observable(), ctx.params || true)) { // 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 || r.message, ctx.params);
return false;
}
}
observable.error = null;
return true;
});
observable.isModified = ko.observable(false);
observable.subscribe(function (newValue) {
observable.isModified(true);
});
}
return observable;
};
//#endregion
//#region Additional Rules
ko.validation.rules['email'] = {
validator: function (val, validate) {
return 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: '{0} is not a proper email address'
};
ko.validation.rules['date'] = {
validator: function (value, validate) {
return validate && !/Invalid|NaN/.test(new Date(value));
},
message: 'Please enter a proper date'
};
ko.validation.rules['dateISO'] = {
validator: function (value, validate) {
return 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 validate && /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(value);
},
message: 'Please enter a number'
};
ko.validation.rules['digits'] = {
validator: function (value, validate) {
return validate && /^\d+$/.test(value);
},
message: 'Please enter a digit'
};
ko.validation.rules['phoneUS'] = {
validator: function (phoneNumber, validate) {
if (typeof (phoneNumber) !== 'string') { return false; }
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'
};
//#endregion
//#region validatedObservable
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.dependentObservable(function () {
return obsv.errors().length === 0;
});
return obsv;
};
//#endregion
})();