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源码

MVVM架构~knockoutjs系列之从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;

};

;}));
View Code

 返回目录

你可能感兴趣的:(validation)