diff --git a/API.md b/API.md index 6c20b40..82139c0 100644 --- a/API.md +++ b/API.md @@ -46,6 +46,20 @@ You can also add a custom validation message by using `message-id` attribute. It ``` +### **Use a validation group**
+You can also add a `validation-group` directive to group many elements into a group. The group will be considered as valid if and only if one of them is valid. Otherwise, the group will be marked as invalid. A valid/invalid message will be placed inside an element that contains an id attribute with the same name as provided to the directive `validation-group`. + +```html + + + + + + +``` + +> Note that the `validation-group` directive can only be used to group elements placed in the same form. In other words, you can't group elements across different `
` tags. + ### **Don't show the Valid Message `no-validation-message="true"`** diff --git a/dist/angular-validation.js b/dist/angular-validation.js index ccef0c3..79d8d6a 100644 --- a/dist/angular-validation.js +++ b/dist/angular-validation.js @@ -377,9 +377,11 @@ var validFunc = function(element, validMessage, validation, scope, ctrl, attrs) { var messageToShow = validMessage || $validationProvider.getDefaultMsg(validation).success; var validCallback = $parse('success'); + var messageId = attrs.messageId; + var validationGroup = attrs.validationGroup; var messageElem; - if (attrs.messageId) messageElem = angular.element(document.querySelector('#' + attrs.messageId)); + if (messageId || validationGroup) messageElem = angular.element(document.querySelector('#' + (messageId || validationGroup))); else messageElem = element.next(); if (element.attr('no-validation-message')) { @@ -413,9 +415,11 @@ var invalidFunc = function(element, validMessage, validation, scope, ctrl, attrs) { var messageToShow = validMessage || $validationProvider.getDefaultMsg(validation).error; var invalidCallback = $parse('error'); + var messageId = attrs.messageId; + var validationGroup = attrs.validationGroup; var messageElem; - if (attrs.messageId) messageElem = angular.element(document.querySelector('#' + attrs.messageId)); + if (messageId || validationGroup) messageElem = angular.element(document.querySelector('#' + (messageId || validationGroup))); else messageElem = element.next(); if (element.attr('no-validation-message')) { @@ -436,6 +440,33 @@ return false; }; + /** + * Verify whether there is one of the elements inside the group valid. + * If so, it returns true, otherwise, it returns false + * + * @param scope + * @param element + * @param attrs + * @param ctrl + * @return {boolean} + */ + var checkValidationGroup = function(scope, element, attrs, ctrl) { + var validationGroup = attrs.validationGroup; + var validationGroupElems = document.querySelectorAll('*[validation-group=' + validationGroup + ']'); + var validationGroupElem; + + // Set the element to be invalid + ctrl.$setValidity(ctrl.$name, false); + + // Loop through all elements inside the group + for (var i = 0, len = validationGroupElems.length; i < len; i++) { + validationGroupElem = angular.element(validationGroupElems[i]); + + // If the element is valid and it's not the same element with the current checking element, returns true + if (validationGroupElem.hasClass('ng-valid') && validationGroupElem[0] !== element[0]) return true; + } + return false; + }; /** * collect elements for focus @@ -465,6 +496,7 @@ var successMessage = validator + 'SuccessMessage'; var errorMessage = validator + 'ErrorMessage'; var expression = $validationProvider.getExpression(validator); + var validationGroup = attrs.validationGroup; var valid = { success: function() { validFunc(element, attrs[successMessage], validator, scope, ctrl, attrs); @@ -489,7 +521,11 @@ return $q.all([$validationProvider.getExpression(validator)(value, scope, element, attrs, validatorParam)]) .then(function(data) { if (data && data.length > 0 && data[0]) return valid.success(); - else return valid.error(); + else if (validationGroup) { + // Whenever the element is invalid, we'll check whether one of the elements inside the its group valid or not. + // If there is a valid element, its invalid message won't be shown, Otherwise, shows its invalid message. + if (!checkValidationGroup(scope, element, attrs, ctrl)) valid.error(); + } else return valid.error(); }, function() { return valid.error(); }); @@ -499,11 +535,14 @@ else if (expression.constructor === RegExp) { // Only apply the test if the value is neither undefined or null if (value !== undefined && value !== null) return $validationProvider.getExpression(validator).test(value) ? valid.success() : valid.error(); - else return valid.error(); + else if (validationGroup) { + // Whenever the element is invalid, we'll check whether one of the elements inside the its group valid or not. + // If there is a valid element, its invalid message won't be shown, Otherwise, shows its invalid message. + if (!checkValidationGroup(scope, element, attrs, ctrl)) valid.error(); + } else return valid.error(); } else return valid.error(); }; - /** * generate unique guid */ @@ -519,6 +558,15 @@ restrict: 'A', require: 'ngModel', link: function(scope, element, attrs, ctrl) { + /** + * All attributes + */ + var validator = attrs.validator; + var messageId = attrs.messageId; + var validationGroup = attrs.validationGroup; + var validMethod = attrs.validMethod; + var ngModel = attrs.ngModel; + /** * watch * @type {watch} @@ -535,7 +583,7 @@ * * Convert user input String to Array */ - var validation = attrs.validator.split(','); + var validation = validator.split(','); /** * guid use @@ -554,7 +602,7 @@ /** * Default Valid/Invalid Message */ - if (!attrs.messageId) element.after(''); + if (!(messageId || validationGroup)) element.after(''); /** * Set custom initial validity @@ -579,7 +627,7 @@ ctrl.$setPristine(); ctrl.$setValidity(ctrl.$name, undefined); ctrl.$render(); - if (attrs.messageId) angular.element(document.querySelector('#' + attrs.messageId)).html(''); + if (messageId || validationGroup) angular.element(document.querySelector('#' + (messageId || validationGroup))).html(''); else element.next().html(''); if ($validationProvider.resetCallback) $validationProvider.resetCallback(element); @@ -589,9 +637,7 @@ /** * Check validator */ - - - var validMethod = (angular.isUndefined(attrs.validMethod)) ? $validationProvider.getValidMethod() : attrs.validMethod; + validMethod = (angular.isUndefined(validMethod)) ? $validationProvider.getValidMethod() : validMethod; /** * Click submit form, check the validity when submit @@ -606,7 +652,7 @@ // clear previous scope.$watch watch(); watch = scope.$watch(function() { - return scope.$eval(attrs.ngModel); + return scope.$eval(ngModel); }, function(value, oldValue) { // don't watch when init if (value === oldValue) { @@ -645,7 +691,7 @@ */ if (validMethod === 'blur') { element.bind('blur', function() { - var value = scope.$eval(attrs.ngModel); + var value = scope.$eval(ngModel); scope.$apply(function() { checkValidation(scope, element, attrs, ctrl, validation, value); }); @@ -666,7 +712,7 @@ * This is the default method */ scope.$watch(function() { - return scope.$eval(attrs.ngModel); + return scope.$eval(ngModel); }, function(value) { /** * dirty, pristine, viewValue control here @@ -676,7 +722,7 @@ ctrl.$setViewValue(ctrl.$viewValue); } else if (ctrl.$pristine) { // Don't validate form when the input is clean(pristine) - if (attrs.messageId) angular.element(document.querySelector('#' + attrs.messageId)).html(''); + if (messageId || validationGroup) angular.element(document.querySelector('#' + (messageId || validationGroup))).html(''); else element.next().html(''); return; } @@ -689,7 +735,7 @@ */ attrs.$observe('noValidationMessage', function(value) { var el; - if (attrs.messageId) el = angular.element(document.querySelector('#' + attrs.messageId)); + if (messageId || validationGroup) el = angular.element(document.querySelector('#' + (messageId || validationGroup))); else el = element.next(); if (value === 'true' || value === true) el.css('display', 'none'); else if (value === 'false' || value === false) el.css('display', 'block'); diff --git a/dist/angular-validation.min.js b/dist/angular-validation.min.js index fedc8ef..44abada 100644 --- a/dist/angular-validation.min.js +++ b/dist/angular-validation.min.js @@ -1 +1 @@ -(function(){angular.module("validation",["validation.provider","validation.directive"]),angular.module("validation.provider",[]),angular.module("validation.directive",["validation.provider"])}).call(this),function(){function a(){var a,b,c,d,e,f=this,g=function(f){a=f,b=a.get("$rootScope"),c=a.get("$http"),d=a.get("$q"),e=a.get("$timeout")},h={},i=null,j={};this.setExpression=function(a){return angular.extend(h,a),f},this.getExpression=function(a){return h[a]},this.setDefaultMsg=function(a){return angular.extend(j,a),f},this.getDefaultMsg=function(a){return j[a]},this.setValidMethod=function(a){i=a},this.getValidMethod=function(){return i},this.setErrorHTML=function(a){return a.constructor===Function?(f.getErrorHTML=a,f):void 0},this.getErrorHTML=function(a){return'

'+a+"

"},this.setSuccessHTML=function(a){return a.constructor===Function?(f.getSuccessHTML=a,f):void 0},this.getSuccessHTML=function(a){return'

'+a+"

"},this.showSuccessMessage=!0,this.showErrorMessage=!0,this.checkValid=function(a){return!(!a||!a.$valid)},this.validate=function(a){var c=d.defer(),g=0;if(void 0===a)return console.error("This is not a regular Form name scope"),c.reject("This is not a regular Form name scope"),c.promise;if(a.validationId)b.$broadcast(a.$name+"submit-"+a.validationId,g++);else if(a.constructor===Array)for(var h in a)b.$broadcast(a[h].$name+"submit-"+a[h].validationId,g++);else for(var i in a)"$"!==i[0]&&a[i].hasOwnProperty("$dirty")&&b.$broadcast(i+"submit-"+a[i].validationId,g++);return c.promise.success=function(a){return c.promise.then(function(b){a(b)}),c.promise},c.promise.error=function(a){return c.promise.then(null,function(b){a(b)}),c.promise},e(function(){f.checkValid(a)?c.resolve("success"):c.reject("error")}),c.promise},this.validCallback=null,this.invalidCallback=null,this.resetCallback=null,this.reset=function(a){if(void 0===a)return void console.error("This is not a regular Form name scope");if(a.validationId)b.$broadcast(a.$name+"reset-"+a.validationId);else if(a.constructor===Array)for(var c in a)b.$broadcast(a[c].$name+"reset-"+a[c].validationId);else for(var d in a)"$"!==d[0]&&a[d].hasOwnProperty("$dirty")&&b.$broadcast(d+"reset-"+a[d].validationId)},this.$get=["$injector",function(a){return g(a),{setValidMethod:this.setValidMethod,getValidMethod:this.getValidMethod,setErrorHTML:this.setErrorHTML,getErrorHTML:this.getErrorHTML,setSuccessHTML:this.setSuccessHTML,getSuccessHTML:this.getSuccessHTML,setExpression:this.setExpression,getExpression:this.getExpression,setDefaultMsg:this.setDefaultMsg,getDefaultMsg:this.getDefaultMsg,showSuccessMessage:this.showSuccessMessage,showErrorMessage:this.showErrorMessage,checkValid:this.checkValid,validate:this.validate,validCallback:this.validCallback,invalidCallback:this.invalidCallback,resetCallback:this.resetCallback,reset:this.reset}}]}angular.module("validation.provider").provider("$validation",a)}.call(this),function(){function a(a){var b=a.get("$validation"),c=a.get("$timeout"),d=a.get("$parse");return{link:function(a,e,f){var g=d(f.validationReset)(a);c(function(){e.on("click",function(a){a.preventDefault(),b.reset(g)})})}}}angular.module("validation.directive").directive("validationReset",a),a.$inject=["$injector"]}.call(this),function(){function a(a){var b=a.get("$validation"),c=a.get("$timeout"),d=a.get("$parse");return{priority:1,require:"?ngClick",link:function(a,e,f){var g=d(f.validationSubmit)(a);c(function(){e.off("click"),e.on("click",function(c){c.preventDefault(),b.validate(g).success(function(){d(f.ngClick)(a)})})})}}}angular.module("validation.directive").directive("validationSubmit",a),a.$inject=["$injector"]}.call(this),function(){function a(a){var b=a.get("$validation"),c=a.get("$q"),d=a.get("$timeout"),e=a.get("$parse"),f=function(a,c,d,f,g,h){var i,j=c||b.getDefaultMsg(d).success,k=e("success");return i=h.messageId?angular.element(document.querySelector("#"+h.messageId)):a.next(),a.attr("no-validation-message")?i.css("display","none"):b.showSuccessMessage&&j?(i.html(b.getSuccessHTML(j)),i.css("display","")):i.css("display","none"),g.$setValidity(g.$name,!0),k&&k({message:j}),b.validCallback&&b.validCallback(a),!0},g=function(a,c,d,f,g,h){var i,j=c||b.getDefaultMsg(d).error,k=e("error");return i=h.messageId?angular.element(document.querySelector("#"+h.messageId)):a.next(),a.attr("no-validation-message")?i.css("display","none"):b.showErrorMessage&&j?(i.html(b.getErrorHTML(j)),i.css("display","")):i.css("display","none"),g.$setValidity(g.$name,!1),k&&k({message:j}),b.invalidCallback&&b.invalidCallback(a),!1},h={},i=function(a,d,e,h,j,k){var l=j.slice(0),m=l[0].trim(),n=m.indexOf("="),o=-1===n?m:m.substr(0,n),p=-1===n?null:m.substr(n+1),q=l.slice(1),r=o+"SuccessMessage",s=o+"ErrorMessage",t=b.getExpression(o),u={success:function(){return f(d,e[r],o,a,h,e),q.length?i(a,d,e,h,q,k):!0},error:function(){return g(d,e[s],o,a,h,e)}};return void 0===t?(console.error('You are using undefined validator "%s"',o),q.length?i(a,d,e,h,q,k):void 0):t.constructor===Function?c.all([b.getExpression(o)(k,a,d,e,p)]).then(function(a){return a&&a.length>0&&a[0]?u.success():u.error()},function(){return u.error()}):t.constructor===RegExp&&void 0!==k&&null!==k&&b.getExpression(o).test(k)?u.success():u.error()},j=function(){return(65536*(1+Math.random())|0).toString(16).substring(1)},k=function(){return j()+j()+j()+j()};return{restrict:"A",require:"ngModel",link:function(a,c,e,f){var g,j=function(){},l=e.validator.split(","),m=f.validationId=k();"boolean"==typeof a.initialValidity&&(g=a.initialValidity),e.messageId||c.after(""),f.$setValidity(f.$name,g),a.$on(f.$name+"reset-"+m,function(){j(),d(function(){f.$setViewValue(""),f.$setPristine(),f.$setValidity(f.$name,void 0),f.$render(),e.messageId?angular.element(document.querySelector("#"+e.messageId)).html(""):c.next().html(""),b.resetCallback&&b.resetCallback(c)})});var n=angular.isUndefined(e.validMethod)?b.getValidMethod():e.validMethod;return a.$on(f.$name+"submit-"+m,function(b,g){var k=f.$viewValue,m=!1;m=i(a,c,e,f,l,k),"submit"===n&&(j(),j=a.$watch(function(){return a.$eval(e.ngModel)},function(b,d){b!==d&&((void 0===b||null===b)&&(b=""),m=i(a,c,e,f,l,b))}));var o=function(a){a?delete h[g]:(h[g]=c[0],d(function(){h[Math.min.apply(null,Object.keys(h))].focus()},0))};m.constructor===Object?m.then(o):o(m)}),"blur"===n?void c.bind("blur",function(){var b=a.$eval(e.ngModel);a.$apply(function(){i(a,c,e,f,l,b)})}):void("submit"!==n&&"submit-only"!==n&&(a.$watch(function(){return a.$eval(e.ngModel)},function(b){if(f.$pristine&&f.$viewValue)f.$setViewValue(f.$viewValue);else if(f.$pristine)return void(e.messageId?angular.element(document.querySelector("#"+e.messageId)).html(""):c.next().html(""));i(a,c,e,f,l,b)}),d(function(){e.$observe("noValidationMessage",function(a){var b;b=e.messageId?angular.element(document.querySelector("#"+e.messageId)):c.next(),"true"===a||a===!0?b.css("display","none"):("false"===a||a===!1)&&b.css("display","block")})})))}}}angular.module("validation.directive").directive("validator",a),a.$inject=["$injector"]}.call(this); \ No newline at end of file +(function(){angular.module("validation",["validation.provider","validation.directive"]),angular.module("validation.provider",[]),angular.module("validation.directive",["validation.provider"])}).call(this),function(){function a(){var a,b,c,d,e,f=this,g=function(f){a=f,b=a.get("$rootScope"),c=a.get("$http"),d=a.get("$q"),e=a.get("$timeout")},h={},i=null,j={};this.setExpression=function(a){return angular.extend(h,a),f},this.getExpression=function(a){return h[a]},this.setDefaultMsg=function(a){return angular.extend(j,a),f},this.getDefaultMsg=function(a){return j[a]},this.setValidMethod=function(a){i=a},this.getValidMethod=function(){return i},this.setErrorHTML=function(a){return a.constructor===Function?(f.getErrorHTML=a,f):void 0},this.getErrorHTML=function(a){return'

'+a+"

"},this.setSuccessHTML=function(a){return a.constructor===Function?(f.getSuccessHTML=a,f):void 0},this.getSuccessHTML=function(a){return'

'+a+"

"},this.showSuccessMessage=!0,this.showErrorMessage=!0,this.checkValid=function(a){return!(!a||!a.$valid)},this.validate=function(a){var c=d.defer(),g=0;if(void 0===a)return console.error("This is not a regular Form name scope"),c.reject("This is not a regular Form name scope"),c.promise;if(a.validationId)b.$broadcast(a.$name+"submit-"+a.validationId,g++);else if(a.constructor===Array)for(var h in a)b.$broadcast(a[h].$name+"submit-"+a[h].validationId,g++);else for(var i in a)"$"!==i[0]&&a[i].hasOwnProperty("$dirty")&&b.$broadcast(i+"submit-"+a[i].validationId,g++);return c.promise.success=function(a){return c.promise.then(function(b){a(b)}),c.promise},c.promise.error=function(a){return c.promise.then(null,function(b){a(b)}),c.promise},e(function(){f.checkValid(a)?c.resolve("success"):c.reject("error")}),c.promise},this.validCallback=null,this.invalidCallback=null,this.resetCallback=null,this.reset=function(a){if(void 0===a)return void console.error("This is not a regular Form name scope");if(a.validationId)b.$broadcast(a.$name+"reset-"+a.validationId);else if(a.constructor===Array)for(var c in a)b.$broadcast(a[c].$name+"reset-"+a[c].validationId);else for(var d in a)"$"!==d[0]&&a[d].hasOwnProperty("$dirty")&&b.$broadcast(d+"reset-"+a[d].validationId)},this.$get=["$injector",function(a){return g(a),{setValidMethod:this.setValidMethod,getValidMethod:this.getValidMethod,setErrorHTML:this.setErrorHTML,getErrorHTML:this.getErrorHTML,setSuccessHTML:this.setSuccessHTML,getSuccessHTML:this.getSuccessHTML,setExpression:this.setExpression,getExpression:this.getExpression,setDefaultMsg:this.setDefaultMsg,getDefaultMsg:this.getDefaultMsg,showSuccessMessage:this.showSuccessMessage,showErrorMessage:this.showErrorMessage,checkValid:this.checkValid,validate:this.validate,validCallback:this.validCallback,invalidCallback:this.invalidCallback,resetCallback:this.resetCallback,reset:this.reset}}]}angular.module("validation.provider").provider("$validation",a)}.call(this),function(){function a(a){var b=a.get("$validation"),c=a.get("$timeout"),d=a.get("$parse");return{link:function(a,e,f){var g=d(f.validationReset)(a);c(function(){e.on("click",function(a){a.preventDefault(),b.reset(g)})})}}}angular.module("validation.directive").directive("validationReset",a),a.$inject=["$injector"]}.call(this),function(){function a(a){var b=a.get("$validation"),c=a.get("$timeout"),d=a.get("$parse");return{priority:1,require:"?ngClick",link:function(a,e,f){var g=d(f.validationSubmit)(a);c(function(){e.off("click"),e.on("click",function(c){c.preventDefault(),b.validate(g).success(function(){d(f.ngClick)(a)})})})}}}angular.module("validation.directive").directive("validationSubmit",a),a.$inject=["$injector"]}.call(this),function(){function a(a){var b=a.get("$validation"),c=a.get("$q"),d=a.get("$timeout"),e=a.get("$parse"),f=function(a,c,d,f,g,h){var i,j=c||b.getDefaultMsg(d).success,k=e("success"),l=h.messageId,m=h.validationGroup;return i=l||m?angular.element(document.querySelector("#"+(l||m))):a.next(),a.attr("no-validation-message")?i.css("display","none"):b.showSuccessMessage&&j?(i.html(b.getSuccessHTML(j)),i.css("display","")):i.css("display","none"),g.$setValidity(g.$name,!0),k&&k({message:j}),b.validCallback&&b.validCallback(a),!0},g=function(a,c,d,f,g,h){var i,j=c||b.getDefaultMsg(d).error,k=e("error"),l=h.messageId,m=h.validationGroup;return i=l||m?angular.element(document.querySelector("#"+(l||m))):a.next(),a.attr("no-validation-message")?i.css("display","none"):b.showErrorMessage&&j?(i.html(b.getErrorHTML(j)),i.css("display","")):i.css("display","none"),g.$setValidity(g.$name,!1),k&&k({message:j}),b.invalidCallback&&b.invalidCallback(a),!1},h=function(a,b,c,d){var e,f=c.validationGroup,g=document.querySelectorAll("*[validation-group="+f+"]");d.$setValidity(d.$name,!1);for(var h=0,i=g.length;i>h;h++)if(e=angular.element(g[h]),e.hasClass("ng-valid")&&e[0]!==b[0])return!0;return!1},i={},j=function(a,d,e,i,k,l){var m=k.slice(0),n=m[0].trim(),o=n.indexOf("="),p=-1===o?n:n.substr(0,o),q=-1===o?null:n.substr(o+1),r=m.slice(1),s=p+"SuccessMessage",t=p+"ErrorMessage",u=b.getExpression(p),v=e.validationGroup,w={success:function(){return f(d,e[s],p,a,i,e),r.length?j(a,d,e,i,r,l):!0},error:function(){return g(d,e[t],p,a,i,e)}};return void 0===u?(console.error('You are using undefined validator "%s"',p),r.length?j(a,d,e,i,r,l):void 0):u.constructor===Function?c.all([b.getExpression(p)(l,a,d,e,q)]).then(function(b){return b&&b.length>0&&b[0]?w.success():v?void(h(a,d,e,i)||w.error()):w.error()},function(){return w.error()}):u.constructor!==RegExp?w.error():void 0!==l&&null!==l?b.getExpression(p).test(l)?w.success():w.error():v?void(h(a,d,e,i)||w.error()):w.error()},k=function(){return(65536*(1+Math.random())|0).toString(16).substring(1)},l=function(){return k()+k()+k()+k()};return{restrict:"A",require:"ngModel",link:function(a,c,e,f){var g,h=e.validator,k=e.messageId,m=e.validationGroup,n=e.validMethod,o=e.ngModel,p=function(){},q=h.split(","),r=f.validationId=l();return"boolean"==typeof a.initialValidity&&(g=a.initialValidity),k||m||c.after(""),f.$setValidity(f.$name,g),a.$on(f.$name+"reset-"+r,function(){p(),d(function(){f.$setViewValue(""),f.$setPristine(),f.$setValidity(f.$name,void 0),f.$render(),k||m?angular.element(document.querySelector("#"+(k||m))).html(""):c.next().html(""),b.resetCallback&&b.resetCallback(c)})}),n=angular.isUndefined(n)?b.getValidMethod():n,a.$on(f.$name+"submit-"+r,function(b,g){var h=f.$viewValue,k=!1;k=j(a,c,e,f,q,h),"submit"===n&&(p(),p=a.$watch(function(){return a.$eval(o)},function(b,d){b!==d&&((void 0===b||null===b)&&(b=""),k=j(a,c,e,f,q,b))}));var l=function(a){a?delete i[g]:(i[g]=c[0],d(function(){i[Math.min.apply(null,Object.keys(i))].focus()},0))};k.constructor===Object?k.then(l):l(k)}),"blur"===n?void c.bind("blur",function(){var b=a.$eval(o);a.$apply(function(){j(a,c,e,f,q,b)})}):void("submit"!==n&&"submit-only"!==n&&(a.$watch(function(){return a.$eval(o)},function(b){if(f.$pristine&&f.$viewValue)f.$setViewValue(f.$viewValue);else if(f.$pristine)return void(k||m?angular.element(document.querySelector("#"+(k||m))).html(""):c.next().html(""));j(a,c,e,f,q,b)}),d(function(){e.$observe("noValidationMessage",function(a){var b;b=k||m?angular.element(document.querySelector("#"+(k||m))):c.next(),"true"===a||a===!0?b.css("display","none"):("false"===a||a===!1)&&b.css("display","block")})})))}}}angular.module("validation.directive").directive("validator",a),a.$inject=["$injector"]}.call(this); \ No newline at end of file diff --git a/index.html b/index.html index 2b6cedf..06fdbb6 100644 --- a/index.html +++ b/index.html @@ -329,9 +329,19 @@

Examples

-
- - +
+
+ +
+
+ +
diff --git a/src/validator.directive.js b/src/validator.directive.js index 045e0e3..9a70e36 100644 --- a/src/validator.directive.js +++ b/src/validator.directive.js @@ -21,9 +21,11 @@ var validFunc = function(element, validMessage, validation, scope, ctrl, attrs) { var messageToShow = validMessage || $validationProvider.getDefaultMsg(validation).success; var validCallback = $parse('success'); + var messageId = attrs.messageId; + var validationGroup = attrs.validationGroup; var messageElem; - if (attrs.messageId) messageElem = angular.element(document.querySelector('#' + attrs.messageId)); + if (messageId || validationGroup) messageElem = angular.element(document.querySelector('#' + (messageId || validationGroup))); else messageElem = element.next(); if (element.attr('no-validation-message')) { @@ -57,9 +59,11 @@ var invalidFunc = function(element, validMessage, validation, scope, ctrl, attrs) { var messageToShow = validMessage || $validationProvider.getDefaultMsg(validation).error; var invalidCallback = $parse('error'); + var messageId = attrs.messageId; + var validationGroup = attrs.validationGroup; var messageElem; - if (attrs.messageId) messageElem = angular.element(document.querySelector('#' + attrs.messageId)); + if (messageId || validationGroup) messageElem = angular.element(document.querySelector('#' + (messageId || validationGroup))); else messageElem = element.next(); if (element.attr('no-validation-message')) { @@ -80,6 +84,33 @@ return false; }; + /** + * Verify whether there is one of the elements inside the group valid. + * If so, it returns true, otherwise, it returns false + * + * @param scope + * @param element + * @param attrs + * @param ctrl + * @return {boolean} + */ + var checkValidationGroup = function(scope, element, attrs, ctrl) { + var validationGroup = attrs.validationGroup; + var validationGroupElems = document.querySelectorAll('*[validation-group=' + validationGroup + ']'); + var validationGroupElem; + + // Set the element to be invalid + ctrl.$setValidity(ctrl.$name, false); + + // Loop through all elements inside the group + for (var i = 0, len = validationGroupElems.length; i < len; i++) { + validationGroupElem = angular.element(validationGroupElems[i]); + + // If the element is valid and it's not the same element with the current checking element, returns true + if (validationGroupElem.hasClass('ng-valid') && validationGroupElem[0] !== element[0]) return true; + } + return false; + }; /** * collect elements for focus @@ -109,6 +140,7 @@ var successMessage = validator + 'SuccessMessage'; var errorMessage = validator + 'ErrorMessage'; var expression = $validationProvider.getExpression(validator); + var validationGroup = attrs.validationGroup; var valid = { success: function() { validFunc(element, attrs[successMessage], validator, scope, ctrl, attrs); @@ -133,7 +165,11 @@ return $q.all([$validationProvider.getExpression(validator)(value, scope, element, attrs, validatorParam)]) .then(function(data) { if (data && data.length > 0 && data[0]) return valid.success(); - else return valid.error(); + else if (validationGroup) { + // Whenever the element is invalid, we'll check whether one of the elements inside the its group valid or not. + // If there is a valid element, its invalid message won't be shown, Otherwise, shows its invalid message. + if (!checkValidationGroup(scope, element, attrs, ctrl)) valid.error(); + } else return valid.error(); }, function() { return valid.error(); }); @@ -143,11 +179,14 @@ else if (expression.constructor === RegExp) { // Only apply the test if the value is neither undefined or null if (value !== undefined && value !== null) return $validationProvider.getExpression(validator).test(value) ? valid.success() : valid.error(); - else return valid.error(); + else if (validationGroup) { + // Whenever the element is invalid, we'll check whether one of the elements inside the its group valid or not. + // If there is a valid element, its invalid message won't be shown, Otherwise, shows its invalid message. + if (!checkValidationGroup(scope, element, attrs, ctrl)) valid.error(); + } else return valid.error(); } else return valid.error(); }; - /** * generate unique guid */ @@ -163,6 +202,15 @@ restrict: 'A', require: 'ngModel', link: function(scope, element, attrs, ctrl) { + /** + * All attributes + */ + var validator = attrs.validator; + var messageId = attrs.messageId; + var validationGroup = attrs.validationGroup; + var validMethod = attrs.validMethod; + var ngModel = attrs.ngModel; + /** * watch * @type {watch} @@ -179,7 +227,7 @@ * * Convert user input String to Array */ - var validation = attrs.validator.split(','); + var validation = validator.split(','); /** * guid use @@ -198,7 +246,7 @@ /** * Default Valid/Invalid Message */ - if (!attrs.messageId) element.after(''); + if (!(messageId || validationGroup)) element.after(''); /** * Set custom initial validity @@ -223,7 +271,7 @@ ctrl.$setPristine(); ctrl.$setValidity(ctrl.$name, undefined); ctrl.$render(); - if (attrs.messageId) angular.element(document.querySelector('#' + attrs.messageId)).html(''); + if (messageId || validationGroup) angular.element(document.querySelector('#' + (messageId || validationGroup))).html(''); else element.next().html(''); if ($validationProvider.resetCallback) $validationProvider.resetCallback(element); @@ -233,9 +281,7 @@ /** * Check validator */ - - - var validMethod = (angular.isUndefined(attrs.validMethod)) ? $validationProvider.getValidMethod() : attrs.validMethod; + validMethod = (angular.isUndefined(validMethod)) ? $validationProvider.getValidMethod() : validMethod; /** * Click submit form, check the validity when submit @@ -250,7 +296,7 @@ // clear previous scope.$watch watch(); watch = scope.$watch(function() { - return scope.$eval(attrs.ngModel); + return scope.$eval(ngModel); }, function(value, oldValue) { // don't watch when init if (value === oldValue) { @@ -289,7 +335,7 @@ */ if (validMethod === 'blur') { element.bind('blur', function() { - var value = scope.$eval(attrs.ngModel); + var value = scope.$eval(ngModel); scope.$apply(function() { checkValidation(scope, element, attrs, ctrl, validation, value); }); @@ -310,7 +356,7 @@ * This is the default method */ scope.$watch(function() { - return scope.$eval(attrs.ngModel); + return scope.$eval(ngModel); }, function(value) { /** * dirty, pristine, viewValue control here @@ -320,7 +366,7 @@ ctrl.$setViewValue(ctrl.$viewValue); } else if (ctrl.$pristine) { // Don't validate form when the input is clean(pristine) - if (attrs.messageId) angular.element(document.querySelector('#' + attrs.messageId)).html(''); + if (messageId || validationGroup) angular.element(document.querySelector('#' + (messageId || validationGroup))).html(''); else element.next().html(''); return; } @@ -333,7 +379,7 @@ */ attrs.$observe('noValidationMessage', function(value) { var el; - if (attrs.messageId) el = angular.element(document.querySelector('#' + attrs.messageId)); + if (messageId || validationGroup) el = angular.element(document.querySelector('#' + (messageId || validationGroup))); else el = element.next(); if (value === 'true' || value === true) el.css('display', 'none'); else if (value === 'false' || value === false) el.css('display', 'block'); diff --git a/test/unit/validationGroupSpec.js b/test/unit/validationGroupSpec.js new file mode 100644 index 0000000..c25fd1a --- /dev/null +++ b/test/unit/validationGroupSpec.js @@ -0,0 +1,269 @@ +'use strict'; + +describe('validation-group directive', function() { + var $scope; + var $rootScope; + var $compile; + var $timeout; + var validationProvider; + var element; + + beforeEach(module('validation.directive')); + beforeEach(module('validation.rule')); + + describe('validation-group attribute for checkbox elements', function() { + var messageElem; + + beforeEach(inject(function($injector) { + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); + $scope = $rootScope.$new(); + + element = $compile('')($scope); + angular.element(document.body).append(element); + $scope.$digest(); + })); + + afterEach(function() { + element.remove(); + element = null; + }); + + it('should be pristine', function() { + expect($scope.Form.$pristine).toBe(true); + expect(element.hasClass('ng-pristine')).toBe(true); + expect($scope.Form.$valid).toBeUndefined(true); + expect($scope.Form.$invalid).toBeUndefined(true); + }); + + it('should be dirty and valid', function() { + $scope.Form.checkbox1.$setViewValue(true); + $scope.Form.checkbox2.$setViewValue(true); + + expect($scope.Form.$dirty).toBe(true); + expect(element.hasClass('ng-dirty')).toBe(true); + expect($scope.Form.$valid).toBe(true); + expect(element.hasClass('ng-valid')).toBe(true); + }); + + it('should be dirty and invalid', function() { + $scope.Form.checkbox1.$setViewValue(true); + $scope.Form.checkbox2.$setViewValue(true); + $scope.Form.checkbox1.$setViewValue(false); + $scope.Form.checkbox2.$setViewValue(false); + + expect($scope.Form.$dirty).toBe(true); + expect(element.hasClass('ng-dirty')).toBe(true); + expect($scope.Form.$invalid).toBe(true); + expect(element.hasClass('ng-invalid')).toBe(true); + }); + + it('should have a success message inside the #checkbox element when an element is valid', function() { + $scope.Form.checkbox1.$setViewValue(true); + + messageElem = angular.element(element[0].querySelector('#checkbox > p')); + expect(messageElem.hasClass('validation-valid')).toBe(true); + }); + + it('should have an error message inside the #checkbox element when no element is valid', function() { + $scope.Form.checkbox1.$setViewValue(true); + $scope.Form.checkbox1.$setViewValue(false); + + messageElem = angular.element(element[0].querySelector('#checkbox > p')); + expect(messageElem.hasClass('validation-invalid')).toBe(true); + }); + + it('should have a success message inside the #checkbox element when both elements are valid', function() { + $scope.Form.checkbox1.$setViewValue(true); + $scope.Form.checkbox2.$setViewValue(true); + + messageElem = angular.element(element[0].querySelector('#checkbox > p')); + expect(messageElem.hasClass('validation-valid')).toBe(true); + }); + + it('should have a success message inside the #checkbox element when one of element is valid', function() { + $scope.Form.checkbox1.$setViewValue(true); + $scope.Form.checkbox2.$setViewValue(true); + $scope.Form.checkbox1.$setViewValue(false); + + messageElem = angular.element(element[0].querySelector('#checkbox > p')); + expect(messageElem.hasClass('validation-valid')).toBe(true); + }); + + it('should have an error message inside the #checkbox element when both of elements are invalid', function() { + $scope.Form.checkbox1.$setViewValue(true); + $scope.Form.checkbox2.$setViewValue(true); + $scope.Form.checkbox1.$setViewValue(false); + $scope.Form.checkbox2.$setViewValue(false); + + messageElem = angular.element(element[0].querySelector('#checkbox > p')); + expect(messageElem.hasClass('validation-invalid')).toBe(true); + }); + }); + + describe('validation-group attribute for any elements', function() { + var messageElem; + + beforeEach(inject(function($injector) { + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); + $scope = $rootScope.$new(); + + element = $compile('
')($scope); + angular.element(document.body).append(element); + $scope.$digest(); + })); + + afterEach(function() { + element.remove(); + element = null; + }); + + it('should be pristine', function() { + expect($scope.Form.$pristine).toBe(true); + expect(element.hasClass('ng-pristine')).toBe(true); + expect($scope.Form.$valid).toBeUndefined(true); + expect($scope.Form.$invalid).toBeUndefined(true); + }); + + it('should be dirty and valid', function() { + $scope.Form.email.$setViewValue('foo@bar.com'); + $scope.Form.telephone.$setViewValue('065839481'); + + expect($scope.Form.$dirty).toBe(true); + expect(element.hasClass('ng-dirty')).toBe(true); + expect($scope.Form.$valid).toBe(true); + expect(element.hasClass('ng-valid')).toBe(true); + }); + + it('should be dirty and invalid', function() { + $scope.Form.email.$setViewValue('foo@bar.com'); + $scope.Form.telephone.$setViewValue('065839481'); + $scope.Form.email.$setViewValue(); + $scope.Form.telephone.$setViewValue(); + + expect($scope.Form.$dirty).toBe(true); + expect(element.hasClass('ng-dirty')).toBe(true); + expect($scope.Form.$invalid).toBe(true); + expect(element.hasClass('ng-invalid')).toBe(true); + }); + + it('should have a success message inside the #contact element when an element is valid', function() { + $scope.Form.email.$setViewValue('foo@bar.com'); + + messageElem = angular.element(element[0].querySelector('#contact > p')); + expect(messageElem.hasClass('validation-valid')).toBe(true); + }); + + it('should have an error message inside the #contact element when no element is valid', function() { + $scope.Form.email.$setViewValue('foo@bar.com'); + $scope.Form.email.$setViewValue(); + + messageElem = angular.element(element[0].querySelector('#contact > p')); + expect(messageElem.hasClass('validation-invalid')).toBe(true); + }); + + it('should have a success message inside the #contact element when both elements are valid', function() { + $scope.Form.email.$setViewValue('foo@bar.com'); + $scope.Form.telephone.$setViewValue('065839481'); + + messageElem = angular.element(element[0].querySelector('#contact > p')); + expect(messageElem.hasClass('validation-valid')).toBe(true); + }); + + it('should have a success message inside the #contact element when one of element is valid', function() { + $scope.Form.email.$setViewValue('foo@bar.com'); + $scope.Form.telephone.$setViewValue('065839481'); + $scope.Form.email.$setViewValue(); + + messageElem = angular.element(element[0].querySelector('#contact > p')); + expect(messageElem.hasClass('validation-valid')).toBe(true); + }); + + it('should have an error message inside the #contact element when both of elements are invalid', function() { + $scope.Form.email.$setViewValue('foo@bar.com'); + $scope.Form.telephone.$setViewValue('065839481'); + $scope.Form.email.$setViewValue(); + $scope.Form.telephone.$setViewValue(); + + messageElem = angular.element(element[0].querySelector('#contact > p')); + expect(messageElem.hasClass('validation-invalid')).toBe(true); + }); + }); + + describe('validation-group attribute validated by using the provider', function() { + var successSpy; + var errorSpy; + beforeEach(inject(function($injector) { + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); + validationProvider = $injector.get('$validation'); + $timeout = $injector.get('$timeout'); + $scope = $rootScope.$new(); + + element = $compile('
')($scope); + angular.element(document.body).append(element); + $scope.$digest(); + })); + + afterEach(function() { + element.remove(); + element = null; + }); + + it('should validate a form and call a success callback', function() { + successSpy = jasmine.createSpy('successSpy'); + errorSpy = jasmine.createSpy('errorSpy'); + + $scope.Form.checkbox1.$setViewValue(true); + $scope.Form.checkbox2.$setViewValue(true); + + validationProvider.validate($scope.Form) + .success(function() { + successSpy(); + }) + .error(function() { + errorSpy(); + }); + $timeout.flush(); + expect(successSpy).toHaveBeenCalled(); + expect(errorSpy).not.toHaveBeenCalled(); + }); + + it('should validate a form element and call a success callback', function() { + successSpy = jasmine.createSpy('successSpy'); + errorSpy = jasmine.createSpy('errorSpy'); + + $scope.Form.checkbox1.$setViewValue(true); + + validationProvider.validate($scope.Form.checkbox1) + .success(function() { + successSpy(); + }) + .error(function() { + errorSpy(); + }); + $timeout.flush(); + expect(successSpy).toHaveBeenCalled(); + expect(errorSpy).not.toHaveBeenCalled(); + }); + + it('should validate a form element and call an error callback', function() { + successSpy = jasmine.createSpy('successSpy'); + errorSpy = jasmine.createSpy('errorSpy'); + + $scope.Form.checkbox1.$setViewValue(true); + + validationProvider.validate($scope.Form) + .success(function() { + successSpy(); + }) + .error(function() { + errorSpy(); + }); + $timeout.flush(); + expect(successSpy).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalled(); + }); + }); +});