Click here to Skip to main content
15,893,508 members
Articles / Web Development / HTML

Design and Develop a website using ASP.NET MVC 4, EF, Knockoutjs and Bootstrap : Part - 2

Rate me:
Please Sign up or sign in to vote.
4.92/5 (74 votes)
13 Jan 2013CPOL10 min read 218.8K   15.7K   168  
Design a website architecture that must be simple, easily understandable by any web designer using asp.net MVC, EF, Knockoutjs and Bootstrap
/*
*   Knockout Validation
*   Created By Eric M. Barnard (https://github.com/ericmbarnard)
*
*   Source: https://github.com/ericmbarnard/Knockout-Validation
*   MIT License: http://www.opensource.org/licenses/MIT
*/
(function () {

    if (typeof (ko) === undefined) { throw 'Knockout is required, please ensure it is loaded before loading this validation plug-in'; }

    var defaults = {
        registerExtenders: true,
        messagesOnModified: 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
        decorateElement: false,         // false 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
        grouping: {
            deep: false,        //by default grouping is shallow
            observable: true    //and using observables
        }
    };

    // make a copy  so we can use 'reset' later
    var configuration = ko.utils.extend({}, defaults);

    var html5Attributes = ['required', 'pattern', 'min', 'max', 'step'];

    var async = function (expr) {
        if (window.setImmediate) { window.setImmediate(expr); }
        else { window.setTimeout(expr, 0); }
    };

    //#region Utilities

    var utils = (function () {
        var seedId = new Date().getTime();

        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';
            },
            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;
            },
            isValidatable: function (o) {
                return 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 = utils.contextFor(element);

                return options || configuration;
            },
            setDomData: function (node, data) {
                var key = node[domDataKey];

                if (!key) {
                    node[domDataKey] = key = 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 = utils.getDomData(node);
                        if (context) return context;
                        if (node.parentNode) return utils.contextFor(node.parentNode);
                        break;
                }
                return undefined;
            },
            isEmptyVal: function (val) {
                if (val === undefined) {
                    return true;
                }
                if (val === null) {
                    return true;
                }
                if (val === "") {
                    return true;
                }
            }
        };
    } ());

    //#endregion

    //#region Public API
    ko.validation = (function () {

        var isInitialized = 0;

        return {
            utils: utils,

            //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;

                ko.utils.extend(configuration, options);

                if (configuration.registerExtenders) {
                    ko.validation.registerExtenders();
                }

                isInitialized = 1;
            },
            // backwards compatability
            configure: function (options) { ko.validation.init(options); },

            // resets the config back to its original state
            reset: function () { configuration = $.extend(configuration, defaults); },

            // 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
                var options = ko.utils.extend(configuration.grouping, options),
                validatables = ko.observableArray([]),
                result = null,

                //anonymous, immediate function to traverse objects hierarchically
                //if !options.deep then it will stop on top level
                traverse = function traverse(obj, level) {
                    var objValues = [],
                        val = ko.utils.unwrapObservable(obj);

                    //default level value depends on deep option.
                    level = (level !== undefined ? level : 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 });
                        validatables.push(obj);
                    }

                    //get list of values either from array or object but ignore non-objects
                    if (val) {
                        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) {
                        ko.utils.arrayForEach(objValues, function (observable) {

                            //but not falsy things and not HTML Elements
                            if (observable && !observable.nodeType) traverse(observable, level + 1);
                        });
                    }
                };

                //if using observables then traverse structure once and add observables
                if (options.observable) {

                    traverse(obj);

                    result = ko.computed(function () {
                        var errors = [];
                        ko.utils.arrayForEach(validatables(), function (observable) {
                            if (!observable.isValid()) {
                                errors.push(observable.error);
                            }
                        });
                        return errors;
                    });

                } else { //if not using observables then every call to error() should traverse the structure
                    result = function () {
                        var errors = [];
                        validatables([]); //clear validatables
                        traverse(obj); // and traverse tree again
                        ko.utils.arrayForEach(validatables(), function (observable) {
                            if (!observable.isValid()) {
                                errors.push(observable.error);
                            }
                        });
                        return errors;
                    };


                }

                result.showAllMessages = function (show) { // thanks @heliosPortal
                    if (show == undefined) //default to true
                        show = true;

                    // ensure we have latest changes
                    result();

                    ko.utils.arrayForEach(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();
                    
                    ko.utils.arrayForEach(validatables(), function (observable) {
                        if (!observable.isValid() && observable.isModified()) {
                            invalidAndModifiedPresent = true;
                        }
                    });
                    return invalidAndModifiedPresent;
                };

                return result;
            },

            formatMessage: function (message, params) {
                return message.replace(/\{0\}/gi, params);
            },

            // addRule:
            // This takes in a ko.observable and a Rule Context - which is just a rule name and params to supply to the validator
            // ie: ko.validation.addRule(myObservable, {
            //          rule: 'required',
            //          params: true
            //      });
            //
            addRule: function (observable, rule) {
                observable.extend({ validatable: true });

                //push a Rule Context to the observables local array of Rule Contexts
                observable.rules.push(rule);
                return observable;
            },

            // addAnonymousRule:
            // Anonymous Rules essentially have all the properties of a Rule, but are only specific for a certain property
            // and developers typically are wanting to add them on the fly or not register a rule with the 'ko.validation.rules' object
            //
            // Example:
            // var test = ko.observable('something').extend{(
            //      validation: {
            //          validator: function(val, someOtherVal){
            //              return true;
            //          },
            //          message: "Something must be really wrong!',
            //          params: true
            //      }
            //  )};
            addAnonymousRule: function (observable, ruleObj) {
                var ruleName = utils.newId();

                if ( ruleObj['message'] === undefined ) {
                    rulesObj['message'] = 'Error';
                }

                //Create an anonymous rule to reference
                ko.validation.rules[ruleName] = ruleObj;

                //add the anonymous rule to the observable
                ko.validation.addRule(observable, {
                    rule: ruleName,
                    params: ruleObj.params
                });
            },

            addExtender: function (ruleName) {
                ko.extenders[ruleName] = function (observable, params) {
                    //params can come in a few flavors
                    // 1. Just the params to be passed to the validator
                    // 2. An object containing the Message to be used and the Params to pass to the validator
                    // 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.message || params.onlyIf) { //if it has a message or condition object, then its an object literal to use
                        return ko.validation.addRule(observable, {
                            rule: ruleName,
                            message: params.message,
                            params: utils.isEmptyVal(params.params) ? true : params.params,
                            condition: params.onlyIf
                        });
                    } else {
                        return ko.validation.addRule(observable, {
                            rule: ruleName,
                            params: params
                        });
                    }
                };
            },

            // loops through all ko.validation.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 ko.validation.rules) {
                        if (ko.validation.rules.hasOwnProperty(ruleName)) {
                            if (!ko.extenders[ruleName]) {
                                ko.validation.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) {
                ko.utils.arrayForEach(html5Attributes, function (attr) {
                    if (utils.hasAttribute(element, attr)) {
                        ko.validation.addRule(valueAccessor(), {
                            rule: attr,
                            params: element.getAttribute(attr) || 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
                ko.utils.arrayForEach(html5Attributes, function (attr) {
                    var params;
                    var ctx = ko.utils.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;
            }
        };
    } ());
    //#endregion

    //#region Core Validation Rules

    //Validation Rules:
    // You can view and override messages or rules via:
    // ko.validation.rules[ruleName]
    //
    // To implement a custom Rule, simply use this template:
    // ko.validation.rules['<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:
    // ko.validation.rules['mustEqual'] = {
    //      validator: function( val, mustEqualVal ){
    //          return val === mustEqualVal;
    //      },
    //      message: 'This field must equal {0}'
    // };
    //
    ko.validation.rules = {};
    ko.validation.rules['required'] = {
        validator: function (val, required) {
            var stringTrimRegEx = /^\s+|\s+$/g,
                testVal;

            if (val === undefined || val === null) {
                return !required;
            }

            testVal = val;
            if (typeof (val) == "string") {
                testVal = val.replace(stringTrimRegEx, '');
            }

            return required && (testVal + '').length > 0;
        },
        message: 'This field is required.'
    };

    ko.validation.rules['min'] = {
        validator: function (val, min) {
            return utils.isEmptyVal(val) || val >= min;
        },
        message: 'Please enter a value greater than or equal to {0}.'
    };

    ko.validation.rules['max'] = {
        validator: function (val, max) {
            return utils.isEmptyVal(val) || val <= max;
        },
        message: 'Please enter a value less than or equal to {0}.'
    };

    ko.validation.rules['minLength'] = {
        validator: function (val, minLength) {
            return utils.isEmptyVal(val) || val.length >= minLength;
        },
        message: 'Please enter at least {0} characters.'
    };

    ko.validation.rules['maxLength'] = {
        validator: function (val, maxLength) {
            return utils.isEmptyVal(val) || val.length <= maxLength;
        },
        message: 'Please enter no more than {0} characters.'
    };

    ko.validation.rules['pattern'] = {
        validator: function (val, regex) {
            return utils.isEmptyVal(val) || val.match(regex) != null;
        },
        message: 'Please check this value.'
    };

    ko.validation.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
            return utils.isEmptyVal(val) || (val * 100) % (step * 100) === 0;
        },
        message: 'The value must increment by {0}'
    };

    ko.validation.rules['email'] = {
        validator: function (val, validate) {
            //I think an empty email address is also a valid entry
            //if one want's to enforce entry it should be done with 'required: true'
            return utils.isEmptyVal(val) || (
                validate && /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(val)
            );
        },
        message: 'Please enter a proper email address'
    };

    ko.validation.rules['date'] = {
        validator: function (value, validate) {
            return utils.isEmptyVal(value) || (validate && !/Invalid|NaN/.test(new Date(value)));
        },
        message: 'Please enter a proper date'
    };

    ko.validation.rules['dateISO'] = {
        validator: function (value, validate) {
            return utils.isEmptyVal(value) || (validate && /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(value));
        },
        message: 'Please enter a proper date'
    };

    ko.validation.rules['number'] = {
        validator: function (value, validate) {
            return utils.isEmptyVal(value) || (validate && /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(value));
        },
        message: 'Please enter a number'
    };

    ko.validation.rules['digit'] = {
        validator: function (value, validate) {
            return utils.isEmptyVal(value) || (validate && /^\d+$/.test(value));
        },
        message: 'Please enter a digit'
    };

    ko.validation.rules['phoneUS'] = {
        validator: function (phoneNumber, validate) {
            if (typeof (phoneNumber) !== 'string') { return false; }
            if (utils.isEmptyVal(phoneNumber)) { return true; } // makes it optional, use 'required' rule if it should be required
            phoneNumber = phoneNumber.replace(/\s+/g, "");
            return validate && phoneNumber.length > 9 && phoneNumber.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/);
        },
        message: 'Please specify a valid phone number'
    };

    ko.validation.rules['equal'] = {
        validator: function (val, params) {
            var otherValue = params;
            return val === utils.getValue(otherValue);
        },
        message: 'Values must equal'
    };

    ko.validation.rules['notEqual'] = {
        validator: function (val, params) {
            var otherValue = params;
            return val !== 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
    ko.validation.rules['unique'] = {
        validator: function (val, options) {
            var c = utils.getValue(options.collection),
                external = utils.getValue(options.externalValue),
                counter = 0;

            if (!val || !c) return true;

            ko.utils.arrayFilter(ko.utils.unwrapObservable(c), function (item) {
                if (val === (options.valueAccessor ? options.valueAccessor(item) : item)) counter++;
            });
            // if value is external even 1 same value in collection means the value is not unique
            return counter < (external !== undefined && val !== external ? 1 : 2);
        },
        message: 'Please make sure the value is unique.'
    };


    //now register all of these!
    (function () {
        ko.validation.registerExtenders();
    } ());

    //#endregion

    //#region Knockout Binding Handlers

    // 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 = utils.getConfigOptions(element);

                // parse html5 input validation attributes, optional feature
                if (config.parseInputAttributes) {
                    async(function () { ko.validation.parseInputValidationAttributes(element, valueAccessor) });
                }

                // if requested insert message element and apply bindings
                if (config.insertMessages && utils.isValidatable(valueAccessor())) {

                    // insert the <span></span>
                    var validationMessageElement = ko.validation.insertValidationMessage(element);

                    // if we're told to use a template, make sure that gets rendered
                    if (config.messageTemplate) {
                        ko.renderTemplate(config.messageTemplate, { field: valueAccessor() }, null, validationMessageElement, 'replaceNode');
                    } else {
                        ko.applyBindingsToNode(validationMessageElement, { validationMessage: valueAccessor() });
                    }
                }

                // write the html5 attributes if indicated by the config
                if (config.writeInputAttributes && utils.isValidatable(valueAccessor())) {

                    ko.validation.writeInputValidationAttributes(element, valueAccessor);
                }

                // if requested, add binding to decorate element
                if (config.decorateElement && utils.isValidatable(valueAccessor())) {
                    ko.applyBindingsToNode(element, { validationElement: valueAccessor() });
                }
            },

            update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                // hook for future extensibility
            }
        };

    }());

    // override for KO's default 'value' binding
    (function () {
        var init = ko.bindingHandlers['value'].init;

        ko.bindingHandlers['value'].init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {

            init(element, valueAccessor, allBindingsAccessor);

            return ko.bindingHandlers['validationCore'].init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
        };
    } ());


    ko.bindingHandlers['validationMessage'] = { // individual error message, if modified or post binding
        update: function (element, valueAccessor) {
            var obsv = valueAccessor(),
                config = utils.getConfigOptions(element),
                val = ko.utils.unwrapObservable(obsv),
                msg = null,
                isModified = false,
                isValid = false;
                
            obsv.extend({ validatable: true });

            isModified = obsv.isModified();
            isValid = obsv.isValid();
            
            // create a handler to correctly return an error message
            var errorMsgAccessor = function () {
                if (!config.messagesOnModified || isModified) {
                    return isValid ? null : obsv.error;
                } else {
                    return null;
                }
            };

            //toggle visibility on validation messages when validation hasn't been evaluated, or when the object isValid
            var visiblityAccessor = function () {
                return isModified ? !isValid : false;
            };

            ko.bindingHandlers.text.update(element, errorMsgAccessor);
            ko.bindingHandlers.visible.update(element, visiblityAccessor);
        }
    };

    ko.bindingHandlers['validationElement'] = {
        update: function (element, valueAccessor) {
            var obsv = valueAccessor(),
                config = utils.getConfigOptions(element),
                val = ko.utils.unwrapObservable(obsv),
                msg = null,
                isModified = false,
                isValid = false;

            obsv.extend({ validatable: true });

            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 = (isModified ? !isValid : false);

                if (!config.decorateElement) { shouldShow = false; }

                // css: { validationElement: false }
                css[config.errorElementClass] = shouldShow;

                return css;
            };

            //add or remove class on the element;
            ko.bindingHandlers.css.update(element, cssSettingsAccessor);
        }
    };

    // 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 = ko.utils.unwrapObservable(valueAccessor());
                if (options) {
                    var newConfig = ko.utils.extend({}, configuration);
                    ko.utils.extend(newConfig, options);

                    //store the validation options on the node so we can retrieve it later
                    utils.setDomData(element, newConfig);
                }
            }
        };
    } ());
    //#endregion

    //#region Knockout Extenders

    // Validation Extender:
    // This is for creating custom validation logic on the fly
    // Example:
    // var test = ko.observable('something').extend{(
    //      validation: {
    //          validator: function(val, someOtherVal){
    //              return true;
    //          },
    //          message: "Something must be really wrong!',
    //          params: true
    //      }
    //  )};
    ko.extenders['validation'] = function (observable, rules) { // allow single rule or array
        ko.utils.arrayForEach(utils.isArray(rules) ? rules : [rules], function (rule) {
            // the 'rule' being passed in here has no name to identify a core Rule,
            // so we add it as an anonymous rule
            // If the developer is wanting to use a core Rule, but use a different message see the 'addExtender' logic for examples
            ko.validation.addAnonymousRule(observable, rule);
        });
        return observable;
    };

    //This is the extender that makes a Knockout Observable also 'Validatable'
    //examples include:
    // 1. var test = ko.observable('something').extend({validatable: true});
    // this will ensure that the Observable object is setup properly to respond to rules
    //
    // 2. test.extend({validatable: false});
    // this will remove the validation properties from the Observable object should you need to do that.
    ko.extenders['validatable'] = function (observable, enable) {
        if (enable && !utils.isValidatable(observable)) {

            observable.error = null; // holds the error message, we only need one since we stop processing validators when one is invalid

            // observable.rules:
            // ObservableArray of Rule Contexts, where a Rule Context is simply the name of a rule and the params to supply to it
            //
            // Rule Context = { rule: '<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);

            // we use a computed here to ensure that anytime a dependency changes, the
            // validation logic evaluates
            var h_obsValidationTrigger = ko.computed(function () {
                var obs = observable(),
                    ruleContexts = observable.rules();

                ko.validation.validateObservable(observable);

                return true;
            });

            // a semi-protected observable
            observable.isValid = ko.computed(function () {
                return observable.__valid__();
            });

            //subscribe to changes in the observable
            var h_change = observable.subscribe(function () {
                observable.isModified(true);
            });

            observable._disposeValidation = function () {
                //first dispose of the subscriptions
                observable.isValid.dispose();
                observable.rules.removeAll();
                observable.isModified._subscriptions['change'] = [];
                observable.isValidating._subscriptions['change'] = [];
                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 (enable === false && utils.isValidatable(observable)) {

            if (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 : ctx.params)) { // default param is true, eg. required = true

            //not valid, so format the error message and stick it in the 'error' variable
            observable.error = ko.validation.formatMessage(ctx.message || rule.message, ctx.params);
            observable.__valid__(false);
            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 = ko.validation.formatMessage(msg || ctx.message || rule.message, ctx.params);
                observable.__valid__(isValid);
            }

            // tell it that we're done
            observable.isValidating(false);
        };

        //fire the validator and hand it the callback
        rule.validator(observable(), ctx.params || true, callBack);
    }

    ko.validation.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 = ko.validation.rules[ctx.rule];

            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.error = null;
        observable.__valid__(true);
        return true;
    };

    //#endregion

    //#region Validated Observable

    ko.validatedObservable = function (initialValue) {
        if (!ko.validation.utils.isObject(initialValue)) { return ko.observable(initialValue).extend({ validatable: true }); }

        var obsv = ko.observable(initialValue);
        obsv.errors = ko.validation.group(initialValue);
        obsv.isValid = ko.computed(function () {
            return obsv.errors().length === 0;
        });

        return obsv;
    };

    //#endregion

    //#region Localization

    //quick function to override rule messages
    ko.validation.localize = function (msgTranslations) {

        var msg, rule;

        //loop the properties in the object and assign the msg to the rule
        for (rule in msgTranslations) {
            if (ko.validation.rules.hasOwnProperty(rule)) {
                ko.validation.rules[rule].message = msgTranslations[rule];
            }
        }
    };
    //#endregion

    //#region ApplyBindings Added Functionality
    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];
            }
        }

        ko.validation.init();

        if (config) { ko.validation.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) {

        ko.validation.init();

        origApplyBindings(viewModel, rootNode);
    };

    //#endregion

})();

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions