MVVM架构~knockoutjs系列之从Knockout.Validation.js源码中学习它的用法
说在前
有时,我们在使用一个插件时,在网上即找不到它的相关API,这时,我们会很抓狂的,与其抓狂,还不如踏下心来,分析一下它的源码,事实上,对于JS这种开发语言来说,它开发的插件的使用方法都在它的源码里,只要你踏下心去看,一切就都有了!
Knockout.Validation.js是为Knockout插件服务的,它可以为Knockout对象进行验证,就像你使用MVC模型验证一样,而这种绑定的验证方式对于开发人员来说是很容易接受的,也是一种趋势,它在验证过程中,会将出现异常的点记录下来,然后在
某个时候将它抛出来,这个抛出的时刻通常是对象失去焦点时(blur)。
总结Knockout.Validation.js几个常用的东西
为空验证
self.CategoryId = ko.observable().extend({ required: true });
最大最小值验证
self.price = ko.observable().extend({ required: { params: true, message: "请输入价格" }, min: { params: 1, message: "请输入大于1的整数" }, max: 100 });
长度验证
self.name = ko.observable().extend({ minLength: 2, maxLength: { params: 30, message: "名称最大长度为30个字符" }, required: { params: true, message: "请输入名称", } });
电话验证
self.phone = ko.observable().extend({ phoneUS: { params: true, message: "电话不合法", } });
邮箱验证
self.Email = ko.observable().extend({ required: { params: true, message: "请填写Email" }, email: { params: true, message: "Email格式不正确" } });
数字验证
self.age = ko.observable().extend({ number: { params: true, message: "必须是数字", } });
相等验证
self.PayPassword = ko.observable().extend({ required: { params: true, message: "请填写支付密码" }, equal:{ params:"zzl", message:"支付密码错误" }
事实上,Knockout.Validation.js还有包括range,date,digit,notEqual等验证,都大同小意,我就不一一说了。
Knockout.Validation.js源码
/*============================================================================= Author: Eric M. Barnard - @ericmbarnard License: MIT (http://opensource.org/licenses/mit-license.php) Description: Validation Library for KnockoutJS =============================================================================== */ /*globals require: false, exports: false, define: false, ko: false */ (function (factory) { // Module systems magic dance. if (typeof require === "function" && typeof exports === "object" && typeof module === "object") { // CommonJS or Node: hard-coded dependency on "knockout" factory(require("knockout"), exports); } else if (typeof define === "function" && define["amd"]) { // AMD anonymous module with hard-coded dependency on "knockout" define(["knockout", "exports"], factory); } else { // <script> tag: use the global `ko` object, attaching a `mapping` property factory(ko, ko.validation = {}); } }(function ( ko, exports ) { if (typeof (ko) === undefined) { throw 'Knockout is required, please ensure it is loaded before loading this validation plug-in'; } // create our namespace object ko.validation = exports; var kv = ko.validation, koUtils = ko.utils, unwrap = koUtils.unwrapObservable, forEach = koUtils.arrayForEach, extend = koUtils.extend; ;/*global ko: false*/ var defaults = { registerExtenders: true, messagesOnModified: true, errorsAsTitle: true, // enables/disables showing of errors as title attribute of the target element. errorsAsTitleOnModified: false, // shows the error when hovering the input field (decorateElement must be 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 decorateInputElement: false, // false to keep backward compatibility decorateElementOnModified: true,// true 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 allowHtmlMessages: false, // allows HTML in validation messages grouping: { deep: false, //by default grouping is shallow observable: true, //and using observables live: false //react to changes to observableArrays if observable === true }, validate: { // throttle: 10 } }; // make a copy so we can use 'reset' later var configuration = extend({}, defaults); configuration.html5Attributes = ['required', 'pattern', 'min', 'max', 'step']; configuration.html5InputTypes = ['email', 'number', 'date']; configuration.reset = function () { extend(configuration, defaults); }; kv.configuration = configuration; ;kv.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'; }, isObservableArray: function(instance) { return !!instance && typeof instance["remove"] === "function" && typeof instance["removeAll"] === "function" && typeof instance["destroy"] === "function" && typeof instance["destroyAll"] === "function" && typeof instance["indexOf"] === "function" && typeof instance["replace"] === "function"; }, 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; }, getAttribute: function (element, attr) { return element.getAttribute(attr); }, setAttribute: function (element, attr, value) { return element.setAttribute(attr, value); }, isValidatable: function (o) { return !!(o && 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 = kv.utils.contextFor(element); return options || kv.configuration; }, setDomData: function (node, data) { var key = node[domDataKey]; if (!key) { node[domDataKey] = key = kv.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 = kv.utils.getDomData(node); if (context) { return context; } if (node.parentNode) { return kv.utils.contextFor(node.parentNode); } break; } return undefined; }, isEmptyVal: function (val) { if (val === undefined) { return true; } if (val === null) { return true; } if (val === "") { return true; } }, getOriginalElementTitle: function (element) { var savedOriginalTitle = kv.utils.getAttribute(element, 'data-orig-title'), currentTitle = element.title, hasSavedOriginalTitle = kv.utils.hasAttribute(element, 'data-orig-title'); return hasSavedOriginalTitle ? savedOriginalTitle : currentTitle; }, async: function (expr) { if (window.setImmediate) { window.setImmediate(expr); } else { window.setTimeout(expr, 0); } }, forEach: function (object, callback) { if (kv.utils.isArray(object)) { return forEach(object, callback); } for (var prop in object) { if (object.hasOwnProperty(prop)) { callback(object[prop], prop); } } } }; }());;var api = (function () { var isInitialized = 0, configuration = kv.configuration, utils = kv.utils; function cleanUpSubscriptions(context) { forEach(context.subscriptions, function (subscription) { subscription.dispose(); }); context.subscriptions = []; } function dispose(context) { if (context.options.deep) { forEach(context.flagged, function (obj) { delete obj.__kv_traversed; }); context.flagged.length = 0; } if (!context.options.live) { cleanUpSubscriptions(context); } } function runTraversal(obj, context) { context.validatables = []; cleanUpSubscriptions(context); traverseGraph(obj, context); dispose(context); } function traverseGraph(obj, context, level) { var objValues = [], val = obj.peek ? obj.peek() : obj; if (obj.__kv_traversed === true) { return; } if (context.options.deep) { obj.__kv_traversed = true; context.flagged.push(obj); } //default level value depends on deep option. level = (level !== undefined ? level : context.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 }); } context.validatables.push(obj); if(context.options.live && utils.isObservableArray(obj)) { context.subscriptions.push(obj.subscribe(function () { context.graphMonitor.valueHasMutated(); })); } } //get list of values either from array or object but ignore non-objects // and destroyed objects if (val && !val._destroy) { 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) { utils.forEach(objValues, function (observable) { //but not falsy things and not HTML Elements if (observable && !observable.nodeType) { traverseGraph(observable, context, level + 1); } }); } } function collectErrors(array) { var errors = []; forEach(array, function (observable) { if (!observable.isValid()) { errors.push(observable.error()); } }); return errors; } return { //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; extend(configuration, options); if (configuration.registerExtenders) { kv.registerExtenders(); } isInitialized = 1; }, // backwards compatability configure: function (options) { kv.init(options); }, // resets the config back to its original state reset: kv.configuration.reset, // 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 options = extend(extend({}, configuration.grouping), options); var context = { options: options, graphMonitor: ko.observable(), flagged: [], subscriptions: [], validatables: [] }; var result = null; //if using observables then traverse structure once and add observables if (options.observable) { runTraversal(obj, context); result = ko.computed(function () { context.graphMonitor(); //register dependency runTraversal(obj, context); return collectErrors(context.validatables); }); } else { //if not using observables then every call to error() should traverse the structure result = function () { runTraversal(obj, context); return collectErrors(context.validatables); }; } result.showAllMessages = function (show) { // thanks @heliosPortal if (show === undefined) {//default to true show = true; } // ensure we have latest changes result(); forEach(context.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(); invalidAndModifiedPresent = !!koUtils.arrayFirst(context.validatables, function (observable) { return !observable.isValid() && observable.isModified(); }); return invalidAndModifiedPresent; }; return result; }, formatMessage: function (message, params, observable) { if (typeof (message) === 'function') { return message(params, observable); } return message.replace(/\{0\}/gi, unwrap(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: kv.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 'kv.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) { if (ruleObj['message'] === undefined) { ruleObj['message'] = 'Error'; } //make sure onlyIf is honoured if (ruleObj.onlyIf) { ruleObj.condition = ruleObj.onlyIf; } //add the anonymous rule to the observable kv.addRule(observable, ruleObj); }, 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 && (params.message || params.onlyIf)) { //if it has a message or condition object, then its an object literal to use return kv.addRule(observable, { rule: ruleName, message: params.message, params: utils.isEmptyVal(params.params) ? true : params.params, condition: params.onlyIf }); } else { return kv.addRule(observable, { rule: ruleName, params: params }); } }; }, // loops through all kv.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 kv.rules) { if (kv.rules.hasOwnProperty(ruleName)) { if (!ko.extenders[ruleName]) { kv.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) { forEach(kv.configuration.html5Attributes, function (attr) { if (utils.hasAttribute(element, attr)) { var params = element.getAttribute(attr) || true; if (attr === 'min' || attr === 'max') { // If we're validating based on the min and max attributes, we'll // need to know what the 'type' attribute is set to var typeAttr = element.getAttribute('type'); if (typeof typeAttr === "undefined" || !typeAttr) { // From http://www.w3.org/TR/html-markup/input: // An input element with no type attribute specified represents the // same thing as an input element with its type attribute set to "text". typeAttr = "text"; } params = {typeAttr: typeAttr, value: params}; } kv.addRule(valueAccessor(), { rule: attr, params: params }); } }); var currentType = element.getAttribute('type'); forEach(kv.configuration.html5InputTypes, function (type) { if (type === currentType) { kv.addRule(valueAccessor(), { rule: (type === 'date') ? 'dateISO' : type, params: 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 forEach(kv.configuration.html5Attributes, function (attr) { var params; var ctx = koUtils.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; }, //take an existing binding handler and make it cause automatic validations makeBindingHandlerValidatable: function (handlerName) { var init = ko.bindingHandlers[handlerName].init; ko.bindingHandlers[handlerName].init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext); return ko.bindingHandlers['validationCore'].init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext); }; }, // visit an objects properties and apply validation rules from a definition setRules: function (target, definition) { var setRules = function (target, definition) { if (!target || !definition) { return; } for (var prop in definition) { if (!definition.hasOwnProperty(prop)) { continue; } var ruleDefinitions = definition[prop]; //check the target property exists and has a value if (!target[prop]) { continue; } var targetValue = target[prop], unwrappedTargetValue = unwrap(targetValue), rules = {}, nonRules = {}; for (var rule in ruleDefinitions) { if (!ruleDefinitions.hasOwnProperty(rule)) { continue; } if (kv.rules[rule]) { rules[rule] = ruleDefinitions[rule]; } else { nonRules[rule] = ruleDefinitions[rule]; } } //apply rules if (ko.isObservable(targetValue)) { targetValue.extend(rules); } //then apply child rules //if it's an array, apply rules to all children if (unwrappedTargetValue && utils.isArray(unwrappedTargetValue)) { for (var i = 0; i < unwrappedTargetValue.length; i++) { setRules(unwrappedTargetValue[i], nonRules); } //otherwise, just apply to this property } else { setRules(unwrappedTargetValue, nonRules); } } }; setRules(target, definition); } }; }()); // expose api publicly extend(ko.validation, api);;//Validation Rules: // You can view and override messages or rules via: // kv.rules[ruleName] // // To implement a custom Rule, simply use this template: // kv.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: // kv.rules['mustEqual'] = { // validator: function( val, mustEqualVal ){ // return val === mustEqualVal; // }, // message: 'This field must equal {0}' // }; // kv.rules = {}; kv.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, ''); } if (!required) {// if they passed: { required: false }, then don't require this return true; } return ((testVal + '').length > 0); }, message: 'This field is required.' }; function minMaxValidatorFactory(validatorName) { var isMaxValidation = validatorName === "max"; return function (val, options) { if (kv.utils.isEmptyVal(val)) { return true; } var comparisonValue, type; if (options.typeAttr === undefined) { // This validator is being called from javascript rather than // being bound from markup type = "text"; comparisonValue = options; } else { type = options.typeAttr; comparisonValue = options.value; } // From http://www.w3.org/TR/2012/WD-html5-20121025/common-input-element-attributes.html#attr-input-min, // if the value is parseable to a number, then the minimum should be numeric if (!isNaN(comparisonValue)) { type = "number"; } var regex, valMatches, comparisonValueMatches; switch (type.toLowerCase()) { case "week": regex = /^(\d{4})-W(\d{2})$/; valMatches = val.match(regex); if (valMatches === null) { throw "Invalid value for " + validatorName + " attribute for week input. Should look like " + "'2000-W33' http://www.w3.org/TR/html-markup/input.week.html#input.week.attrs.min"; } comparisonValueMatches = comparisonValue.match(regex); // If no regex matches were found, validation fails if (!comparisonValueMatches) { return false; } if (isMaxValidation) { return (valMatches[1] < comparisonValueMatches[1]) || // older year // same year, older week ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] <= comparisonValueMatches[2])); } else { return (valMatches[1] > comparisonValueMatches[1]) || // newer year // same year, newer week ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] >= comparisonValueMatches[2])); } break; case "month": regex = /^(\d{4})-(\d{2})$/; valMatches = val.match(regex); if (valMatches === null) { throw "Invalid value for " + validatorName + " attribute for month input. Should look like " + "'2000-03' http://www.w3.org/TR/html-markup/input.month.html#input.month.attrs.min"; } comparisonValueMatches = comparisonValue.match(regex); // If no regex matches were found, validation fails if (!comparisonValueMatches) { return false; } if (isMaxValidation) { return ((valMatches[1] < comparisonValueMatches[1]) || // older year // same year, older month ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] <= comparisonValueMatches[2]))); } else { return (valMatches[1] > comparisonValueMatches[1]) || // newer year // same year, newer month ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] >= comparisonValueMatches[2])); } break; case "number": case "range": if (isMaxValidation) { return (!isNaN(val) && parseFloat(val) <= parseFloat(comparisonValue)); } else { return (!isNaN(val) && parseFloat(val) >= parseFloat(comparisonValue)); } break; default: if (isMaxValidation) { return val <= comparisonValue; } else { return val >= comparisonValue; } } }; } kv.rules['min'] = { validator: minMaxValidatorFactory("min"), message: 'Please enter a value greater than or equal to {0}.' }; kv.rules['max'] = { validator: minMaxValidatorFactory("max"), message: 'Please enter a value less than or equal to {0}.' }; kv.rules['minLength'] = { validator: function (val, minLength) { return kv.utils.isEmptyVal(val) || val.length >= minLength; }, message: 'Please enter at least {0} characters.' }; kv.rules['maxLength'] = { validator: function (val, maxLength) { return kv.utils.isEmptyVal(val) || val.length <= maxLength; }, message: 'Please enter no more than {0} characters.' }; kv.rules['pattern'] = { validator: function (val, regex) { return kv.utils.isEmptyVal(val) || val.toString().match(regex) !== null; }, message: 'Please check this value.' }; kv.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 if (kv.utils.isEmptyVal(val) || step === 'any') { return true; } var dif = (val * 100) % (step * 100); return Math.abs(dif) < 0.00001 || Math.abs(1 - dif) < 0.00001; }, message: 'The value must increment by {0}' }; kv.rules['email'] = { validator: function (val, validate) { if (!validate) { return true; } //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 kv.utils.isEmptyVal(val) || ( // jquery validate regex - thanks Scott Gonzalez 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' }; kv.rules['date'] = { validator: function (value, validate) { if (!validate) { return true; } return kv.utils.isEmptyVal(value) || (validate && !/Invalid|NaN/.test(new Date(value))); }, message: 'Please enter a proper date' }; kv.rules['dateISO'] = { validator: function (value, validate) { if (!validate) { return true; } return kv.utils.isEmptyVal(value) || (validate && /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value)); }, message: 'Please enter a proper date' }; kv.rules['number'] = { validator: function (value, validate) { if (!validate) { return true; } return kv.utils.isEmptyVal(value) || (validate && /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)); }, message: 'Please enter a number' }; kv.rules['digit'] = { validator: function (value, validate) { if (!validate) { return true; } return kv.utils.isEmptyVal(value) || (validate && /^\d+$/.test(value)); }, message: 'Please enter a digit' }; kv.rules['phoneUS'] = { validator: function (phoneNumber, validate) { if (!validate) { return true; } if (kv.utils.isEmptyVal(phoneNumber)) { return true; } // makes it optional, use 'required' rule if it should be required 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' }; kv.rules['equal'] = { validator: function (val, params) { var otherValue = params; return val === kv.utils.getValue(otherValue); }, message: 'Values must equal' }; kv.rules['notEqual'] = { validator: function (val, params) { var otherValue = params; return val !== kv.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 kv.rules['unique'] = { validator: function (val, options) { var c = kv.utils.getValue(options.collection), external = kv.utils.getValue(options.externalValue), counter = 0; if (!val || !c) { return true; } koUtils.arrayFilter(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 ? 1 : 2); }, message: 'Please make sure the value is unique.' }; //now register all of these! (function () { kv.registerExtenders(); }()); ;// 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 = kv.utils.getConfigOptions(element); var observable = valueAccessor(); // parse html5 input validation attributes, optional feature if (config.parseInputAttributes) { kv.utils.async(function () { kv.parseInputValidationAttributes(element, valueAccessor); }); } // if requested insert message element and apply bindings if (config.insertMessages && kv.utils.isValidatable(observable)) { // insert the <span></span> var validationMessageElement = kv.insertValidationMessage(element); // if we're told to use a template, make sure that gets rendered if (config.messageTemplate) { ko.renderTemplate(config.messageTemplate, { field: observable }, null, validationMessageElement, 'replaceNode'); } else { ko.applyBindingsToNode(validationMessageElement, { validationMessage: observable }); } } // write the html5 attributes if indicated by the config if (config.writeInputAttributes && kv.utils.isValidatable(observable)) { kv.writeInputValidationAttributes(element, valueAccessor); } // if requested, add binding to decorate element if (config.decorateInputElement && kv.utils.isValidatable(observable)) { ko.applyBindingsToNode(element, { validationElement: observable }); } } }; }()); // override for KO's default 'value' and 'checked' bindings kv.makeBindingHandlerValidatable("value"); kv.makeBindingHandlerValidatable("checked"); ko.bindingHandlers['validationMessage'] = { // individual error message, if modified or post binding update: function (element, valueAccessor) { var obsv = valueAccessor(), config = kv.utils.getConfigOptions(element), val = unwrap(obsv), msg = null, isModified = false, isValid = false; if (!obsv.isValid || !obsv.isModified) { throw new Error("Observable is not validatable"); } isModified = obsv.isModified(); isValid = obsv.isValid(); var error = null; if (!config.messagesOnModified || isModified) { error = isValid ? null : obsv.error; } var isVisible = !config.messagesOnModified || isModified ? !isValid : false; var isCurrentlyVisible = element.style.display !== "none"; if (config.allowHtmlMessages) { koUtils.setHtml(element, error); } else { ko.bindingHandlers.text.update(element, function () { return error; }); } if (isCurrentlyVisible && !isVisible) { element.style.display = 'none'; } else if (!isCurrentlyVisible && isVisible) { element.style.display = ''; } } }; ko.bindingHandlers['validationElement'] = { update: function (element, valueAccessor, allBindingsAccessor) { var obsv = valueAccessor(), config = kv.utils.getConfigOptions(element), val = unwrap(obsv), msg = null, isModified = false, isValid = false; if (!obsv.isValid || !obsv.isModified) { throw new Error("Observable is not validatable"); } 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 = ((!config.decorateElementOnModified || isModified) ? !isValid : false); // css: { validationElement: false } css[config.errorElementClass] = shouldShow; return css; }; //add or remove class on the element; ko.bindingHandlers.css.update(element, cssSettingsAccessor, allBindingsAccessor); if (!config.errorsAsTitle) { return; } ko.bindingHandlers.attr.update(element, function () { var hasModification = !config.errorsAsTitleOnModified || isModified, title = kv.utils.getOriginalElementTitle(element); if (hasModification && !isValid) { return { title: obsv.error, 'data-orig-title': title }; } else if (!hasModification || isValid) { return { title: title, 'data-orig-title': null }; } }); } }; // 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 = unwrap(valueAccessor()); if (options) { var newConfig = extend({}, kv.configuration); extend(newConfig, options); //store the validation options on the node so we can retrieve it later kv.utils.setDomData(element, newConfig); } } }; }()); ;// 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 forEach(kv.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 kv.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, options) { if (!kv.utils.isObject(options)) { options = { enable: options }; } if (!('enable' in options)) { options.enable = true; } if (options.enable && !kv.utils.isValidatable(observable)) { var config = kv.configuration.validate || {}; var validationOptions = { throttleEvaluation : options.throttle || config.throttle }; observable.error = ko.observable(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); // a semi-protected observable observable.isValid = ko.computed(observable.__valid__); //manually set error state observable.setError = function (error) { observable.error(error); observable.__valid__(false); }; //manually clear error state observable.clearError = function () { observable.error(null); observable.__valid__(true); return observable; }; //subscribe to changes in the observable var h_change = observable.subscribe(function () { observable.isModified(true); }); // we use a computed here to ensure that anytime a dependency changes, the // validation logic evaluates var h_obsValidationTrigger = ko.computed(extend({ read: function () { var obs = observable(), ruleContexts = observable.rules(); kv.validateObservable(observable); return true; } }, validationOptions)); extend(h_obsValidationTrigger, validationOptions); observable._disposeValidation = function () { //first dispose of the subscriptions observable.isValid.dispose(); observable.rules.removeAll(); if (observable.isModified.getSubscriptionsCount() > 0) { observable.isModified._subscriptions['change'] = []; } if (observable.isValidating.getSubscriptionsCount() > 0) { observable.isValidating._subscriptions['change'] = []; } if (observable.__valid__.getSubscriptionsCount() > 0) { 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 (options.enable === false && 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 : unwrap(ctx.params)))) { // default param is true, eg. required = true //not valid, so format the error message and stick it in the 'error' variable observable.setError(kv.formatMessage( ctx.message || rule.message, unwrap(ctx.params), observable)); 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(kv.formatMessage( msg || ctx.message || rule.message, unwrap(ctx.params), observable)); observable.__valid__(isValid); } // tell it that we're done observable.isValidating(false); }; //fire the validator and hand it the callback rule.validator(observable(), unwrap(ctx.params || true), callBack); } kv.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 = ctx.rule ? kv.rules[ctx.rule] : ctx; 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.clearError(); return true; }; ; //quick function to override rule messages kv.localize = function (msgTranslations) { var msg, rule; //loop the properties in the object and assign the msg to the rule for (rule in msgTranslations) { if (kv.rules.hasOwnProperty(rule)) { kv.rules[rule].message = msgTranslations[rule]; } } };;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]; } } kv.init(); if (config) { kv.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) { kv.init(); origApplyBindings(viewModel, rootNode); }; ko.validatedObservable = function (initialValue) { if (!kv.utils.isObject(initialValue)) { return ko.observable(initialValue).extend({ validatable: true }); } var obsv = ko.observable(initialValue); obsv.isValid = ko.observable(); obsv.errors = kv.group(initialValue); obsv.errors.subscribe(function (errors) { obsv.isValid(errors.length === 0); }); return obsv; }; ;}));