Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

much improved validationOptions binding

  • Loading branch information...
commit e6988d0efa0e0e44d98d0d7d23324b0d0a7670aa 1 parent c428f5f
Eric Barnard ericmbarnard authored
681 Src/knockout.validation.js
View
@@ -32,6 +32,9 @@
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]';
@@ -62,36 +65,72 @@
},
newId: function () {
return seedId += 1;
- }
- };
- } ());
+ },
+ getConfigOptions: function (element) {
+ var options = utils.contextFor(element);
- //#endregion
+ return options || configuration;
+ },
+ setDomData: function (node, data) {
+ var key = node[domDataKey];
- ko.validation = {
+ if (!key) {
+ node[domDataKey] = key = utils.newId();
+ }
- //Call this on startup
- //any config can be overridden with the passed in options
- init: function (options) {
- //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;
+ domData[key] = data;
+ },
+ getDomData: function (node) {
+ var key = node[domDataKey];
- ko.utils.extend(configuration, options);
+ if (!key) {
+ return undefined;
+ }
- if (configuration.registerExtenders) {
- ko.validation.registerExtenders();
+ 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;
}
- },
- //backwards compatability
- configure: function (options) { ko.validation.init(options); },
+ };
+ } ());
+
+ //#endregion
+
+ ko.validation = (function () {
+ return {
+ utils: utils,
+
+ //Call this on startup
+ //any config can be overridden with the passed in options
+ init: function (options) {
+ //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();
+ }
+ },
+ //backwards compatability
+ configure: function (options) { ko.validation.init(options); },
- group: function group(obj, options) { // array of observables or viewModel
- var options = ko.utils.extend(configuration.grouping, options),
+ group: function group(obj, options) { // array of observables or viewModel
+ var options = ko.utils.extend(configuration.grouping, options),
validatables = [],
result = null,
@@ -119,200 +158,164 @@
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 (observable && !observable.nodeType) traverse(observable, level + 1);
});
}
};
-
- //if using observables then traverse structure once and add observables
- if (options.observable) {
- traverse(obj);
- result = ko.dependentObservable(function () {
- var errors = [];
- ko.utils.arrayForEach(validatables, function (observable) {
- if (!observable.isValid()) {
- errors.push(observable.error);
- }
- });
- return errors;
- });
-
- result.showAllMessages = function () {
- ko.utils.arrayForEach(validatables, function (observable) {
- observable.isModified(true);
- });
- };
- } 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 () {
- ko.utils.arrayForEach(validatables, function (observable) {
- observable.isModified(true);
- });
- };
-
- obj.errors = result;
- obj.isValid = function () {
- return obj.errors().length === 0;
- }
- }
- 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;
- },
+ //if using observables then traverse structure once and add observables
+ if (options.observable) {
+ traverse(obj);
+ result = ko.dependentObservable(function () {
+ var errors = [];
+ ko.utils.arrayForEach(validatables, function (observable) {
+ if (!observable.isValid()) {
+ errors.push(observable.error);
+ }
+ });
+ return errors;
+ });
- // 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'
- };
+ result.showAllMessages = function () {
+ ko.utils.arrayForEach(validatables, function (observable) {
+ observable.isModified(true);
+ });
+ };
+ } 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;
+ };
- //add the anonymous rule to the observable
- ko.validation.addRule(observable, {
- rule: ruleName,
- params: ruleObj.params
- });
- },
+ result.showAllMessages = function () {
+ ko.utils.arrayForEach(validatables, function (observable) {
+ observable.isModified(true);
+ });
+ };
- 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);
- }
+ obj.errors = result;
+ obj.isValid = function () {
+ return obj.errors().length === 0;
}
}
- }
- },
- 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
- });
- }
- });
- }
- };
-
- ko.validation.utils = utils;
+ return result;
+ },
- //setup the 'init' bindingHandler override where we inject validation messages
- (function () {
- var init = ko.bindingHandlers.value.init;
+ formatMessage: function (message, params) {
+ return message.replace('{0}', params);
+ },
- ko.bindingHandlers.value.init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ // 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 });
- init(element, valueAccessor, allBindingsAccessor);
+ //push a Rule Context to the observables local array of Rule Contexts
+ observable.rules.push(rule);
+ return observable;
+ },
- //if the bindingContext contains a $validation object, they must be using a validationOptions binding
- //TODO: when bound to anything other than INPUT binding context is null causing an error
- var config = ko.utils.extend({}, configuration);
- ko.utils.extend(config, bindingContext.$data.$validation); //$validation should be able to be undefined
+ // 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'
+ };
- // parse html5 input validation attributes, optional feature
- if (config.parseInputAttributes) {
- async(function () { ko.validation.parseInputValidationAttributes(element, valueAccessor) });
- }
+ //add the anonymous rule to the observable
+ ko.validation.addRule(observable, {
+ rule: ruleName,
+ params: ruleObj.params
+ });
+ },
- //if requested insert message element and apply bindings
- 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() });
+ 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);
+ }
+ }
+ }
}
- }
- //if requested add binding to decorate element
- if (config.decorateElement && utils.isValidatable(valueAccessor())) {
- ko.applyBindingsToNode(element, { validationElement: valueAccessor() });
+ },
+ 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
+ });
+ }
+ });
}
};
} ());
-
//#region Core Validation Rules
//Validation Rules:
@@ -389,17 +392,138 @@
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 (!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: '{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'
+ };
+
+ ko.validation.rules['equal'] = {
+ validator: function (val, params) {
+ var otherValue = params;
+ return val === otherValue;
+ },
+ message: 'values must equal'
+ };
+
+ ko.validation.rules['notEqual'] = {
+ validator: function (val, params) {
+ var otherValue = params;
+ return val !== 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.'
+ };
+
//#endregion
//#region Knockout Binding Handlers
+ //setup the 'init' bindingHandler override where we inject validation messages
+ (function () {
+ var init = ko.bindingHandlers.value.init;
+
+ ko.bindingHandlers.value.init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+
+ init(element, valueAccessor, allBindingsAccessor);
+
+ 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())) {
+ var validationMessageElement = ko.validation.insertValidationMessage(element);
+ if (config.messageTemplate) {
+ ko.renderTemplate(config.messageTemplate, { field: valueAccessor() }, null, validationMessageElement, 'replaceNode');
+ } else {
+ ko.applyBindingsToNode(validationMessageElement, { validationMessage: valueAccessor() });
+ }
+ }
+ //if requested add binding to decorate element
+ if (config.decorateElement && utils.isValidatable(valueAccessor())) {
+ ko.applyBindingsToNode(element, { validationElement: valueAccessor() });
+ }
+ };
+ } ());
+
ko.bindingHandlers['validationMessage'] = { // individual error message, if modified or post binding
update: function (element, valueAccessor) {
- var obsv = valueAccessor();
+ var obsv = valueAccessor(),
+ config = utils.getConfigOptions(element);
+
obsv.extend({ validatable: true });
var errorMsgAccessor = function () {
- if (!configuration.messagesOnModified || obsv.isModified()) {
+ if (!config.messagesOnModified || obsv.isModified()) {
return obsv.isValid() ? null : obsv.error;
} else {
return null;
@@ -419,11 +543,12 @@
ko.bindingHandlers['validationElement'] = {
update: function (element, valueAccessor) {
var obsv = valueAccessor();
- obsv.extend({ validatable: true });
+ obsv.extend({ validatable: true }),
+ config = utils.getConfigOptions(element);
var cssSettingsAccessor = function () {
var result = {};
- result[configuration.errorElementClass] = !obsv.isValid();
+ result[config.errorElementClass] = !obsv.isValid();
return result;
};
//add or remove class on the element;
@@ -439,26 +564,20 @@
// <input type="text" data-bind="value: someValue"/>
// <input type="text" data-bind="value: someValue2"/>
// </div>
- ko.bindingHandlers['validationOptions'] = {
- makeValueAccessor: function (valueAccessor, bindingContext) {
- return function () {
- var validationAddIn = { $validation: valueAccessor() };
- return ko.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);
- }
- };
+ 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
@@ -544,99 +663,7 @@
//#endregion
- //#region Additional Rules
-
- 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 (!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: '{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'
- };
-
- ko.validation.rules['equal'] = {
- validator: function (val, params) {
- var otherValue = params;
- return val === otherValue;
- },
- message: 'values must equal'
- };
-
- ko.validation.rules['notEqual'] = {
- validator: function (val, params) {
- var otherValue = params;
- return val !== 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.'
- };
-
- //#endregion
-
- //#region validatedObservable
+ //#region Validated Observable
ko.validatedObservable = function (initialValue) {
if (!ko.validation.utils.isObject(initialValue)) { return ko.observable(initialValue).extend({ validatable: true }); }
@@ -650,10 +677,30 @@
return obsv;
};
+ //#endregion
+
+ //#region ApplyBindingsWithValidation
ko.applyBindingsWithValidation = function (viewModel, rootNode, options) {
- //TODO: support variable number of parameters to imitate ko.applyBindings
- ko.validation.init(options);
- ko.validation.group(viewModel);
+ 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);
};
//#endregion
141 Tests/test-runner.htm
View
@@ -3,6 +3,7 @@
<head>
<link href="Qunit/qunit.css" rel="stylesheet" type="text/css" />
<script src="Qunit/qunit.js" type="text/javascript"></script>
+ <!--We only include jQuery for testing purposes (testing UI changes)-->
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="../Lib/knockout-latest.debug.js" type="text/javascript"></script>
<script src="../Src/knockout.validation.js" type="text/javascript"></script>
@@ -33,80 +34,84 @@ <h2 id="qunit-banner"></h2>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup, will be hidden</div>
+ <div id="testContainer">
+
+ </div>
+ <div id="workbench">
+ <script type="text/javascript">
+ ko.validation.rules.pattern.message = 'Invalid.';
- <script type="text/javascript">
- ko.validation.rules.pattern.message = 'Invalid.';
-
- var captcha = function (val) {
- return val == 11;
- };
+ var captcha = function (val) {
+ return val == 11;
+ };
- var mustEqual = function (val, other) {
- return val == other;
- };
+ var mustEqual = function (val, other) {
+ return val == other;
+ };
- var viewModel = {
- firstName: ko.observable().extend({ minLength: 2, maxLength: 10 }),
- lastName: ko.observable().extend({ required: true }),
- emailAddress: ko.observable().extend({ // custom message
- required: { message: 'Please supply your email address.' }
- }),
- age: ko.observable().extend({ min: 1, max: 100 }),
- location: ko.observable(),
- subscriptionOptions: ['Technology', 'Music'],
- subscription: ko.observable().extend({ required: true }),
- password: ko.observable(),
- captcha: ko.observable().extend({ // custom Anonymous validator
- validation: { validator: captcha, message: 'Please check.' }
- }),
- submit: function () {
- if (viewModel.errors().length == 0) {
- alert('Thank you.');
- } else {
- alert('Please check your submission.');
- viewModel.errors.showAllMessages();
+ var viewModel = {
+ firstName: ko.observable().extend({ minLength: 2, maxLength: 10 }),
+ lastName: ko.observable().extend({ required: true }),
+ emailAddress: ko.observable().extend({ // custom message
+ required: { message: 'Please supply your email address.' }
+ }),
+ age: ko.observable().extend({ min: 1, max: 100 }),
+ location: ko.observable(),
+ subscriptionOptions: ['Technology', 'Music'],
+ subscription: ko.observable().extend({ required: true }),
+ password: ko.observable(),
+ captcha: ko.observable().extend({ // custom Anonymous validator
+ validation: { validator: captcha, message: 'Please check.' }
+ }),
+ submit: function () {
+ if (viewModel.errors().length == 0) {
+ alert('Thank you.');
+ } else {
+ alert('Please check your submission.');
+ viewModel.errors.showAllMessages();
+ }
}
- }
- };
+ };
- viewModel.confirmPassword = ko.observable().extend({
- validation: { validator: mustEqual, message: 'Passwords do not match.', params: viewModel.password }
- }),
+ viewModel.confirmPassword = ko.observable().extend({
+ validation: { validator: mustEqual, message: 'Passwords do not match.', params: viewModel.password }
+ }),
- viewModel.errors = ko.validation.group(viewModel);
+ viewModel.errors = ko.validation.group(viewModel);
- viewModel.requireLocation = function () {
- viewModel.location.extend({ required: true });
- };
- </script>
- <script id="customMessageTemplate" type="text/html">
- <em class="customMessage" data-bind='validationMessage: field'></em>
- </script>
- <fieldset>
- <legend>User: <span id="errorCount" data-bind='text: errors().length'></span> errors</legend>
- <label>First name: <input id="firstNameTxt" data-bind='value: firstName'/></label>
- <label>Last name: <input id="lastNameTxt" data-bind='value: lastName'/></label>
- <div data-bind='validationOptions: { messageTemplate: "customMessageTemplate" }'>
- <label>Email: <input id="emailAddressTxt" data-bind='value: emailAddress' required pattern="@"/></label>
- <label>Location: <input id="locationTxt" data-bind='value: location'/></label>
- <label>Age: <input id="testAgeInput" data-bind='value: age' required/></label>
- </div>
- <label>
- Subscriptions:
- <select data-bind='value: subscription, options: subscriptionOptions, optionsCaption: "Choose one..."'></select>
- </label>
- <label>Password: <input data-bind='value: password' type="password"/></label>
- <label>Retype password: <input data-bind='value: confirmPassword' type="password"/></label>
- <label>10 + 1 = <input data-bind='value: captcha'/></label>
- </fieldset>
- <button type="button" data-bind='click: submit'>Submit</button>
- <br />
- <br />
- <button type="button" data-bind='click: requireLocation'>Make 'Location' required</button>
- <script type="text/javascript">
- window.onload = function () {
- ko.applyBindings(viewModel);
- };
- </script>
+ viewModel.requireLocation = function () {
+ viewModel.location.extend({ required: true });
+ };
+ </script>
+ <script id="customMessageTemplate" type="text/html">
+ <em class="customMessage" data-bind='validationMessage: field'></em>
+ </script>
+ <fieldset>
+ <legend>User: <span id="errorCount" data-bind='text: errors().length'></span> errors</legend>
+ <label>First name: <input id="firstNameTxt" data-bind='value: firstName'/></label>
+ <label>Last name: <input id="lastNameTxt" data-bind='value: lastName'/></label>
+ <div data-bind='validationOptions: { messageTemplate: "customMessageTemplate" }'>
+ <label>Email: <input id="emailAddressTxt" data-bind='value: emailAddress' required pattern="@"/></label>
+ <label>Location: <input id="locationTxt" data-bind='value: location'/></label>
+ <label>Age: <input id="testAgeInput" data-bind='value: age' required/></label>
+ </div>
+ <label>
+ Subscriptions:
+ <select data-bind='value: subscription, options: subscriptionOptions, optionsCaption: "Choose one..."'></select>
+ </label>
+ <label>Password: <input data-bind='value: password' type="password"/></label>
+ <label>Retype password: <input data-bind='value: confirmPassword' type="password"/></label>
+ <label>10 + 1 = <input data-bind='value: captcha'/></label>
+ </fieldset>
+ <button type="button" data-bind='click: submit'>Submit</button>
+ <br />
+ <br />
+ <button type="button" data-bind='click: requireLocation'>Make 'Location' required</button>
+ <script type="text/javascript">
+ $(function () {
+ ko.applyBindings(viewModel, $('#workbench')[0]);
+ });
+ </script>
+ </div>
</body>
</html>
17 Tests/validation-tests.js
View
@@ -552,16 +552,21 @@ test('Object is NOT Valid and isValid returns False', function () {
});
//#endregion
-module('Utils Tests');
-test('hasAttribute works in old IE', function () {
- var el = document.getElementById('testAgeInput');
+//#region Utils Tests
+module('Grouping Tests');
- ok(el, 'found element');
+test('Error Grouping works', function () {
+ var vm = {
+ firstName: ko.observable().extend({ required: true }),
+ lastName: ko.observable().extend({ minLength: 2 })
+ };
+
+ var errors = ko.validation.group(vm);
- ok(ko.validation.utils.hasAttribute(el, 'required'), 'element correctly has html5 input attribute');
- ok(!ko.validation.utils.hasAttribute(el, 'pattern'), 'element correctly does not have html5 input attribute');
+ equals(errors().length, 2, 'Grouping correctly finds 2 invalid properties');
});
+//endregion
//#region validatedObservable
module('validatedObservable Tests');
174 Tests/validation-ui-tests.js
View
@@ -3,26 +3,178 @@
/// <reference path="../Src/knockout.validation.js" />
/// <reference path="Qunit/qunit.js" />
-module('UI Tests');
+module('UI Tests', {
+ setup: function () {
-test('Error Grouping works', function () {
+ },
+ teardown: function () {
+ ko.cleanNode($('#testContainer')[0]);
+ $('#testContainer').empty();
+ }
+});
+
+//utility functions
+var applyTestBindings = function (vm) {
+ ko.applyBindingsWithValidation(vm, $('#testContainer')[0]);
+};
+
+var addTestHtml = function(html){
+ $('#testContainer').html(html);
+};
+
+test('hasAttribute works in old IE', function () {
+
+ addTestHtml('<input id="myTestInput" type="text" required />');
- var errorCount = $('#errorCount').text();
+ var el = document.getElementById('myTestInput');
- equals(errorCount, '6', 'Correct Error Count');
+ ok(el, 'found element');
+
+ ok(ko.validation.utils.hasAttribute(el, 'required'), 'element correctly has html5 input attribute');
+ ok(!ko.validation.utils.hasAttribute(el, 'pattern'), 'element correctly does not have html5 input attribute');
});
+//#region Inserting Messages
+
test('Inserting Messages Works', function () {
- var $firstName = $('#firstNameTxt');
- $firstName.val('a'); //has a minLength of 2
- $firstName.change(); //trigger change event
+ addTestHtml('<input id="myTestInput" data-bind="value: firstName" type="text" />');
+
+ var vm = {
+ firstName: ko.observable('').extend({ required: true })
+ };
+
+ applyTestBindings(vm);
+
+ var $testInput = $('#myTestInput');
+
+ $testInput.val("a"); //set it
+ $testInput.change(); //trigger change event
+
+ $testInput.val(""); //set it
+ $testInput.change(); //trigger change event
+
+ var isValid = vm.firstName.isValid();
+
+ ok(!isValid, 'First Name is NOT Valid');
- var isValid = window.viewModel.firstName.isValid();
+ var msg = $testInput.siblings().first().text();
+
+ equals(msg, 'This field is required.', msg);
+});
+
+//#endregion
+
+//#region Validation Option Tests
+
+test('Validation Options - Basic Tests', function () {
+
+ var testHtml = '<div data-bind="validationOptions: { insertMessages: false }"><input type="text" id="myTestInput" data-bind="value: firstName" /></div>';
+
+ addTestHtml(testHtml);
+
+ var vm = {
+ firstName: ko.observable('').extend({ required: true })
+ };
+
+ applyTestBindings(vm);
+
+ var $testInput = $('#myTestInput');
+
+ $testInput.val("a"); //set it
+ $testInput.change(); //trigger change event
+
+ $testInput.val(""); //set it
+ $testInput.change(); //trigger change event
+
+ var isValid = vm.firstName.isValid();
ok(!isValid, 'First Name is NOT Valid');
- var msg = $firstName.siblings().first().text();
+ var noMsgs = $testInput.siblings().length;
+
+ equals(noMsgs, 0, 'No Messages were inserted');
+
+});
+
+test('Validation Options - Nested Test', function () {
+
+ var testHtml = '<div data-bind="validationOptions: { insertMessages: false }">' +
+ '<input type="text" id="myTestInput" data-bind="value: firstName" />' +
+ '<div data-bind="with: someObj">' +
+ '<input id="myLastName" type="text" data-bind="value: lastName" />' +
+ '</div>' +
+ '</div>';
+
+ addTestHtml(testHtml);
+
+ var vm = {
+ firstName: ko.observable('').extend({ required: true }),
+ someObj: {
+ lastName: ko.observable().extend({ minLength : 2 })
+ }
+ };
+
+ applyTestBindings(vm);
+
+ var $testInput = $('#myLastName');
+
+ $testInput.val("a"); //set it
+ $testInput.change(); //trigger change event
+
+ var isValid = vm.someObj.lastName.isValid();
+
+ ok(!isValid, 'Last Name is NOT Valid');
+
+ var noMsgs = $testInput.siblings().length;
+
+ equals(noMsgs, 0, 'No Messages were inserted');
+
+});
+
+test('Validation Options - Options only apply to their HTML Contexts', function () {
- equals(msg, 'Please enter at least 2 characters.', msg);
-});
+ var testHtml = '<div >' +
+ '<div data-bind="validationOptions: { insertMessages: false }">' +
+ '<div data-bind="with: someObj">' +
+ '<input id="myLastName" type="text" data-bind="value: lastName" />' +
+ '</div>' +
+ '</div>' +
+ '<input type="text" id="myFirstName" data-bind="value: firstName" />' +
+ '</div>';
+
+ addTestHtml(testHtml);
+
+ var vm = {
+ firstName: ko.observable('a').extend({ required: true }),
+ someObj: {
+ lastName: ko.observable().extend({ minLength: 2 })
+ }
+ };
+
+ applyTestBindings(vm);
+
+ var $testInput = $('#myLastName');
+
+ $testInput.val("a"); //set it
+ $testInput.change(); //trigger change event
+
+ var isValid = vm.someObj.lastName.isValid();
+
+ ok(!isValid, 'Last Name is NOT Valid');
+
+ var noMsgs = $testInput.siblings().length;
+
+ equals(noMsgs, 0, 'No Messages were inserted');
+
+ var $firstName = $('#myFirstName');
+ $firstName.val(""); //set it
+ $firstName.change(); //trigger change event
+
+ ok(!vm.firstName.isValid(), 'Validation Still works correctly');
+
+ var insertMsgCt = $firstName.siblings('span').length;
+ equals(insertMsgCt, 1, 'Should have inserted 1 message beside the first name!');
+
+});
+//#endregion
Please sign in to comment.
Something went wrong with that request. Please try again.