/*=============================================================================
|
Author: Eric M. Barnard - @ericmbarnard
|
License: MIT (http://opensource.org/licenses/mit-license.php)
|
|
Description: Validation Library for KnockoutJS
|
Version: 2.0.1
|
===============================================================================
|
*/
|
/*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 new Error('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';
|
},
|
isNumber: function(o) {
|
return !isNaN(o);
|
},
|
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)) {
|
// ensure it's validatable but don't extend validatedObservable because it
|
// would overwrite isValid property.
|
if (!obj.errors && !utils.isValidatable(obj)) {
|
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 recursively 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 && (!ko.isComputed(observable) || observable.rules)) {
|
traverseGraph(observable, context, level + 1);
|
}
|
});
|
}
|
}
|
|
function collectErrors(array) {
|
var errors = [];
|
forEach(array, function (observable) {
|
// Do not collect validatedObservable errors
|
if (utils.isValidatable(observable) && !observable.isValid()) {
|
// Use peek because we don't want a dependency for 'error' property because it
|
// changes before 'isValid' does. (Issue #99)
|
errors.push(observable.error.peek());
|
}
|
});
|
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;
|
}
|
|
//because 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;
|
},
|
|
// resets the config back to its original state
|
reset: kv.configuration.reset,
|
|
// recursively 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) {
|
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;
|
}
|
|
result.forEach(function (observable) {
|
if (utils.isValidatable(observable)) {
|
observable.isModified(show);
|
}
|
});
|
};
|
|
result.isAnyMessageShown = function () {
|
var invalidAndModifiedPresent;
|
|
invalidAndModifiedPresent = !!result.find(function (observable) {
|
return utils.isValidatable(observable) && !observable.isValid() && observable.isModified();
|
});
|
return invalidAndModifiedPresent;
|
};
|
|
result.filter = function(predicate) {
|
predicate = predicate || function () { return true; };
|
// ensure we have latest changes
|
result();
|
|
return koUtils.arrayFilter(context.validatables, predicate);
|
};
|
|
result.find = function(predicate) {
|
predicate = predicate || function () { return true; };
|
// ensure we have latest changes
|
result();
|
|
return koUtils.arrayFirst(context.validatables, predicate);
|
};
|
|
result.forEach = function(callback) {
|
callback = callback || function () { };
|
// ensure we have latest changes
|
result();
|
|
forEach(context.validatables, callback);
|
};
|
|
result.map = function(mapping) {
|
mapping = mapping || function (item) { return item; };
|
// ensure we have latest changes
|
result();
|
|
return koUtils.arrayMap(context.validatables, mapping);
|
};
|
|
/**
|
* @private You should not rely on this method being here.
|
* It's a private method and it may change in the future.
|
*
|
* @description Updates the validated object and collects errors from it.
|
*/
|
result._updateState = function(newValue) {
|
if (!utils.isObject(newValue)) {
|
throw new Error('An object is required.');
|
}
|
obj = newValue;
|
if (options.observable) {
|
context.graphMonitor.valueHasMutated();
|
}
|
else {
|
runTraversal(newValue, context);
|
return collectErrors(context.validatables);
|
}
|
};
|
return result;
|
},
|
|
formatMessage: function (message, params, observable) {
|
if (utils.isObject(params) && params.typeAttr) {
|
params = params.value;
|
}
|
if (typeof (message) === 'function') {
|
return message(params, observable);
|
}
|
var replacements = unwrap(params) || [];
|
if (!utils.isArray(replacements)) {
|
replacements = [replacements];
|
}
|
return message.replace(/{(\d+)}/gi, function(match, index) {
|
if (typeof replacements[index] !== 'undefined') {
|
return replacements[index];
|
}
|
return match;
|
});
|
},
|
|
// 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 });
|
|
var hasRule = !!koUtils.arrayFirst(observable.rules(), function(item) {
|
return item.rule && item.rule === rule.rule;
|
});
|
|
if (!hasRule) {
|
//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 ctx = koUtils.arrayFirst(contexts, function (ctx) {
|
return ctx.rule && ctx.rule.toLowerCase() === attr.toLowerCase();
|
});
|
|
if (!ctx) {
|
return;
|
}
|
|
// we have a rule matching a validation attribute at this point
|
// so lets add it to the element along with the params
|
ko.computed({
|
read: function() {
|
var params = ko.unwrap(ctx.params);
|
|
// we have to do some special things for the pattern validation
|
if (ctx.rule === "pattern" && params instanceof RegExp) {
|
// we need the pure string representation of the RegExpr without the //gi stuff
|
params = params.source;
|
}
|
|
element.setAttribute(attr, params);
|
},
|
disposeWhenNodeIsRemoved: element
|
});
|
});
|
|
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 testVal;
|
|
if (val === undefined || val === null) {
|
return !required;
|
}
|
|
testVal = val;
|
if (typeof (val) === 'string') {
|
if (String.prototype.trim) {
|
testVal = val.trim();
|
}
|
else {
|
testVal = val.replace(/^\s+|\s+$/g, '');
|
}
|
}
|
|
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) && !(comparisonValue instanceof Date)) {
|
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 new Error("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 new Error("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) {
|
if(kv.utils.isEmptyVal(val)) { return true; }
|
var normalizedVal = kv.utils.isNumber(val) ? ('' + val) : val;
|
return normalizedVal.length >= minLength;
|
},
|
message: 'Please enter at least {0} characters.'
|
};
|
|
kv.rules['maxLength'] = {
|
validator: function (val, maxLength) {
|
if(kv.utils.isEmptyVal(val)) { return true; }
|
var normalizedVal = kv.utils.isNumber(val) ? ('' + val) : val;
|
return normalizedVal.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}[-/](?:0?[1-9]|1[012])[-/](?:0?[1-9]|[12][0-9]|3[01])$/.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', 'checked', 'textInput' and selectedOptions bindings
|
kv.makeBindingHandlerValidatable("value");
|
kv.makeBindingHandlerValidatable("checked");
|
if (ko.bindingHandlers.textInput) {
|
kv.makeBindingHandlerValidatable("textInput");
|
}
|
kv.makeBindingHandlerValidatable("selectedOptions");
|
|
|
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 === null || typeof obsv === 'undefined') {
|
throw new Error('Cannot bind validationMessage to undefined value. data-bind expression: ' +
|
element.getAttribute('data-bind'));
|
}
|
|
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 === null || typeof obsv === 'undefined') {
|
throw new Error('Cannot bind validationElement to undefined value. data-bind expression: ' +
|
element.getAttribute('data-bind'));
|
}
|
|
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 occurring
|
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) {
|
var previousError = observable.error.peek();
|
var previousIsValid = observable.__valid__.peek();
|
|
observable.error(error);
|
observable.__valid__(false);
|
|
if (previousError !== error && !previousIsValid) {
|
// if the observable was not valid before then isValid will not mutate,
|
// hence causing any grouping to not display the latest error.
|
observable.isValid.notifySubscribers();
|
}
|
};
|
|
//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;
|
};
|
;
|
var _locales = {};
|
var _currentLocale;
|
|
kv.defineLocale = function(name, values) {
|
if (name && values) {
|
_locales[name.toLowerCase()] = values;
|
return values;
|
}
|
return null;
|
};
|
|
kv.locale = function(name) {
|
if (name) {
|
name = name.toLowerCase();
|
|
if (_locales.hasOwnProperty(name)) {
|
kv.localize(_locales[name]);
|
_currentLocale = name;
|
}
|
else {
|
throw new Error('Localization ' + name + ' has not been loaded.');
|
}
|
}
|
return _currentLocale;
|
};
|
|
//quick function to override rule messages
|
kv.localize = function (msgTranslations) {
|
var rules = kv.rules;
|
|
//loop the properties in the object and assign the msg to the rule
|
for (var ruleName in msgTranslations) {
|
if (rules.hasOwnProperty(ruleName)) {
|
rules[ruleName].message = msgTranslations[ruleName];
|
}
|
}
|
};
|
|
// Populate default locale (this will make en-US.js somewhat redundant)
|
(function() {
|
var localeData = {};
|
var rules = kv.rules;
|
|
for (var ruleName in rules) {
|
if (rules.hasOwnProperty(ruleName)) {
|
localeData[ruleName] = rules[ruleName].message;
|
}
|
}
|
kv.defineLocale('en-us', localeData);
|
})();
|
|
// No need to invoke locale because the messages are already defined along with the rules for en-US
|
_currentLocale = 'en-us';
|
;/**
|
* Possible invocations:
|
* applyBindingsWithValidation(viewModel)
|
* applyBindingsWithValidation(viewModel, options)
|
* applyBindingsWithValidation(viewModel, rootNode)
|
* applyBindingsWithValidation(viewModel, rootNode, options)
|
*/
|
ko.applyBindingsWithValidation = function (viewModel, rootNode, options) {
|
var node = document.body,
|
config;
|
|
if (rootNode && rootNode.nodeType) {
|
node = rootNode;
|
config = options;
|
}
|
else {
|
config = rootNode;
|
}
|
|
kv.init();
|
|
if (config) {
|
config = extend(extend({}, kv.configuration), config);
|
kv.utils.setDomData(node, config);
|
}
|
|
ko.applyBindings(viewModel, node);
|
};
|
|
//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, options) {
|
if (!options && !kv.utils.isObject(initialValue)) {
|
return ko.observable(initialValue).extend({ validatable: true });
|
}
|
|
var obsv = ko.observable(initialValue);
|
obsv.errors = kv.group(kv.utils.isObject(initialValue) ? initialValue : {}, options);
|
obsv.isValid = ko.observable(obsv.errors().length === 0);
|
|
if (ko.isObservable(obsv.errors)) {
|
obsv.errors.subscribe(function(errors) {
|
obsv.isValid(errors.length === 0);
|
});
|
}
|
else {
|
ko.computed(obsv.errors).subscribe(function (errors) {
|
obsv.isValid(errors.length === 0);
|
});
|
}
|
|
obsv.subscribe(function(newValue) {
|
if (!kv.utils.isObject(newValue)) {
|
/*
|
* The validation group works on objects.
|
* Since the new value is a primitive (scalar, null or undefined) we need
|
* to create an empty object to pass along.
|
*/
|
newValue = {};
|
}
|
// Force the group to refresh
|
obsv.errors._updateState(newValue);
|
obsv.isValid(obsv.errors().length === 0);
|
});
|
|
return obsv;
|
};
|
;}));
|