diff --git a/dist/backbone-validation-amd-min.js b/dist/backbone-validation-amd-min.js index 6b59c18c..6671447f 100644 --- a/dist/backbone-validation-amd-min.js +++ b/dist/backbone-validation-amd-min.js @@ -5,4 +5,4 @@ // // Documentation and full license available at: // http://thedersen.com/projects/backbone-validation -!function(a){"object"==typeof exports?module.exports=a(require("backbone"),require("underscore")):"function"==typeof define&&define.amd&&define(["backbone","underscore"],a)}(function(a,b){return a.Validation=function(b){"use strict";var c={forceUpdate:!1,selector:"name",labelFormatter:"sentenceCase",valid:Function.prototype,invalid:Function.prototype},d={formatLabel:function(a,b){return j[c.labelFormatter](a,b)},format:function(){var a=Array.prototype.slice.call(arguments),b=a.shift();return b.replace(/\{(\d+)\}/g,function(b,c){return"undefined"!=typeof a[c]?a[c]:b})}},e=function(c,d,f){return d=d||{},f=f||"",b.each(c,function(b,g){c.hasOwnProperty(g)&&(!b||"object"!=typeof b||b instanceof Array||b instanceof Date||b instanceof RegExp||b instanceof a.Model||b instanceof a.Collection?d[f+g]=b:e(b,d,f+g+"."))}),d},f=function(){var a=function(a){return b.reduce(b.keys(b.result(a,"validation")||{}),function(a,b){return a[b]=void 0,a},{})},f=function(a,c){var d=a.validation?b.result(a,"validation")[c]||{}:{};return(b.isFunction(d)||b.isString(d))&&(d={fn:d}),b.isArray(d)||(d=[d]),b.reduce(d,function(a,c){return b.each(b.without(b.keys(c),"msg"),function(b){a.push({fn:k[b],val:c[b],msg:c.msg})}),a},[])},h=function(a,c,e,g){return b.reduce(f(a,c),function(f,h){var i=b.extend({},d,k),j=h.fn.call(i,e,c,h.val,a,g);return j===!1||f===!1?!1:j&&!f?b.result(h,"msg")||j:f},"")},i=function(a,c){var d,f={},g=!0,i=b.clone(c),j=e(c);return b.each(j,function(b,c){d=h(a,c,b,i),d&&(f[c]=d,g=!1)}),{invalidAttrs:f,isValid:g}},j=function(c,d){return{preValidate:function(a,c){var d,e=this,f={};return b.isObject(a)?(b.each(a,function(a,b){d=e.preValidate(b,a),d&&(f[b]=d)}),b.isEmpty(f)?void 0:f):h(this,a,c,b.extend({},this.attributes))},isValid:function(a){var c=e(this.attributes);return b.isString(a)?!h(this,a,c[a],b.extend({},this.attributes)):b.isArray(a)?b.reduce(a,function(a,d){return a&&!h(this,d,c[d],b.extend({},this.attributes))},!0,this):(a===!0&&this.validate(),this.validation?this._isValid:!0)},validate:function(f,g){var h=this,j=!f,k=b.extend({},d,g),l=a(h),m=b.extend({},l,h.attributes,f),n=e(f||m),o=i(h,m);return h._isValid=o.isValid,b.each(l,function(a,b){var d=o.invalidAttrs.hasOwnProperty(b);d||k.valid(c,b,k.selector)}),b.each(l,function(a,b){var d=o.invalidAttrs.hasOwnProperty(b),e=n.hasOwnProperty(b);d&&(e||j)&&k.invalid(c,b,o.invalidAttrs[b],k.selector)}),b.defer(function(){h.trigger("validated",h._isValid,h,o.invalidAttrs),h.trigger("validated:"+(h._isValid?"valid":"invalid"),h,o.invalidAttrs)}),!k.forceUpdate&&b.intersection(b.keys(o.invalidAttrs),b.keys(n)).length>0?o.invalidAttrs:void 0}}},l=function(a,c,d){b.extend(c,j(a,d))},m=function(a){delete a.validate,delete a.preValidate,delete a.isValid},n=function(a){l(this.view,a,this.options)},o=function(a){m(a)};return{version:"0.9.1",configure:function(a){b.extend(c,a)},bind:function(a,d){d=b.extend({},c,g,d);var e=d.model||a.model,f=d.collection||a.collection;if("undefined"==typeof e&&"undefined"==typeof f)throw"Before you execute the binding your view must have a model or a collection.\nSee http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.";e?l(a,e,d):f&&(f.each(function(b){l(a,b,d)}),f.bind("add",n,{view:a,options:d}),f.bind("remove",o))},unbind:function(a,c){c=b.extend({},c);var d=c.model||a.model,e=c.collection||a.collection;d?m(d):e&&(e.each(function(a){m(a)}),e.unbind("add",n),e.unbind("remove",o))},mixin:j(null,c)}}(),g=f.callbacks={valid:function(a,b,c){a.$("["+c+'~="'+b+'"]').removeClass("invalid").removeAttr("data-error")},invalid:function(a,b,c,d){a.$("["+d+'~="'+b+'"]').addClass("invalid").attr("data-error",c)}},h=f.patterns={digits:/^\d+$/,number:/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,email:/^((([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,url:/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([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])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i},i=f.messages={required:"{0} is required",acceptance:"{0} must be accepted",min:"{0} must be greater than or equal to {1}",max:"{0} must be less than or equal to {1}",range:"{0} must be between {1} and {2}",length:"{0} must be {1} characters",minLength:"{0} must be at least {1} characters",maxLength:"{0} must be at most {1} characters",rangeLength:"{0} must be between {1} and {2} characters",oneOf:"{0} must be one of: {1}",equalTo:"{0} must be the same as {1}",digits:"{0} must only contain digits",number:"{0} must be a number",email:"{0} must be a valid email",url:"{0} must be a valid url",inlinePattern:"{0} is invalid"},j=f.labelFormatters={none:function(a){return a},sentenceCase:function(a){return a.replace(/(?:^\w|[A-Z]|\b\w)/g,function(a,b){return 0===b?a.toUpperCase():" "+a.toLowerCase()}).replace(/_/g," ")},label:function(a,b){return b.labels&&b.labels[a]||j.sentenceCase(a,b)}},k=f.validators=function(){var a=String.prototype.trim?function(a){return null===a?"":String.prototype.trim.call(a)}:function(a){var b=/^\s+/,c=/\s+$/;return null===a?"":a.toString().replace(b,"").replace(c,"")},c=function(a){return b.isNumber(a)||b.isString(a)&&a.match(h.number)},d=function(c){return!(b.isNull(c)||b.isUndefined(c)||b.isString(c)&&""===a(c)||b.isArray(c)&&b.isEmpty(c))};return{fn:function(a,c,d,e,f){return b.isString(d)&&(d=e[d]),d.call(e,a,c,f)},required:function(a,c,e,f,g){var h=b.isFunction(e)?e.call(f,a,c,g):e;return h||d(a)?h&&!d(a)?this.format(i.required,this.formatLabel(c,f)):void 0:!1},acceptance:function(a,c,d,e){return"true"===a||b.isBoolean(a)&&a!==!1?void 0:this.format(i.acceptance,this.formatLabel(c,e))},min:function(a,b,d,e){return!c(a)||d>a?this.format(i.min,this.formatLabel(b,e),d):void 0},max:function(a,b,d,e){return!c(a)||a>d?this.format(i.max,this.formatLabel(b,e),d):void 0},range:function(a,b,d,e){return!c(a)||ad[1]?this.format(i.range,this.formatLabel(b,e),d[0],d[1]):void 0},length:function(a,c,d,e){return b.isString(a)&&a.length===d?void 0:this.format(i.length,this.formatLabel(c,e),d)},minLength:function(a,c,d,e){return!b.isString(a)||a.lengthd?this.format(i.maxLength,this.formatLabel(c,e),d):void 0},rangeLength:function(a,c,d,e){return!b.isString(a)||a.lengthd[1]?this.format(i.rangeLength,this.formatLabel(c,e),d[0],d[1]):void 0},oneOf:function(a,c,d,e){return b.include(d,a)?void 0:this.format(i.oneOf,this.formatLabel(c,e),d.join(", "))},equalTo:function(a,b,c,d,e){return a!==e[c]?this.format(i.equalTo,this.formatLabel(b,d),this.formatLabel(c,d)):void 0},pattern:function(a,b,c,e){return d(a)&&a.toString().match(h[c]||c)?void 0:this.format(i[c]||i.inlinePattern,this.formatLabel(b,e),c)}}}();return b.each(k,function(a,c){k[c]=b.bind(k[c],b.extend({},d,k))}),f}(b),a.Validation}); \ No newline at end of file +!function(a){"object"==typeof exports?module.exports=a(require("backbone"),require("underscore")):"function"==typeof define&&define.amd&&define(["backbone","underscore"],a)}(function(a,b){return a.Validation=function(b){"use strict";var c={forceUpdate:!1,selector:"name",labelFormatter:"sentenceCase",valid:Function.prototype,invalid:Function.prototype},d={formatLabel:function(a,b){return j[c.labelFormatter](a,b)},format:function(){var a=Array.prototype.slice.call(arguments),b=a.shift();return b.replace(/\{(\d+)\}/g,function(b,c){return"undefined"!=typeof a[c]?a[c]:b})}},e=function(c,d,f){return d=d||{},f=f||"",b.each(c,function(b,g){c.hasOwnProperty(g)&&(b&&"object"==typeof b&&!(b instanceof Array||b instanceof Date||b instanceof RegExp||b instanceof a.Model||b instanceof a.Collection)?e(b,d,f+g+"."):d[f+g]=b)}),d},f=function(){var a=function(a){return b.reduce(b.keys(b.result(a,"validation")||{}),function(a,b){return a[b]=void 0,a},{})},f=function(a,c){var d=a.validation?b.result(a,"validation")[c]||{}:{};return(b.isFunction(d)||b.isString(d))&&(d={fn:d}),b.isArray(d)||(d=[d]),b.reduce(d,function(a,c){return b.each(b.without(b.keys(c),"msg"),function(b){a.push({fn:k[b],val:c[b],msg:c.msg})}),a},[])},h=function(a,c,e,g){return b.reduce(f(a,c),function(f,h){var i=b.extend({},d,k),j=h.fn.call(i,e,c,h.val,a,g);return j===!1||f===!1?!1:j&&!f?b.result(h,"msg")||j:f},"")},i=function(a,c){var d,f={},g=!0,i=b.clone(c),j=e(c);return b.each(j,function(b,c){d=h(a,c,b,i),d&&(f[c]=d,g=!1)}),{invalidAttrs:f,isValid:g}},j=function(c,d){return{preValidate:function(a,c){var d,e=this,f={};return b.isObject(a)?(b.each(a,function(a,b){d=e.preValidate(b,a),d&&(f[b]=d)}),b.isEmpty(f)?void 0:f):h(this,a,c,b.extend({},this.attributes))},isValid:function(a){var c=e(this.attributes);return b.isString(a)?!h(this,a,c[a],b.extend({},this.attributes)):b.isArray(a)?b.reduce(a,function(a,d){return a&&!h(this,d,c[d],b.extend({},this.attributes))},!0,this):(a===!0&&this.validate(),this.validation?this._isValid:!0)},validate:function(f,g){var h=this,j=!f,k=b.extend({},d,g),l=a(h),m=b.extend({},l,h.attributes,f),n=e(f||m),o=i(h,m);return h._isValid=o.isValid,b.each(l,function(a,b){var d=o.invalidAttrs.hasOwnProperty(b);d||k.valid(c,b,k.selector)}),b.each(l,function(a,b){var d=o.invalidAttrs.hasOwnProperty(b),e=n.hasOwnProperty(b);d&&(e||j)&&k.invalid(c,b,o.invalidAttrs[b],k.selector)}),b.defer(function(){h.trigger("validated",h._isValid,h,o.invalidAttrs),h.trigger("validated:"+(h._isValid?"valid":"invalid"),h,o.invalidAttrs)}),!k.forceUpdate&&b.intersection(b.keys(o.invalidAttrs),b.keys(n)).length>0?o.invalidAttrs:void 0}}},l=function(a,c,d){b.extend(c,j(a,d))},m=function(a){delete a.validate,delete a.preValidate,delete a.isValid},n=function(a){l(this.view,a,this.options)},o=function(a){m(a)};return{version:"0.9.1",configure:function(a){b.extend(c,a)},bind:function(a,d){d=b.extend({},c,g,d);var e=d.model||a.model,f=d.collection||a.collection;if("undefined"==typeof e&&"undefined"==typeof f)throw"Before you execute the binding your view must have a model or a collection.\nSee http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.";e?l(a,e,d):f&&(f.each(function(b){l(a,b,d)}),f.bind("add",n,{view:a,options:d}),f.bind("remove",o))},unbind:function(a,c){c=b.extend({},c);var d=c.model||a.model,e=c.collection||a.collection;d?m(d):e&&(e.each(function(a){m(a)}),e.unbind("add",n),e.unbind("remove",o))},mixin:j(null,c)}}(),g=f.callbacks={valid:function(a,b,c){a.$("["+c+'~="'+b+'"]').removeClass("invalid").removeAttr("data-error")},invalid:function(a,b,c,d){a.$("["+d+'~="'+b+'"]').addClass("invalid").attr("data-error",c)}},h=f.patterns={digits:/^\d+$/,number:/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,email:/^((([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,url:/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([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])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i},i=f.messages={required:"{0} is required",acceptance:"{0} must be accepted",min:"{0} must be greater than or equal to {1}",max:"{0} must be less than or equal to {1}",range:"{0} must be between {1} and {2}",length:"{0} must be {1} characters",minLength:"{0} must be at least {1} characters",maxLength:"{0} must be at most {1} characters",rangeLength:"{0} must be between {1} and {2} characters",oneOf:"{0} must be one of: {1}",equalTo:"{0} must be the same as {1}",digits:"{0} must only contain digits",number:"{0} must be a number",email:"{0} must be a valid email",url:"{0} must be a valid url",inlinePattern:"{0} is invalid",validModel:"{0} must be a validated model",validCollection:"{0} must be a validated collection"},j=f.labelFormatters={none:function(a){return a},sentenceCase:function(a){return a.replace(/(?:^\w|[A-Z]|\b\w)/g,function(a,b){return 0===b?a.toUpperCase():" "+a.toLowerCase()}).replace(/_/g," ")},label:function(a,b){return b.labels&&b.labels[a]||j.sentenceCase(a,b)}},k=f.validators=function(){var a=String.prototype.trim?function(a){return null===a?"":String.prototype.trim.call(a)}:function(a){var b=/^\s+/,c=/\s+$/;return null===a?"":a.toString().replace(b,"").replace(c,"")},c=function(a){return b.isNumber(a)||b.isString(a)&&a.match(h.number)},d=function(c){return!(b.isNull(c)||b.isUndefined(c)||b.isString(c)&&""===a(c)||b.isArray(c)&&b.isEmpty(c))};return{fn:function(a,c,d,e,f){return b.isString(d)&&(d=e[d]),d.call(e,a,c,f)},required:function(a,c,e,f,g){var h=b.isFunction(e)?e.call(f,a,c,g):e;return h||d(a)?h&&!d(a)?this.format(i.required,this.formatLabel(c,f)):void 0:!1},acceptance:function(a,c,d,e){return"true"===a||b.isBoolean(a)&&a!==!1?void 0:this.format(i.acceptance,this.formatLabel(c,e))},min:function(a,b,d,e){return!c(a)||d>a?this.format(i.min,this.formatLabel(b,e),d):void 0},max:function(a,b,d,e){return!c(a)||a>d?this.format(i.max,this.formatLabel(b,e),d):void 0},range:function(a,b,d,e){return!c(a)||ad[1]?this.format(i.range,this.formatLabel(b,e),d[0],d[1]):void 0},length:function(a,c,d,e){return b.isString(a)&&a.length===d?void 0:this.format(i.length,this.formatLabel(c,e),d)},minLength:function(a,c,d,e){return!b.isString(a)||a.lengthd?this.format(i.maxLength,this.formatLabel(c,e),d):void 0},rangeLength:function(a,c,d,e){return!b.isString(a)||a.lengthd[1]?this.format(i.rangeLength,this.formatLabel(c,e),d[0],d[1]):void 0},oneOf:function(a,c,d,e){return b.include(d,a)?void 0:this.format(i.oneOf,this.formatLabel(c,e),d.join(", "))},equalTo:function(a,b,c,d,e){return a!==e[c]?this.format(i.equalTo,this.formatLabel(b,d),this.formatLabel(c,d)):void 0},pattern:function(a,b,c,e){return d(a)&&a.toString().match(h[c]||c)?void 0:this.format(i[c]||i.inlinePattern,this.formatLabel(b,e),c)},validModel:function(a,b,c,d){return a&&!a.isValid(!0)?this.format(i.validModel,this.formatLabel(b,d)):void 0},validCollection:function(a,c,d,e){var f=a.map(function(a){return a.isValid(!0)});return-1!==b.indexOf(f,!1)?this.format(i.validCollection,this.formatLabel(c,e)):void 0}}}();return b.each(k,function(a,c){k[c]=b.bind(k[c],b.extend({},d,k))}),f}(b),a.Validation}); \ No newline at end of file diff --git a/dist/backbone-validation-amd.js b/dist/backbone-validation-amd.js index 31f041a1..34587aa2 100644 --- a/dist/backbone-validation-amd.js +++ b/dist/backbone-validation-amd.js @@ -434,7 +434,9 @@ number: '{0} must be a number', email: '{0} must be a valid email', url: '{0} must be a valid url', - inlinePattern: '{0} is invalid' + inlinePattern: '{0} is invalid', + validModel: '{0} must be a validated model', + validCollection: '{0} must be a validated collection' }; // Label formatters @@ -627,6 +629,26 @@ if (!hasValue(value) || !value.toString().match(defaultPatterns[pattern] || pattern)) { return this.format(defaultMessages[pattern] || defaultMessages.inlinePattern, this.formatLabel(attr, model), pattern); } + }, + + // Model validator + // Validates that a (nested) model is valid as defined by it's own validations + validModel: function (value, attr, customValue, model) { + if (value && !value.isValid(true)) { + return this.format(defaultMessages.validModel, this.formatLabel(attr, model)); + } + }, + + // Collection validator + // Validates that a (nested) collection of models is valid as defined by their own validations + validCollection: function (value, attr, customValue, model) { + var errors = value.map(function (entry) { + return entry.isValid(true); + }); + + if (_.indexOf(errors, false) !== -1) { + return this.format(defaultMessages.validCollection, this.formatLabel(attr, model)); + } } }; }()); diff --git a/dist/backbone-validation-min.js b/dist/backbone-validation-min.js index 4b62a7df..064424cd 100644 --- a/dist/backbone-validation-min.js +++ b/dist/backbone-validation-min.js @@ -5,4 +5,4 @@ // // Documentation and full license available at: // http://thedersen.com/projects/backbone-validation -Backbone.Validation=function(a){"use strict";var b={forceUpdate:!1,selector:"name",labelFormatter:"sentenceCase",valid:Function.prototype,invalid:Function.prototype},c={formatLabel:function(a,c){return i[b.labelFormatter](a,c)},format:function(){var a=Array.prototype.slice.call(arguments),b=a.shift();return b.replace(/\{(\d+)\}/g,function(b,c){return"undefined"!=typeof a[c]?a[c]:b})}},d=function(b,c,e){return c=c||{},e=e||"",a.each(b,function(a,f){b.hasOwnProperty(f)&&(!a||"object"!=typeof a||a instanceof Array||a instanceof Date||a instanceof RegExp||a instanceof Backbone.Model||a instanceof Backbone.Collection?c[e+f]=a:d(a,c,e+f+"."))}),c},e=function(){var e=function(b){return a.reduce(a.keys(a.result(b,"validation")||{}),function(a,b){return a[b]=void 0,a},{})},g=function(b,c){var d=b.validation?a.result(b,"validation")[c]||{}:{};return(a.isFunction(d)||a.isString(d))&&(d={fn:d}),a.isArray(d)||(d=[d]),a.reduce(d,function(b,c){return a.each(a.without(a.keys(c),"msg"),function(a){b.push({fn:j[a],val:c[a],msg:c.msg})}),b},[])},h=function(b,d,e,f){return a.reduce(g(b,d),function(g,h){var i=a.extend({},c,j),k=h.fn.call(i,e,d,h.val,b,f);return k===!1||g===!1?!1:k&&!g?a.result(h,"msg")||k:g},"")},i=function(b,c){var e,f={},g=!0,i=a.clone(c),j=d(c);return a.each(j,function(a,c){e=h(b,c,a,i),e&&(f[c]=e,g=!1)}),{invalidAttrs:f,isValid:g}},k=function(b,c){return{preValidate:function(b,c){var d,e=this,f={};return a.isObject(b)?(a.each(b,function(a,b){d=e.preValidate(b,a),d&&(f[b]=d)}),a.isEmpty(f)?void 0:f):h(this,b,c,a.extend({},this.attributes))},isValid:function(b){var c=d(this.attributes);return a.isString(b)?!h(this,b,c[b],a.extend({},this.attributes)):a.isArray(b)?a.reduce(b,function(b,d){return b&&!h(this,d,c[d],a.extend({},this.attributes))},!0,this):(b===!0&&this.validate(),this.validation?this._isValid:!0)},validate:function(f,g){var h=this,j=!f,k=a.extend({},c,g),l=e(h),m=a.extend({},l,h.attributes,f),n=d(f||m),o=i(h,m);return h._isValid=o.isValid,a.each(l,function(a,c){var d=o.invalidAttrs.hasOwnProperty(c);d||k.valid(b,c,k.selector)}),a.each(l,function(a,c){var d=o.invalidAttrs.hasOwnProperty(c),e=n.hasOwnProperty(c);d&&(e||j)&&k.invalid(b,c,o.invalidAttrs[c],k.selector)}),a.defer(function(){h.trigger("validated",h._isValid,h,o.invalidAttrs),h.trigger("validated:"+(h._isValid?"valid":"invalid"),h,o.invalidAttrs)}),!k.forceUpdate&&a.intersection(a.keys(o.invalidAttrs),a.keys(n)).length>0?o.invalidAttrs:void 0}}},l=function(b,c,d){a.extend(c,k(b,d))},m=function(a){delete a.validate,delete a.preValidate,delete a.isValid},n=function(a){l(this.view,a,this.options)},o=function(a){m(a)};return{version:"0.9.1",configure:function(c){a.extend(b,c)},bind:function(c,d){d=a.extend({},b,f,d);var e=d.model||c.model,g=d.collection||c.collection;if("undefined"==typeof e&&"undefined"==typeof g)throw"Before you execute the binding your view must have a model or a collection.\nSee http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.";e?l(c,e,d):g&&(g.each(function(a){l(c,a,d)}),g.bind("add",n,{view:c,options:d}),g.bind("remove",o))},unbind:function(b,c){c=a.extend({},c);var d=c.model||b.model,e=c.collection||b.collection;d?m(d):e&&(e.each(function(a){m(a)}),e.unbind("add",n),e.unbind("remove",o))},mixin:k(null,b)}}(),f=e.callbacks={valid:function(a,b,c){a.$("["+c+'~="'+b+'"]').removeClass("invalid").removeAttr("data-error")},invalid:function(a,b,c,d){a.$("["+d+'~="'+b+'"]').addClass("invalid").attr("data-error",c)}},g=e.patterns={digits:/^\d+$/,number:/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,email:/^((([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,url:/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([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])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i},h=e.messages={required:"{0} is required",acceptance:"{0} must be accepted",min:"{0} must be greater than or equal to {1}",max:"{0} must be less than or equal to {1}",range:"{0} must be between {1} and {2}",length:"{0} must be {1} characters",minLength:"{0} must be at least {1} characters",maxLength:"{0} must be at most {1} characters",rangeLength:"{0} must be between {1} and {2} characters",oneOf:"{0} must be one of: {1}",equalTo:"{0} must be the same as {1}",digits:"{0} must only contain digits",number:"{0} must be a number",email:"{0} must be a valid email",url:"{0} must be a valid url",inlinePattern:"{0} is invalid"},i=e.labelFormatters={none:function(a){return a},sentenceCase:function(a){return a.replace(/(?:^\w|[A-Z]|\b\w)/g,function(a,b){return 0===b?a.toUpperCase():" "+a.toLowerCase()}).replace(/_/g," ")},label:function(a,b){return b.labels&&b.labels[a]||i.sentenceCase(a,b)}},j=e.validators=function(){var b=String.prototype.trim?function(a){return null===a?"":String.prototype.trim.call(a)}:function(a){var b=/^\s+/,c=/\s+$/;return null===a?"":a.toString().replace(b,"").replace(c,"")},c=function(b){return a.isNumber(b)||a.isString(b)&&b.match(g.number)},d=function(c){return!(a.isNull(c)||a.isUndefined(c)||a.isString(c)&&""===b(c)||a.isArray(c)&&a.isEmpty(c))};return{fn:function(b,c,d,e,f){return a.isString(d)&&(d=e[d]),d.call(e,b,c,f)},required:function(b,c,e,f,g){var i=a.isFunction(e)?e.call(f,b,c,g):e;return i||d(b)?i&&!d(b)?this.format(h.required,this.formatLabel(c,f)):void 0:!1},acceptance:function(b,c,d,e){return"true"===b||a.isBoolean(b)&&b!==!1?void 0:this.format(h.acceptance,this.formatLabel(c,e))},min:function(a,b,d,e){return!c(a)||d>a?this.format(h.min,this.formatLabel(b,e),d):void 0},max:function(a,b,d,e){return!c(a)||a>d?this.format(h.max,this.formatLabel(b,e),d):void 0},range:function(a,b,d,e){return!c(a)||ad[1]?this.format(h.range,this.formatLabel(b,e),d[0],d[1]):void 0},length:function(b,c,d,e){return a.isString(b)&&b.length===d?void 0:this.format(h.length,this.formatLabel(c,e),d)},minLength:function(b,c,d,e){return!a.isString(b)||b.lengthd?this.format(h.maxLength,this.formatLabel(c,e),d):void 0},rangeLength:function(b,c,d,e){return!a.isString(b)||b.lengthd[1]?this.format(h.rangeLength,this.formatLabel(c,e),d[0],d[1]):void 0},oneOf:function(b,c,d,e){return a.include(d,b)?void 0:this.format(h.oneOf,this.formatLabel(c,e),d.join(", "))},equalTo:function(a,b,c,d,e){return a!==e[c]?this.format(h.equalTo,this.formatLabel(b,d),this.formatLabel(c,d)):void 0},pattern:function(a,b,c,e){return d(a)&&a.toString().match(g[c]||c)?void 0:this.format(h[c]||h.inlinePattern,this.formatLabel(b,e),c)}}}();return a.each(j,function(b,d){j[d]=a.bind(j[d],a.extend({},c,j))}),e}(_); \ No newline at end of file +Backbone.Validation=function(a){"use strict";var b={forceUpdate:!1,selector:"name",labelFormatter:"sentenceCase",valid:Function.prototype,invalid:Function.prototype},c={formatLabel:function(a,c){return i[b.labelFormatter](a,c)},format:function(){var a=Array.prototype.slice.call(arguments),b=a.shift();return b.replace(/\{(\d+)\}/g,function(b,c){return"undefined"!=typeof a[c]?a[c]:b})}},d=function(b,c,e){return c=c||{},e=e||"",a.each(b,function(a,f){b.hasOwnProperty(f)&&(a&&"object"==typeof a&&!(a instanceof Array||a instanceof Date||a instanceof RegExp||a instanceof Backbone.Model||a instanceof Backbone.Collection)?d(a,c,e+f+"."):c[e+f]=a)}),c},e=function(){var e=function(b){return a.reduce(a.keys(a.result(b,"validation")||{}),function(a,b){return a[b]=void 0,a},{})},g=function(b,c){var d=b.validation?a.result(b,"validation")[c]||{}:{};return(a.isFunction(d)||a.isString(d))&&(d={fn:d}),a.isArray(d)||(d=[d]),a.reduce(d,function(b,c){return a.each(a.without(a.keys(c),"msg"),function(a){b.push({fn:j[a],val:c[a],msg:c.msg})}),b},[])},h=function(b,d,e,f){return a.reduce(g(b,d),function(g,h){var i=a.extend({},c,j),k=h.fn.call(i,e,d,h.val,b,f);return k===!1||g===!1?!1:k&&!g?a.result(h,"msg")||k:g},"")},i=function(b,c){var e,f={},g=!0,i=a.clone(c),j=d(c);return a.each(j,function(a,c){e=h(b,c,a,i),e&&(f[c]=e,g=!1)}),{invalidAttrs:f,isValid:g}},k=function(b,c){return{preValidate:function(b,c){var d,e=this,f={};return a.isObject(b)?(a.each(b,function(a,b){d=e.preValidate(b,a),d&&(f[b]=d)}),a.isEmpty(f)?void 0:f):h(this,b,c,a.extend({},this.attributes))},isValid:function(b){var c=d(this.attributes);return a.isString(b)?!h(this,b,c[b],a.extend({},this.attributes)):a.isArray(b)?a.reduce(b,function(b,d){return b&&!h(this,d,c[d],a.extend({},this.attributes))},!0,this):(b===!0&&this.validate(),this.validation?this._isValid:!0)},validate:function(f,g){var h=this,j=!f,k=a.extend({},c,g),l=e(h),m=a.extend({},l,h.attributes,f),n=d(f||m),o=i(h,m);return h._isValid=o.isValid,a.each(l,function(a,c){var d=o.invalidAttrs.hasOwnProperty(c);d||k.valid(b,c,k.selector)}),a.each(l,function(a,c){var d=o.invalidAttrs.hasOwnProperty(c),e=n.hasOwnProperty(c);d&&(e||j)&&k.invalid(b,c,o.invalidAttrs[c],k.selector)}),a.defer(function(){h.trigger("validated",h._isValid,h,o.invalidAttrs),h.trigger("validated:"+(h._isValid?"valid":"invalid"),h,o.invalidAttrs)}),!k.forceUpdate&&a.intersection(a.keys(o.invalidAttrs),a.keys(n)).length>0?o.invalidAttrs:void 0}}},l=function(b,c,d){a.extend(c,k(b,d))},m=function(a){delete a.validate,delete a.preValidate,delete a.isValid},n=function(a){l(this.view,a,this.options)},o=function(a){m(a)};return{version:"0.9.1",configure:function(c){a.extend(b,c)},bind:function(c,d){d=a.extend({},b,f,d);var e=d.model||c.model,g=d.collection||c.collection;if("undefined"==typeof e&&"undefined"==typeof g)throw"Before you execute the binding your view must have a model or a collection.\nSee http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.";e?l(c,e,d):g&&(g.each(function(a){l(c,a,d)}),g.bind("add",n,{view:c,options:d}),g.bind("remove",o))},unbind:function(b,c){c=a.extend({},c);var d=c.model||b.model,e=c.collection||b.collection;d?m(d):e&&(e.each(function(a){m(a)}),e.unbind("add",n),e.unbind("remove",o))},mixin:k(null,b)}}(),f=e.callbacks={valid:function(a,b,c){a.$("["+c+'~="'+b+'"]').removeClass("invalid").removeAttr("data-error")},invalid:function(a,b,c,d){a.$("["+d+'~="'+b+'"]').addClass("invalid").attr("data-error",c)}},g=e.patterns={digits:/^\d+$/,number:/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,email:/^((([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,url:/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([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])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i},h=e.messages={required:"{0} is required",acceptance:"{0} must be accepted",min:"{0} must be greater than or equal to {1}",max:"{0} must be less than or equal to {1}",range:"{0} must be between {1} and {2}",length:"{0} must be {1} characters",minLength:"{0} must be at least {1} characters",maxLength:"{0} must be at most {1} characters",rangeLength:"{0} must be between {1} and {2} characters",oneOf:"{0} must be one of: {1}",equalTo:"{0} must be the same as {1}",digits:"{0} must only contain digits",number:"{0} must be a number",email:"{0} must be a valid email",url:"{0} must be a valid url",inlinePattern:"{0} is invalid",validModel:"{0} must be a validated model",validCollection:"{0} must be a validated collection"},i=e.labelFormatters={none:function(a){return a},sentenceCase:function(a){return a.replace(/(?:^\w|[A-Z]|\b\w)/g,function(a,b){return 0===b?a.toUpperCase():" "+a.toLowerCase()}).replace(/_/g," ")},label:function(a,b){return b.labels&&b.labels[a]||i.sentenceCase(a,b)}},j=e.validators=function(){var b=String.prototype.trim?function(a){return null===a?"":String.prototype.trim.call(a)}:function(a){var b=/^\s+/,c=/\s+$/;return null===a?"":a.toString().replace(b,"").replace(c,"")},c=function(b){return a.isNumber(b)||a.isString(b)&&b.match(g.number)},d=function(c){return!(a.isNull(c)||a.isUndefined(c)||a.isString(c)&&""===b(c)||a.isArray(c)&&a.isEmpty(c))};return{fn:function(b,c,d,e,f){return a.isString(d)&&(d=e[d]),d.call(e,b,c,f)},required:function(b,c,e,f,g){var i=a.isFunction(e)?e.call(f,b,c,g):e;return i||d(b)?i&&!d(b)?this.format(h.required,this.formatLabel(c,f)):void 0:!1},acceptance:function(b,c,d,e){return"true"===b||a.isBoolean(b)&&b!==!1?void 0:this.format(h.acceptance,this.formatLabel(c,e))},min:function(a,b,d,e){return!c(a)||d>a?this.format(h.min,this.formatLabel(b,e),d):void 0},max:function(a,b,d,e){return!c(a)||a>d?this.format(h.max,this.formatLabel(b,e),d):void 0},range:function(a,b,d,e){return!c(a)||ad[1]?this.format(h.range,this.formatLabel(b,e),d[0],d[1]):void 0},length:function(b,c,d,e){return a.isString(b)&&b.length===d?void 0:this.format(h.length,this.formatLabel(c,e),d)},minLength:function(b,c,d,e){return!a.isString(b)||b.lengthd?this.format(h.maxLength,this.formatLabel(c,e),d):void 0},rangeLength:function(b,c,d,e){return!a.isString(b)||b.lengthd[1]?this.format(h.rangeLength,this.formatLabel(c,e),d[0],d[1]):void 0},oneOf:function(b,c,d,e){return a.include(d,b)?void 0:this.format(h.oneOf,this.formatLabel(c,e),d.join(", "))},equalTo:function(a,b,c,d,e){return a!==e[c]?this.format(h.equalTo,this.formatLabel(b,d),this.formatLabel(c,d)):void 0},pattern:function(a,b,c,e){return d(a)&&a.toString().match(g[c]||c)?void 0:this.format(h[c]||h.inlinePattern,this.formatLabel(b,e),c)},validModel:function(a,b,c,d){return a&&!a.isValid(!0)?this.format(h.validModel,this.formatLabel(b,d)):void 0},validCollection:function(b,c,d,e){var f=b.map(function(a){return a.isValid(!0)});return-1!==a.indexOf(f,!1)?this.format(h.validCollection,this.formatLabel(c,e)):void 0}}}();return a.each(j,function(b,d){j[d]=a.bind(j[d],a.extend({},c,j))}),e}(_); \ No newline at end of file diff --git a/dist/backbone-validation.js b/dist/backbone-validation.js index 6b112dd1..54363066 100644 --- a/dist/backbone-validation.js +++ b/dist/backbone-validation.js @@ -427,7 +427,9 @@ Backbone.Validation = (function(_){ number: '{0} must be a number', email: '{0} must be a valid email', url: '{0} must be a valid url', - inlinePattern: '{0} is invalid' + inlinePattern: '{0} is invalid', + validModel: '{0} must be a validated model', + validCollection: '{0} must be a validated collection' }; // Label formatters @@ -620,6 +622,26 @@ Backbone.Validation = (function(_){ if (!hasValue(value) || !value.toString().match(defaultPatterns[pattern] || pattern)) { return this.format(defaultMessages[pattern] || defaultMessages.inlinePattern, this.formatLabel(attr, model), pattern); } + }, + + // Model validator + // Validates that a (nested) model is valid as defined by it's own validations + validModel: function (value, attr, customValue, model) { + if (value && !value.isValid(true)) { + return this.format(defaultMessages.validModel, this.formatLabel(attr, model)); + } + }, + + // Collection validator + // Validates that a (nested) collection of models is valid as defined by their own validations + validCollection: function (value, attr, customValue, model) { + var errors = value.map(function (entry) { + return entry.isValid(true); + }); + + if (_.indexOf(errors, false) !== -1) { + return this.format(defaultMessages.validCollection, this.formatLabel(attr, model)); + } } }; }()); diff --git a/docs/backbone-validation.html b/docs/backbone-validation.html index 2ca0d530..5d961477 100644 --- a/docs/backbone-validation.html +++ b/docs/backbone-validation.html @@ -46,7 +46,7 @@

backbone-validation.js

-
Backbone.Validation = (function(_){
+            
Backbone.Validation = (function(_){
   'use strict';
@@ -124,7 +124,7 @@

Helper functions

-
    formatLabel: function(attrName, model) {
+            
    formatLabel: function(attrName, model) {
       return defaultLabelFormatters[defaultOptions.labelFormatter](attrName, model);
     },
@@ -142,10 +142,10 @@

Helper functions

-
    format: function() {
+            
    format: function() {
       var args = Array.prototype.slice.call(arguments),
           text = args.shift();
-      return text.replace(/\{(\d+)\}/g, function(match, number) {
+      return text.replace(/\{(\d+)\}/g, function(match, number) {
         return typeof args[number] !== 'undefined' ? args[number] : match;
       });
     }
@@ -180,7 +180,7 @@ 

Helper functions

into = into || {}; prefix = prefix || ''; - _.each(obj, function(val, key) { + _.each(obj, function(val, key) { if(obj.hasOwnProperty(key)) { if (val && typeof val === 'object' && !( val instanceof Array || @@ -225,7 +225,7 @@

Validation

-
  var Validation = (function(){
+
  var Validation = (function(){
@@ -242,8 +242,8 @@

Validation

-
    var getValidatedAttrs = function(model) {
-      return _.reduce(_.keys(_.result(model, 'validation') || {}), function(memo, key) {
+            
    var getValidatedAttrs = function(model) {
+      return _.reduce(_.keys(_.result(model, 'validation') || {}), function(memo, key) {
         memo[key] = void 0;
         return memo;
       }, {});
@@ -264,7 +264,7 @@ 

Validation

-
    var getValidators = function(model, attr) {
+            
    var getValidators = function(model, attr) {
       var attrValidationSet = model.validation ? _.result(model, 'validation')[attr] || {} : {};
@@ -318,8 +318,8 @@

Validation

-
      return _.reduce(attrValidationSet, function(memo, attrValidation) {
-        _.each(_.without(_.keys(attrValidation), 'msg'), function(validator) {
+            
      return _.reduce(attrValidationSet, function(memo, attrValidation) {
+        _.each(_.without(_.keys(attrValidation), 'msg'), function(validator) {
           memo.push({
             fn: defaultValidators[validator],
             val: attrValidation[validator],
@@ -346,7 +346,7 @@ 

Validation

-
    var validateAttr = function(model, attr, value, computed) {
+
    var validateAttr = function(model, attr, value, computed) {
@@ -363,7 +363,7 @@

Validation

-
      return _.reduce(getValidators(model, attr), function(memo, validator){
+
      return _.reduce(getValidators(model, attr), function(memo, validator){
@@ -407,14 +407,14 @@

Validation

-
    var validateModel = function(model, attrs) {
+            
    var validateModel = function(model, attrs) {
       var error,
           invalidAttrs = {},
           isValid = true,
           computed = _.clone(attrs),
           flattened = flatten(attrs);
 
-      _.each(flattened, function(val, attr) {
+      _.each(flattened, function(val, attr) {
         error = validateAttr(model, attr, val, computed);
         if (error) {
           invalidAttrs[attr] = error;
@@ -441,7 +441,7 @@ 

Validation

-
    var mixin = function(view, options) {
+            
    var mixin = function(view, options) {
       return {
@@ -458,13 +458,13 @@

Validation

-
        preValidate: function(attr, value) {
+            
        preValidate: function(attr, value) {
           var self = this,
               result = {},
               error;
 
           if(_.isObject(attr)){
-            _.each(attr, function(value, key) {
+            _.each(attr, function(value, key) {
               error = self.preValidate(key, value);
               if(error){
                 result[key] = error;
@@ -493,14 +493,14 @@ 

Validation

-
        isValid: function(option) {
+            
        isValid: function(option) {
           var flattened = flatten(this.attributes);
 
           if(_.isString(option)){
             return !validateAttr(this, option, flattened[option], _.extend({}, this.attributes));
           }
           if(_.isArray(option)){
-            return _.reduce(option, function(memo, attr) {
+            return _.reduce(option, function(memo, attr) {
               return memo && !validateAttr(this, attr, flattened[attr], _.extend({}, this.attributes));
             }, true, this);
           }
@@ -525,7 +525,7 @@ 

Validation

-
        validate: function(attrs, setOptions){
+            
        validate: function(attrs, setOptions){
           var model = this,
               validateAll = !attrs,
               opt = _.extend({}, options, setOptions),
@@ -551,7 +551,7 @@ 

Validation

-
          _.each(validatedAttrs, function(val, attr){
+            
          _.each(validatedAttrs, function(val, attr){
             var invalid = result.invalidAttrs.hasOwnProperty(attr);
             if(!invalid){
               opt.valid(view, attr, opt.selector);
@@ -572,7 +572,7 @@ 

Validation

-
          _.each(validatedAttrs, function(val, attr){
+            
          _.each(validatedAttrs, function(val, attr){
             var invalid = result.invalidAttrs.hasOwnProperty(attr),
                 changed = changedAttrs.hasOwnProperty(attr);
 
@@ -596,7 +596,7 @@ 

Validation

-
          _.defer(function() {
+            
          _.defer(function() {
             model.trigger('validated', model._isValid, model, result.invalidAttrs);
             model.trigger('validated:' + (model._isValid ? 'valid' : 'invalid'), model, result.invalidAttrs);
           });
@@ -636,7 +636,7 @@

Validation

-
    var bindModel = function(view, model, options) {
+            
    var bindModel = function(view, model, options) {
       _.extend(model, mixin(view, options));
     };
@@ -653,7 +653,7 @@

Validation

-
    var unbindModel = function(model) {
+            
    var unbindModel = function(model) {
       delete model.validate;
       delete model.preValidate;
       delete model.isValid;
@@ -673,7 +673,7 @@ 

Validation

-
    var collectionAdd = function(model) {
+            
    var collectionAdd = function(model) {
       bindModel(this.view, model, this.options);
     };
@@ -691,7 +691,7 @@

Validation

-
    var collectionRemove = function(model) {
+            
    var collectionRemove = function(model) {
       unbindModel(model);
     };
@@ -738,7 +738,7 @@

Validation

-
      configure: function(options) {
+            
      configure: function(options) {
         _.extend(defaultOptions, options);
       },
@@ -756,7 +756,7 @@

Validation

-
      bind: function(view, options) {
+            
      bind: function(view, options) {
         options = _.extend({}, defaultOptions, defaultCallbacks, options);
 
         var model = options.model || view.model,
@@ -771,7 +771,7 @@ 

Validation

bindModel(view, model, options); } else if(collection) { - collection.each(function(model){ + collection.each(function(model){ bindModel(view, model, options); }); collection.bind('add', collectionAdd, {view: view, options: options}); @@ -793,7 +793,7 @@

Validation

-
      unbind: function(view, options) {
+            
      unbind: function(view, options) {
         options = _.extend({}, options);
         var model = options.model || view.model,
             collection = options.collection || view.collection;
@@ -802,7 +802,7 @@ 

Validation

unbindModel(model); } if(collection) { - collection.each(function(model){ + collection.each(function(model){ unbindModel(model); }); collection.unbind('add', collectionAdd); @@ -870,7 +870,7 @@

Callbacks

-
    valid: function(view, attr, selector) {
+            
    valid: function(view, attr, selector) {
       view.$('[' + selector + '~="' + attr + '"]')
           .removeClass('invalid')
           .removeAttr('data-error');
@@ -891,7 +891,7 @@ 

Callbacks

-
    invalid: function(view, attr, error, selector) {
+            
    invalid: function(view, attr, error, selector) {
       view.$('[' + selector + '~="' + attr + '"]')
           .addClass('invalid')
           .attr('data-error', error);
@@ -1079,7 +1079,7 @@ 

Label formatters

-
    none: function(attrName) {
+            
    none: function(attrName) {
       return attrName;
     },
@@ -1096,8 +1096,8 @@

Label formatters

-
    sentenceCase: function(attrName) {
-      return attrName.replace(/(?:^\w|[A-Z]|\b\w)/g, function(match, index) {
+            
    sentenceCase: function(attrName) {
+      return attrName.replace(/(?:^\w|[A-Z]|\b\w)/g, function(match, index) {
         return index === 0 ? match.toUpperCase() : ' ' + match.toLowerCase();
       }).replace(/_/g, ' ');
     },
@@ -1126,7 +1126,7 @@

Label formatters

-
    label: function(attrName, model) {
+            
    label: function(attrName, model) {
       return (model.labels && model.labels[attrName]) || defaultLabelFormatters.sentenceCase(attrName, model);
     }
   };
@@ -1156,7 +1156,7 @@

Built in validators

-
  var defaultValidators = Validation.validators = (function(){
+
  var defaultValidators = Validation.validators = (function(){
@@ -1172,10 +1172,10 @@

Built in validators

    var trim = String.prototype.trim ?
-      function(text) {
+      function(text) {
         return text === null ? '' : String.prototype.trim.call(text);
       } :
-      function(text) {
+      function(text) {
         var trimLeft = /^\s+/,
             trimRight = /\s+$/;
 
@@ -1195,7 +1195,7 @@ 

Built in validators

-
    var isNumber = function(value){
+            
    var isNumber = function(value){
       return _.isNumber(value) || (_.isString(value) && value.match(defaultPatterns.number));
     };
@@ -1212,7 +1212,7 @@

Built in validators

-
    var hasValue = function(value) {
+            
    var hasValue = function(value) {
       return !(_.isNull(value) || _.isUndefined(value) || (_.isString(value) && trim(value) === '') || (_.isArray(value) && _.isEmpty(value)));
     };
 
@@ -1232,7 +1232,7 @@ 

Built in validators

-
      fn: function(value, attr, fn, model, computed) {
+            
      fn: function(value, attr, fn, model, computed) {
         if(_.isString(fn)){
           fn = model[fn];
         }
@@ -1254,7 +1254,7 @@ 

Built in validators

-
      required: function(value, attr, required, model, computed) {
+            
      required: function(value, attr, required, model, computed) {
         var isRequired = _.isFunction(required) ? required.call(model, value, attr, computed) : required;
         if(!isRequired && !hasValue(value)) {
           return false; // overrides all other validators
@@ -1279,7 +1279,7 @@ 

Built in validators

-
      acceptance: function(value, attr, accept, model) {
+            
      acceptance: function(value, attr, accept, model) {
         if(value !== 'true' && (!_.isBoolean(value) || value === false)) {
           return this.format(defaultMessages.acceptance, this.formatLabel(attr, model));
         }
@@ -1300,7 +1300,7 @@ 

Built in validators

-
      min: function(value, attr, minValue, model) {
+            
      min: function(value, attr, minValue, model) {
         if (!isNumber(value) || value < minValue) {
           return this.format(defaultMessages.min, this.formatLabel(attr, model), minValue);
         }
@@ -1321,7 +1321,7 @@ 

Built in validators

-
      max: function(value, attr, maxValue, model) {
+            
      max: function(value, attr, maxValue, model) {
         if (!isNumber(value) || value > maxValue) {
           return this.format(defaultMessages.max, this.formatLabel(attr, model), maxValue);
         }
@@ -1342,7 +1342,7 @@ 

Built in validators

-
      range: function(value, attr, range, model) {
+            
      range: function(value, attr, range, model) {
         if(!isNumber(value) || value < range[0] || value > range[1]) {
           return this.format(defaultMessages.range, this.formatLabel(attr, model), range[0], range[1]);
         }
@@ -1363,7 +1363,7 @@ 

Built in validators

-
      length: function(value, attr, length, model) {
+            
      length: function(value, attr, length, model) {
         if (!_.isString(value) || value.length !== length) {
           return this.format(defaultMessages.length, this.formatLabel(attr, model), length);
         }
@@ -1384,7 +1384,7 @@ 

Built in validators

-
      minLength: function(value, attr, minLength, model) {
+            
      minLength: function(value, attr, minLength, model) {
         if (!_.isString(value) || value.length < minLength) {
           return this.format(defaultMessages.minLength, this.formatLabel(attr, model), minLength);
         }
@@ -1405,7 +1405,7 @@ 

Built in validators

-
      maxLength: function(value, attr, maxLength, model) {
+            
      maxLength: function(value, attr, maxLength, model) {
         if (!_.isString(value) || value.length > maxLength) {
           return this.format(defaultMessages.maxLength, this.formatLabel(attr, model), maxLength);
         }
@@ -1426,7 +1426,7 @@ 

Built in validators

-
      rangeLength: function(value, attr, range, model) {
+            
      rangeLength: function(value, attr, range, model) {
         if (!_.isString(value) || value.length < range[0] || value.length > range[1]) {
           return this.format(defaultMessages.rangeLength, this.formatLabel(attr, model), range[0], range[1]);
         }
@@ -1447,7 +1447,7 @@ 

Built in validators

-
      oneOf: function(value, attr, values, model) {
+            
      oneOf: function(value, attr, values, model) {
         if(!_.include(values, value)){
           return this.format(defaultMessages.oneOf, this.formatLabel(attr, model), values.join(', '));
         }
@@ -1468,7 +1468,7 @@ 

Built in validators

-
      equalTo: function(value, attr, equalTo, model, computed) {
+            
      equalTo: function(value, attr, equalTo, model, computed) {
         if(value !== computed[equalTo]) {
           return this.format(defaultMessages.equalTo, this.formatLabel(attr, model), this.formatLabel(equalTo, model));
         }
@@ -1489,7 +1489,7 @@ 

Built in validators

-
      pattern: function(value, attr, pattern, model) {
+            
      pattern: function(value, attr, pattern, model) {
         if (!hasValue(value) || !value.toString().match(defaultPatterns[pattern] || pattern)) {
           return this.format(defaultMessages[pattern] || defaultMessages.inlinePattern, this.formatLabel(attr, model), pattern);
         }
@@ -1511,7 +1511,7 @@ 

Built in validators

-
  _.each(defaultValidators, function(validator, key){
+            
  _.each(defaultValidators, function(validator, key){
     defaultValidators[key] = _.bind(defaultValidators[key], _.extend({}, formatFunctions, defaultValidators));
   });
 
diff --git a/src/backbone-validation.js b/src/backbone-validation.js
index 4e1da56a..3462da42 100644
--- a/src/backbone-validation.js
+++ b/src/backbone-validation.js
@@ -420,7 +420,9 @@ Backbone.Validation = (function(_){
     number: '{0} must be a number',
     email: '{0} must be a valid email',
     url: '{0} must be a valid url',
-    inlinePattern: '{0} is invalid'
+    inlinePattern: '{0} is invalid',
+    validModel: '{0} must be a validated model',
+    validCollection: '{0} must be a validated collection'
   };
 
   // Label formatters
@@ -613,6 +615,26 @@ Backbone.Validation = (function(_){
         if (!hasValue(value) || !value.toString().match(defaultPatterns[pattern] || pattern)) {
           return this.format(defaultMessages[pattern] || defaultMessages.inlinePattern, this.formatLabel(attr, model), pattern);
         }
+      },
+
+      // Model validator
+      // Validates that a (nested) model is valid as defined by it's own validations
+      validModel: function (value, attr, customValue, model) {
+          if (value && !value.isValid(true)) {
+              return this.format(defaultMessages.validModel, this.formatLabel(attr, model));
+          }
+      },
+
+      // Collection validator
+      // Validates that a (nested) collection of models is valid as defined by their own validations
+      validCollection: function (value, attr, customValue, model) {
+          var errors = value.map(function (entry) {
+              return entry.isValid(true);
+          });
+
+          if (_.indexOf(errors, false) !== -1) {
+            return this.format(defaultMessages.validCollection, this.formatLabel(attr, model));
+          }
       }
     };
   }());
diff --git a/tests/validators/validCollection.js b/tests/validators/validCollection.js
new file mode 100644
index 00000000..772a9314
--- /dev/null
+++ b/tests/validators/validCollection.js
@@ -0,0 +1,60 @@
+buster.testCase("validCollection validator", {
+    setUp: function() {
+        var that = this;
+        var ParentModel = Backbone.Model.extend({
+            validation: {
+                childCollection: {
+                    validCollection: true
+                }
+            }
+        });
+        var ChildModel = Backbone.Model.extend({
+            validation: {
+                name: {
+                    required: true
+                }
+            }
+        });
+        var ChildCollection = Backbone.Collection.extend({
+            model: ChildModel
+        });
+        var childCollection = new ChildCollection([{name:''}]);
+
+        Backbone.Validation.bind(new Backbone.View({collection: childCollection})); 
+        this.model = new ParentModel({childCollection:childCollection});
+        this.view = new Backbone.View({
+            model: this.model
+        });
+
+        Backbone.Validation.bind(this.view, {
+            valid: this.spy(),
+            invalid: this.spy()
+        });
+    },
+
+    "has default error message for string": function(done) {
+        this.model.bind('validated:invalid', function(model, error){
+            assert.equals({childCollection: 'Child collection must be a validated collection'}, error);
+            done();
+        });
+        this.model.isValid(true);
+    },
+    "has valid childCollection": function() {
+        this.model.get("childCollection").first().set("name", "Steve");
+        assert(this.model.isValid(true));
+    },
+    "has valid childCollection with multiple items": function() {
+        this.model.get("childCollection").first().set("name", "Steve");
+        this.model.get("childCollection").add({name:"Amy"});
+        this.model.get("childCollection").add({name:"John"});
+        assert(this.model.isValid(true));
+    },
+    "has invalid childCollection": function() {
+        refute(this.model.isValid(true));
+    },
+    "has invalid childCollection with multiple items": function() {
+        this.model.get("childCollection").add({name:"Amy"});
+        this.model.get("childCollection").add({name:"John"});
+        refute(this.model.isValid(true));
+    }
+});
\ No newline at end of file
diff --git a/tests/validators/validModel.js b/tests/validators/validModel.js
new file mode 100644
index 00000000..7b43682b
--- /dev/null
+++ b/tests/validators/validModel.js
@@ -0,0 +1,50 @@
+buster.testCase("validModel validator", {
+    setUp: function() {
+        var that = this;
+        var ParentModel = Backbone.Model.extend({
+            validation: {
+                childModel: {
+                    validModel: true
+                }
+            }
+        });
+        var ChildModel = Backbone.Model.extend({
+            validation: {
+                name: {
+                    required: true
+                }
+            }
+        });
+        this.childModel = new ChildModel();
+        this.childModel.set("name", '');
+        Backbone.Validation.bind(new Backbone.View({model: this.childModel})); // childModel requires a view for validation to work
+        this.model = new ParentModel({childModel:this.childModel});
+        this.view = new Backbone.View({
+            model: this.model
+        });
+
+        Backbone.Validation.bind(this.view, {
+            valid: this.spy(),
+            invalid: this.spy()
+        });
+    },
+
+    "has default error message for string": function(done) {
+        this.model.bind('validated:invalid', function(model, error){
+            assert.equals({childModel: 'Child model must be a validated model'}, error);
+            done();
+        });
+        this.model.isValid(true);
+    },
+    "has valid childModel": function() {
+        this.model.get("childModel").set("name", "Steve");
+        assert(this.model.isValid(true));
+    },
+    "has no childModel": function() {
+        this.model.set("childModel", null);
+        assert(this.model.isValid(true));
+    },
+    "has invalid childModel": function() {
+        refute(this.model.isValid(true));
+    }
+});
\ No newline at end of file